aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.thicket/tickets.jsonl18
-rw-r--r--frontend/coverage/clover.xml34
-rw-r--r--frontend/coverage/coverage-final.json4
-rw-r--r--frontend/coverage/index.html34
-rw-r--r--frontend/coverage/src/App.css.html117
-rw-r--r--frontend/coverage/src/App.tsx.html53
-rw-r--r--frontend/coverage/src/components/FeedList.css.html226
-rw-r--r--frontend/coverage/src/components/FeedList.tsx.html235
-rw-r--r--frontend/coverage/src/components/Login.css.html2
-rw-r--r--frontend/coverage/src/components/Login.tsx.html2
-rw-r--r--frontend/coverage/src/components/index.html44
-rw-r--r--frontend/coverage/src/index.html2
-rw-r--r--frontend/src/App.css78
-rw-r--r--frontend/src/App.test.tsx10
-rw-r--r--frontend/src/App.tsx23
-rw-r--r--frontend/src/components/FeedList.css48
-rw-r--r--frontend/src/components/FeedList.test.tsx61
-rw-r--r--frontend/src/components/FeedList.tsx50
-rw-r--r--frontend/src/components/Login.test.tsx2
-rw-r--r--frontend/src/types.ts7
-rw-r--r--frontend/tsconfig.app.json22
21 files changed, 945 insertions, 127 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl
index c3ca9e5..e3174e1 100644
--- a/.thicket/tickets.jsonl
+++ b/.thicket/tickets.jsonl
@@ -1,18 +1,19 @@
-{"id":"NK-0nf7hu","title":"Implement Frontend Logout","description":"Add logout button/link in dashboard and call /api/logout.","type":"","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:50:46.760744241Z","updated":"2026-02-13T05:50:46.760744241Z"}
-{"id":"NK-0ppv3f","title":"Implement Frontend Settings","description":"Create settings page for managing feeds/categories.","type":"","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.631640578Z","updated":"2026-02-13T05:44:01.631640578Z"}
+{"id":"NK-0nf7hu","title":"Implement Frontend Logout","description":"Add logout button/link in dashboard and call /api/logout.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:50:46.760744241Z","updated":"2026-02-13T05:50:46.760744241Z"}
+{"id":"NK-0ppv3f","title":"Implement Frontend Settings","description":"Create settings page for managing feeds/categories.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.631640578Z","updated":"2026-02-13T05:44:01.631640578Z"}
{"id":"NK-1phdpf","title":"refactor backend to have a clean API","description":"create a nice clean API for the backend GO code that is more independent of the frontend\n\nensure that it is working with good tests","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T01:52:49.8322638Z","updated":"2026-02-13T04:26:47.517515371Z"}
-{"id":"NK-27or4b","title":"Increase Test Coverage to \u003e80%","description":"Project-wide test coverage is currently ~63%. Key gaps are in the new and packages, as well as some core model logic. Increase coverage to at least 80% to ensure stability.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:03:09.677147894Z","updated":"2026-02-13T05:03:09.677147894Z"}
-{"id":"NK-6q9nyg","title":"Refactor HTTP-dependent functions for testability","description":"Several functions use http.Get or external libraries directly (GetFullContent uses goose, ResolveFeedURL uses http.Get + goquery, imageProxyHandler uses http.Client). Refactor these to accept interfaces for HTTP fetching so they can be unit tested with mocks. This is the primary blocker for reaching 90% coverage.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:37.630148644Z","updated":"2026-02-13T03:54:37.630148644Z"}
+{"id":"NK-27or4b","title":"Increase Test Coverage to \u003e80%","description":"Project-wide test coverage is currently ~63%. Key gaps are in the new and packages, as well as some core model logic. Increase coverage to at least 80% to ensure stability.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T05:03:09.677147894Z","updated":"2026-02-13T05:03:09.677147894Z"}
+{"id":"NK-3om7x2","title":"Implement Feed Items View","description":"Create a component to display items for a selected feed. Fetch items from /api/stream?feed_id=...","type":"","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:59:46.161356437Z","updated":"2026-02-13T05:59:46.161356437Z"}
+{"id":"NK-6q9nyg","title":"Refactor HTTP-dependent functions for testability","description":"Several functions use http.Get or external libraries directly (GetFullContent uses goose, ResolveFeedURL uses http.Get + goquery, imageProxyHandler uses http.Client). Refactor these to accept interfaces for HTTP fetching so they can be unit tested with mocks. This is the primary blocker for reaching 90% coverage.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:37.630148644Z","updated":"2026-02-13T03:54:37.630148644Z"}
{"id":"NK-7tzbql","title":"Fix TUI Content View Navigation and Interaction","description":"The TUI content view (reading a single item) is currently non-functional or severely limited. Users cannot easily navigate back, scroll, or interact with the content. This task involves improving the 'viewContent' state in the TUI.","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:02:57.382793121Z","updated":"2026-02-13T05:06:15.144485446Z"}
{"id":"NK-9hx0y7","title":"Implement Frontend Login","description":"Create login page and auth logic in the new React frontend. Port functionality from legacy login.html.","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.546395342Z","updated":"2026-02-13T05:50:33.877452063Z"}
{"id":"NK-bsdwqz","title":"terminal UI","description":"once there is good test coverage and a clean backend API, work on a nice efficient TUI with https://github.com/charmbracelet/bubbletea","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T01:54:02.285738454Z","updated":"2026-02-13T04:42:09.824268427Z"}
-{"id":"NK-ed1iah","title":"Make feed crawling async in API","description":"Currently, POST /api/feed triggers an immediate crawl which blocks the response (or at least keeps the goroutine alive). Refactor the crawling architecture to be truly async with a job queue or status updates, improving API responsiveness and reliability.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.908243985Z","updated":"2026-02-13T04:26:55.908243985Z"}
-{"id":"NK-gqkh96","title":"Remaining test coverage gaps","description":"Cross-package test coverage is at 81.2%. The remaining untested functions are: GetFullContent (goose HTTP extraction), indexHandler/serveBoxedFile (rice.MustFindBox), Serve (starts HTTP server), main, util.init. To reach 90%, consider: (1) refactoring GetFullContent to accept an interface for HTTP fetching, (2) refactoring Serve to extract route setup into a testable function, (3) mocking rice.MustFindBox, (4) using feeds from https://trenchant.org/feeds.txt as static test fixtures for integration tests.","type":"task","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:30.298141982Z","updated":"2026-02-13T03:54:30.298141982Z"}
+{"id":"NK-ed1iah","title":"Make feed crawling async in API","description":"Currently, POST /api/feed triggers an immediate crawl which blocks the response (or at least keeps the goroutine alive). Refactor the crawling architecture to be truly async with a job queue or status updates, improving API responsiveness and reliability.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.908243985Z","updated":"2026-02-13T04:26:55.908243985Z"}
+{"id":"NK-gqkh96","title":"Remaining test coverage gaps","description":"Cross-package test coverage is at 81.2%. The remaining untested functions are: GetFullContent (goose HTTP extraction), indexHandler/serveBoxedFile (rice.MustFindBox), Serve (starts HTTP server), main, util.init. To reach 90%, consider: (1) refactoring GetFullContent to accept an interface for HTTP fetching, (2) refactoring Serve to extract route setup into a testable function, (3) mocking rice.MustFindBox, (4) using feeds from https://trenchant.org/feeds.txt as static test fixtures for integration tests.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T03:54:30.298141982Z","updated":"2026-02-13T03:54:30.298141982Z"}
{"id":"NK-op5594","title":"Ensure 80% Frontend Test Coverage","description":"Configure coverage reporting in vitest and ensure the frontend codebase maintains at least 80% test coverage.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T05:46:24.13314466Z","updated":"2026-02-13T05:50:46.728239299Z"}
-{"id":"NK-ric1zs","title":"Migrate frontend to /api/ endpoints","description":"The backend now provides a clean REST API at /api/. Update the frontend UI to use these new endpoints instead of the legacy backward-compatibility routes (/stream/, /feed/, etc.). This will allow for cleaner separation and better utilization of proper REST patterns.","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.864725765Z","updated":"2026-02-13T04:26:55.864725765Z"}
+{"id":"NK-ric1zs","title":"Migrate frontend to /api/ endpoints","description":"The backend now provides a clean REST API at /api/. Update the frontend UI to use these new endpoints instead of the legacy backward-compatibility routes (/stream/, /feed/, etc.). This will allow for cleaner separation and better utilization of proper REST patterns.","type":"cleanup","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.864725765Z","updated":"2026-02-13T04:26:55.864725765Z"}
{"id":"NK-t0nmbj","title":"new web frontend","description":"The current frontend uses an old version of backbone and jquery. Let's \"deprecate\" it -- keep it arouond so we can test against it and use it, but let's be able to also serve and use a nice shiny new frontend written in either simiple, highly efficient vanilla javascript, or put together something in react or similar. Needs to feel fast and low latency!\n\nIt's very important that this new frontend has all the functionality of the existing one AND looks similar (use same style, etc, but adjust a little if needed.)\n\nALSO make it highly testable and have high test coverage as you go. I don't want it to use the Chrome browser plugin thing, just test it on your own using things from the command line you can do.","type":"epic","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T02:01:37.2107893Z","updated":"2026-02-13T05:43:47.613995925Z"}
{"id":"NK-x924bu","title":"test coverage","description":"assume the code works properly (it mostly does)\nget to 90% test coverage on the go code","type":"task","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-13T01:52:01.042476226Z","updated":"2026-02-13T03:54:21.526519915Z"}
-{"id":"NK-zt4e32","title":"Implement Frontend Feed List","description":"Create feed list view in new frontend. Fetch feeds from API.","type":"","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.58866298Z","updated":"2026-02-13T05:44:01.58866298Z"}
+{"id":"NK-zt4e32","title":"Implement Frontend Feed List","description":"Create feed list view in new frontend. Fetch feeds from API.","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:44:01.58866298Z","updated":"2026-02-13T05:59:46.132148641Z"}
{"id":"NK-d0ghccy","from_ticket_id":"NK-ric1zs","to_ticket_id":"NK-1phdpf","type":"created_from","created":"2026-02-13T04:26:55.875394997Z"}
{"id":"NK-d1uyy71","from_ticket_id":"NK-27or4b","to_ticket_id":"NK-bsdwqz","type":"created_from","created":"2026-02-13T05:03:09.689282214Z"}
{"id":"NK-d50pbhs","from_ticket_id":"NK-zt4e32","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.598803513Z"}
@@ -21,5 +22,6 @@
{"id":"NK-dgfppki","from_ticket_id":"NK-gqkh96","to_ticket_id":"NK-x924bu","type":"created_from","created":"2026-02-13T03:54:30.303602703Z"}
{"id":"NK-dl8clj9","from_ticket_id":"NK-0nf7hu","to_ticket_id":"NK-9hx0y7","type":"created_from","created":"2026-02-13T05:50:46.769436228Z"}
{"id":"NK-dlvmiyc","from_ticket_id":"NK-7tzbql","to_ticket_id":"NK-bsdwqz","type":"created_from","created":"2026-02-13T05:02:57.392616851Z"}
+{"id":"NK-dm75oc8","from_ticket_id":"NK-3om7x2","to_ticket_id":"NK-zt4e32","type":"created_from","created":"2026-02-13T05:59:46.169842933Z"}
{"id":"NK-dofihuz","from_ticket_id":"NK-0ppv3f","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.640770816Z"}
{"id":"NK-dwav3hh","from_ticket_id":"NK-6q9nyg","to_ticket_id":"NK-x924bu","type":"created_from","created":"2026-02-13T03:54:37.639569082Z"}
diff --git a/frontend/coverage/clover.xml b/frontend/coverage/clover.xml
index c45c1a8..5e830d2 100644
--- a/frontend/coverage/clover.xml
+++ b/frontend/coverage/clover.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
-<coverage generated="1770961817903" clover="3.2.0">
- <project timestamp="1770961817903" name="All files">
- <metrics statements="32" coveredstatements="29" conditionals="12" coveredconditionals="9" methods="9" coveredmethods="8" elements="53" coveredelements="46" complexity="0" loc="32" ncloc="32" packages="2" files="4" classes="4"/>
+<coverage generated="1770962373767" clover="3.2.0">
+ <project timestamp="1770962373767" name="All files">
+ <metrics statements="48" coveredstatements="44" conditionals="24" coveredconditionals="19" methods="15" coveredmethods="14" elements="87" coveredelements="77" complexity="0" loc="48" ncloc="48" packages="2" files="6" classes="6"/>
<package name="src">
<metrics statements="15" coveredstatements="12" conditionals="6" coveredconditionals="4" methods="6" coveredmethods="5"/>
<file name="App.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/App.css">
@@ -22,12 +22,34 @@
<line num="27" count="1" type="cond" truecount="1" falsecount="1"/>
<line num="28" count="0" type="stmt"/>
<line num="31" count="1" type="stmt"/>
- <line num="36" count="1" type="stmt"/>
- <line num="45" count="2" type="stmt"/>
+ <line num="37" count="1" type="stmt"/>
+ <line num="58" count="2" type="stmt"/>
</file>
</package>
<package name="src.components">
- <metrics statements="17" coveredstatements="17" conditionals="6" coveredconditionals="5" methods="3" coveredmethods="3"/>
+ <metrics statements="33" coveredstatements="32" conditionals="18" coveredconditionals="15" methods="9" coveredmethods="9"/>
+ <file name="FeedList.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css">
+ <metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
+ </file>
+ <file name="FeedList.tsx" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx">
+ <metrics statements="16" coveredstatements="15" conditionals="12" coveredconditionals="10" methods="6" coveredmethods="6"/>
+ <line num="6" count="9" type="stmt"/>
+ <line num="7" count="9" type="stmt"/>
+ <line num="8" count="9" type="stmt"/>
+ <line num="10" count="9" type="stmt"/>
+ <line num="11" count="5" type="stmt"/>
+ <line num="13" count="3" type="cond" truecount="1" falsecount="1"/>
+ <line num="14" count="0" type="stmt"/>
+ <line num="16" count="3" type="stmt"/>
+ <line num="19" count="3" type="stmt"/>
+ <line num="20" count="3" type="stmt"/>
+ <line num="23" count="1" type="stmt"/>
+ <line num="24" count="1" type="stmt"/>
+ <line num="28" count="9" type="cond" truecount="2" falsecount="0"/>
+ <line num="29" count="4" type="cond" truecount="2" falsecount="0"/>
+ <line num="31" count="3" type="stmt"/>
+ <line num="39" count="2" type="stmt"/>
+ </file>
<file name="Login.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css">
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
</file>
diff --git a/frontend/coverage/coverage-final.json b/frontend/coverage/coverage-final.json
index ebc723c..1fe79ba 100644
--- a/frontend/coverage/coverage-final.json
+++ b/frontend/coverage/coverage-final.json
@@ -1,5 +1,7 @@
{"/Users/adam/workspace/vibecode/neko/frontend/src/App.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}}
-,"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx","statementMap":{"0":{"start":{"line":8,"column":22},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":8},"end":{"line":9,"column":null}},"2":{"start":{"line":11,"column":2},"end":{"line":21,"column":null}},"3":{"start":{"line":12,"column":4},"end":{"line":20,"column":null}},"4":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"5":{"start":{"line":15,"column":10},"end":{"line":15,"column":null}},"6":{"start":{"line":17,"column":10},"end":{"line":17,"column":null}},"7":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"8":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"11":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"12":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"13":{"start":{"line":36,"column":2},"end":{"line":40,"column":null}},"14":{"start":{"line":45,"column":2},"end":{"line":58,"column":null}}},"fnMap":{"0":{"name":"RequireAuth","decl":{"start":{"line":7,"column":9},"end":{"line":7,"column":21}},"loc":{"start":{"line":7,"column":69},"end":{"line":32,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":12},"end":{"line":11,"column":18}},"loc":{"start":{"line":11,"column":18},"end":{"line":21,"column":5}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":12},"end":{"line":13,"column":13}},"loc":{"start":{"line":13,"column":21},"end":{"line":19,"column":7}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":13},"end":{"line":20,"column":19}},"loc":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"line":20},"4":{"name":"Dashboard","decl":{"start":{"line":34,"column":9},"end":{"line":34,"column":21}},"loc":{"start":{"line":34,"column":21},"end":{"line":42,"column":null}},"line":34},"5":{"name":"App","decl":{"start":{"line":44,"column":9},"end":{"line":44,"column":15}},"loc":{"start":{"line":44,"column":15},"end":{"line":60,"column":null}},"line":44}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},{"start":{"line":16,"column":15},"end":{"line":18,"column":null}}],"line":14},"1":{"loc":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":23},"2":{"loc":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":27}},"s":{"0":2,"1":2,"2":2,"3":1,"4":1,"5":1,"6":0,"7":0,"8":2,"9":1,"10":1,"11":0,"12":1,"13":1,"14":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":6,"lastStatement":15,"seen":{"f:7:9:7:21":0,"s:8:22:8:Infinity":0,"s:9:8:9:Infinity":1,"s:11:2:21:Infinity":2,"f:11:12:11:18":1,"s:12:4:20:Infinity":3,"f:13:12:13:13":2,"b:14:8:18:Infinity:16:15:18:Infinity":0,"s:14:8:18:Infinity":4,"s:15:10:15:Infinity":5,"s:17:10:17:Infinity":6,"f:20:13:20:19":3,"s:20:19:20:33":7,"b:23:2:25:Infinity:undefined:undefined:undefined:undefined":1,"s:23:2:25:Infinity":8,"s:24:4:24:Infinity":9,"b:27:2:29:Infinity:undefined:undefined:undefined:undefined":2,"s:27:2:29:Infinity":10,"s:28:4:28:Infinity":11,"s:31:2:31:Infinity":12,"f:34:9:34:21":4,"s:36:2:40:Infinity":13,"f:44:9:44:15":5,"s:45:2:58:Infinity":14}}}
+,"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx","statementMap":{"0":{"start":{"line":8,"column":22},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":8},"end":{"line":9,"column":null}},"2":{"start":{"line":11,"column":2},"end":{"line":21,"column":null}},"3":{"start":{"line":12,"column":4},"end":{"line":20,"column":null}},"4":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"5":{"start":{"line":15,"column":10},"end":{"line":15,"column":null}},"6":{"start":{"line":17,"column":10},"end":{"line":17,"column":null}},"7":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"8":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"9":{"start":{"line":24,"column":4},"end":{"line":24,"column":null}},"10":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"11":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"12":{"start":{"line":31,"column":2},"end":{"line":31,"column":null}},"13":{"start":{"line":37,"column":2},"end":{"line":53,"column":null}},"14":{"start":{"line":58,"column":2},"end":{"line":71,"column":null}}},"fnMap":{"0":{"name":"RequireAuth","decl":{"start":{"line":7,"column":9},"end":{"line":7,"column":21}},"loc":{"start":{"line":7,"column":69},"end":{"line":32,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":12},"end":{"line":11,"column":18}},"loc":{"start":{"line":11,"column":18},"end":{"line":21,"column":5}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":12},"end":{"line":13,"column":13}},"loc":{"start":{"line":13,"column":21},"end":{"line":19,"column":7}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":20,"column":13},"end":{"line":20,"column":19}},"loc":{"start":{"line":20,"column":19},"end":{"line":20,"column":33}},"line":20},"4":{"name":"Dashboard","decl":{"start":{"line":36,"column":9},"end":{"line":36,"column":21}},"loc":{"start":{"line":36,"column":21},"end":{"line":55,"column":null}},"line":36},"5":{"name":"App","decl":{"start":{"line":57,"column":9},"end":{"line":57,"column":15}},"loc":{"start":{"line":57,"column":15},"end":{"line":73,"column":null}},"line":57}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":8},"end":{"line":18,"column":null}},{"start":{"line":16,"column":15},"end":{"line":18,"column":null}}],"line":14},"1":{"loc":{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":2},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":23},"2":{"loc":{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":27,"column":2},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":27}},"s":{"0":2,"1":2,"2":2,"3":1,"4":1,"5":1,"6":0,"7":0,"8":2,"9":1,"10":1,"11":0,"12":1,"13":1,"14":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":6,"lastStatement":15,"seen":{"f:7:9:7:21":0,"s:8:22:8:Infinity":0,"s:9:8:9:Infinity":1,"s:11:2:21:Infinity":2,"f:11:12:11:18":1,"s:12:4:20:Infinity":3,"f:13:12:13:13":2,"b:14:8:18:Infinity:16:15:18:Infinity":0,"s:14:8:18:Infinity":4,"s:15:10:15:Infinity":5,"s:17:10:17:Infinity":6,"f:20:13:20:19":3,"s:20:19:20:33":7,"b:23:2:25:Infinity:undefined:undefined:undefined:undefined":1,"s:23:2:25:Infinity":8,"s:24:4:24:Infinity":9,"b:27:2:29:Infinity:undefined:undefined:undefined:undefined":2,"s:27:2:29:Infinity":10,"s:28:4:28:Infinity":11,"s:31:2:31:Infinity":12,"f:36:9:36:21":4,"s:37:2:53:Infinity":13,"f:57:9:57:15":5,"s:58:2:71:Infinity":14}}}
+,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}}
+,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedList.tsx","statementMap":{"0":{"start":{"line":6,"column":26},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":30},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":26},"end":{"line":8,"column":null}},"3":{"start":{"line":10,"column":4},"end":{"line":26,"column":null}},"4":{"start":{"line":11,"column":8},"end":{"line":25,"column":null}},"5":{"start":{"line":13,"column":16},"end":{"line":15,"column":null}},"6":{"start":{"line":14,"column":20},"end":{"line":14,"column":null}},"7":{"start":{"line":16,"column":16},"end":{"line":16,"column":null}},"8":{"start":{"line":19,"column":16},"end":{"line":19,"column":null}},"9":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"10":{"start":{"line":23,"column":16},"end":{"line":23,"column":null}},"11":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"12":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"13":{"start":{"line":28,"column":17},"end":{"line":28,"column":null}},"14":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"15":{"start":{"line":29,"column":15},"end":{"line":29,"column":null}},"16":{"start":{"line":31,"column":4},"end":{"line":48,"column":null}},"17":{"start":{"line":39,"column":24},"end":{"line":44,"column":null}}},"fnMap":{"0":{"name":"FeedList","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":35}},"loc":{"start":{"line":5,"column":35},"end":{"line":50,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":14},"end":{"line":10,"column":20}},"loc":{"start":{"line":10,"column":20},"end":{"line":26,"column":7}},"line":10},"2":{"name":"(anonymous_2)","decl":{"start":{"line":12,"column":18},"end":{"line":12,"column":19}},"loc":{"start":{"line":12,"column":27},"end":{"line":17,"column":13}},"line":12},"3":{"name":"(anonymous_3)","decl":{"start":{"line":18,"column":18},"end":{"line":18,"column":19}},"loc":{"start":{"line":18,"column":28},"end":{"line":21,"column":13}},"line":18},"4":{"name":"(anonymous_4)","decl":{"start":{"line":22,"column":19},"end":{"line":22,"column":20}},"loc":{"start":{"line":22,"column":28},"end":{"line":25,"column":13}},"line":22},"5":{"name":"(anonymous_5)","decl":{"start":{"line":38,"column":31},"end":{"line":38,"column":32}},"loc":{"start":{"line":39,"column":24},"end":{"line":44,"column":null}},"line":39}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":16},"end":{"line":15,"column":null}},"type":"if","locations":[{"start":{"line":13,"column":16},"end":{"line":15,"column":null}},{"start":{},"end":{}}],"line":13},"1":{"loc":{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},"type":"if","locations":[{"start":{"line":28,"column":4},"end":{"line":28,"column":null}},{"start":{},"end":{}}],"line":28},"2":{"loc":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},{"start":{},"end":{}}],"line":29},"3":{"loc":{"start":{"line":34,"column":13},"end":{"line":46,"column":null}},"type":"cond-expr","locations":[{"start":{"line":35,"column":16},"end":{"line":35,"column":null}},{"start":{"line":37,"column":16},"end":{"line":46,"column":null}}],"line":34},"4":{"loc":{"start":{"line":41,"column":33},"end":{"line":41,"column":null}},"type":"binary-expr","locations":[{"start":{"line":41,"column":33},"end":{"line":41,"column":47}},{"start":{"line":41,"column":47},"end":{"line":41,"column":null}}],"line":41},"5":{"loc":{"start":{"line":43,"column":29},"end":{"line":43,"column":null}},"type":"binary-expr","locations":[{"start":{"line":43,"column":29},"end":{"line":43,"column":46}},{"start":{"line":43,"column":46},"end":{"line":43,"column":null}}],"line":43}},"s":{"0":9,"1":9,"2":9,"3":9,"4":5,"5":3,"6":0,"7":3,"8":3,"9":3,"10":1,"11":1,"12":9,"13":5,"14":4,"15":4,"16":3,"17":2},"f":{"0":9,"1":5,"2":3,"3":3,"4":1,"5":2},"b":{"0":[0,3],"1":[5,4],"2":[1,3],"3":[2,1],"4":[2,0],"5":[2,2]},"meta":{"lastBranch":6,"lastFunction":6,"lastStatement":18,"seen":{"f:5:24:5:35":0,"s:6:26:6:Infinity":0,"s:7:30:7:Infinity":1,"s:8:26:8:Infinity":2,"s:10:4:26:Infinity":3,"f:10:14:10:20":1,"s:11:8:25:Infinity":4,"f:12:18:12:19":2,"b:13:16:15:Infinity:undefined:undefined:undefined:undefined":0,"s:13:16:15:Infinity":5,"s:14:20:14:Infinity":6,"s:16:16:16:Infinity":7,"f:18:18:18:19":3,"s:19:16:19:Infinity":8,"s:20:16:20:Infinity":9,"f:22:19:22:20":4,"s:23:16:23:Infinity":10,"s:24:16:24:Infinity":11,"b:28:4:28:Infinity:undefined:undefined:undefined:undefined":1,"s:28:4:28:Infinity":12,"s:28:17:28:Infinity":13,"b:29:4:29:Infinity:undefined:undefined:undefined:undefined":2,"s:29:4:29:Infinity":14,"s:29:15:29:Infinity":15,"s:31:4:48:Infinity":16,"b:35:16:35:Infinity:37:16:46:Infinity":3,"f:38:31:38:32":5,"s:39:24:44:Infinity":17,"b:41:33:41:47:41:47:41:Infinity":4,"b:43:29:43:46:43:46:43:Infinity":5}}}
,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}}
,"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.tsx","statementMap":{"0":{"start":{"line":6,"column":32},"end":{"line":6,"column":null}},"1":{"start":{"line":7,"column":26},"end":{"line":7,"column":null}},"2":{"start":{"line":8,"column":10},"end":{"line":8,"column":null}},"3":{"start":{"line":10,"column":25},"end":{"line":33,"column":null}},"4":{"start":{"line":11,"column":8},"end":{"line":11,"column":null}},"5":{"start":{"line":12,"column":8},"end":{"line":12,"column":null}},"6":{"start":{"line":14,"column":8},"end":{"line":32,"column":null}},"7":{"start":{"line":16,"column":27},"end":{"line":16,"column":null}},"8":{"start":{"line":17,"column":12},"end":{"line":17,"column":null}},"9":{"start":{"line":19,"column":24},"end":{"line":22,"column":null}},"10":{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":27,"column":29},"end":{"line":27,"column":null}},"13":{"start":{"line":28,"column":16},"end":{"line":28,"column":null}},"14":{"start":{"line":31,"column":12},"end":{"line":31,"column":null}},"15":{"start":{"line":35,"column":4},"end":{"line":52,"column":null}},"16":{"start":{"line":45,"column":41},"end":{"line":45,"column":null}}},"fnMap":{"0":{"name":"Login","decl":{"start":{"line":5,"column":24},"end":{"line":5,"column":32}},"loc":{"start":{"line":5,"column":32},"end":{"line":54,"column":null}},"line":5},"1":{"name":"(anonymous_1)","decl":{"start":{"line":10,"column":25},"end":{"line":10,"column":32}},"loc":{"start":{"line":10,"column":49},"end":{"line":33,"column":null}},"line":10},"2":{"name":"(anonymous_2)","decl":{"start":{"line":45,"column":34},"end":{"line":45,"column":35}},"loc":{"start":{"line":45,"column":41},"end":{"line":45,"column":null}},"line":45}},"branchMap":{"0":{"loc":{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},"type":"if","locations":[{"start":{"line":24,"column":12},"end":{"line":29,"column":null}},{"start":{"line":26,"column":19},"end":{"line":29,"column":null}}],"line":24},"1":{"loc":{"start":{"line":28,"column":25},"end":{"line":28,"column":55}},"type":"binary-expr","locations":[{"start":{"line":28,"column":25},"end":{"line":28,"column":41}},{"start":{"line":28,"column":41},"end":{"line":28,"column":55}}],"line":28},"2":{"loc":{"start":{"line":49,"column":17},"end":{"line":49,"column":null}},"type":"binary-expr","locations":[{"start":{"line":49,"column":17},"end":{"line":49,"column":26}},{"start":{"line":49,"column":26},"end":{"line":49,"column":null}}],"line":49}},"s":{"0":14,"1":14,"2":14,"3":14,"4":3,"5":3,"6":3,"7":3,"8":3,"9":3,"10":2,"11":1,"12":1,"13":1,"14":1,"15":14,"16":3},"f":{"0":14,"1":3,"2":3},"b":{"0":[1,1],"1":[1,0],"2":[14,2]},"meta":{"lastBranch":3,"lastFunction":3,"lastStatement":17,"seen":{"f:5:24:5:32":0,"s:6:32:6:Infinity":0,"s:7:26:7:Infinity":1,"s:8:10:8:Infinity":2,"s:10:25:33:Infinity":3,"f:10:25:10:32":1,"s:11:8:11:Infinity":4,"s:12:8:12:Infinity":5,"s:14:8:32:Infinity":6,"s:16:27:16:Infinity":7,"s:17:12:17:Infinity":8,"s:19:24:22:Infinity":9,"b:24:12:29:Infinity:26:19:29:Infinity":0,"s:24:12:29:Infinity":10,"s:25:16:25:Infinity":11,"s:27:29:27:Infinity":12,"s:28:16:28:Infinity":13,"b:28:25:28:41:28:41:28:55":1,"s:31:12:31:Infinity":14,"s:35:4:52:Infinity":15,"f:45:34:45:35":2,"s:45:41:45:Infinity":16,"b:49:17:49:26:49:26:49:Infinity":2}}}
}
diff --git a/frontend/coverage/index.html b/frontend/coverage/index.html
index 8fcf963..e90ae39 100644
--- a/frontend/coverage/index.html
+++ b/frontend/coverage/index.html
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
- <span class="strong">90.62% </span>
+ <span class="strong">92% </span>
<span class="quiet">Statements</span>
- <span class='fraction'>29/32</span>
+ <span class='fraction'>46/50</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">75% </span>
+ <span class="strong">79.16% </span>
<span class="quiet">Branches</span>
- <span class='fraction'>9/12</span>
+ <span class='fraction'>19/24</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">88.88% </span>
+ <span class="strong">93.33% </span>
<span class="quiet">Functions</span>
- <span class='fraction'>8/9</span>
+ <span class='fraction'>14/15</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">90.62% </span>
+ <span class="strong">91.66% </span>
<span class="quiet">Lines</span>
- <span class='fraction'>29/32</span>
+ <span class='fraction'>44/48</span>
</div>
@@ -95,17 +95,17 @@
<tr>
<td class="file high" data-value="src/components"><a href="src/components/index.html">src/components</a></td>
- <td data-value="100" class="pic high">
- <div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
+ <td data-value="97.14" class="pic high">
+ <div class="chart"><div class="cover-fill" style="width: 97%"></div><div class="cover-empty" style="width: 3%"></div></div>
</td>
- <td data-value="100" class="pct high">100%</td>
- <td data-value="17" class="abs high">17/17</td>
+ <td data-value="97.14" class="pct high">97.14%</td>
+ <td data-value="35" class="abs high">34/35</td>
<td data-value="83.33" class="pct high">83.33%</td>
- <td data-value="6" class="abs high">5/6</td>
- <td data-value="100" class="pct high">100%</td>
- <td data-value="3" class="abs high">3/3</td>
+ <td data-value="18" class="abs high">15/18</td>
<td data-value="100" class="pct high">100%</td>
- <td data-value="17" class="abs high">17/17</td>
+ <td data-value="9" class="abs high">9/9</td>
+ <td data-value="96.96" class="pct high">96.96%</td>
+ <td data-value="33" class="abs high">32/33</td>
</tr>
</tbody>
@@ -116,7 +116,7 @@
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/App.css.html b/frontend/coverage/src/App.css.html
index 4df3a49..4ba93ef 100644
--- a/frontend/coverage/src/App.css.html
+++ b/frontend/coverage/src/App.css.html
@@ -105,7 +105,24 @@
<a name='L40'></a><a href='#L40'>40</a>
<a name='L41'></a><a href='#L41'>41</a>
<a name='L42'></a><a href='#L42'>42</a>
-<a name='L43'></a><a href='#L43'>43</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<a name='L43'></a><a href='#L43'>43</a>
+<a name='L44'></a><a href='#L44'>44</a>
+<a name='L45'></a><a href='#L45'>45</a>
+<a name='L46'></a><a href='#L46'>46</a>
+<a name='L47'></a><a href='#L47'>47</a>
+<a name='L48'></a><a href='#L48'>48</a>
+<a name='L49'></a><a href='#L49'>49</a>
+<a name='L50'></a><a href='#L50'>50</a>
+<a name='L51'></a><a href='#L51'>51</a>
+<a name='L52'></a><a href='#L52'>52</a>
+<a name='L53'></a><a href='#L53'>53</a>
+<a name='L54'></a><a href='#L54'>54</a>
+<a name='L55'></a><a href='#L55'>55</a>
+<a name='L56'></a><a href='#L56'>56</a>
+<a name='L57'></a><a href='#L57'>57</a>
+<a name='L58'></a><a href='#L58'>58</a>
+<a name='L59'></a><a href='#L59'>59</a>
+<a name='L60'></a><a href='#L60'>60</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -147,56 +164,90 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
-<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">/* Resets and Base Styles */
+* {
+ box-sizing: border-box;
}
&nbsp;
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
+&nbsp;
+/* Dashboard Layout */
+.dashboard {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
+ /* Prevent body scroll */
}
&nbsp;
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+.dashboard-header {
+ background: #2c3e50;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-shrink: 0;
}
&nbsp;
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
+.dashboard-header h1 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
}
&nbsp;
-.card {
- padding: 2em;
+.dashboard-content {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
}
&nbsp;
-.read-the-docs {
- color: #888;
+.dashboard-sidebar {
+ width: 300px;
+ background: #f8f9fa;
+ border-right: 1px solid #ddd;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
}
-&nbsp;</pre></td></tr></table></pre>
+&nbsp;
+.dashboard-main {
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+ background: #fff;
+}</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/App.tsx.html b/frontend/coverage/src/App.tsx.html
index 7a8d1c9..9d8fd6f 100644
--- a/frontend/coverage/src/App.tsx.html
+++ b/frontend/coverage/src/App.tsx.html
@@ -125,7 +125,20 @@
<a name='L60'></a><a href='#L60'>60</a>
<a name='L61'></a><a href='#L61'>61</a>
<a name='L62'></a><a href='#L62'>62</a>
-<a name='L63'></a><a href='#L63'>63</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<a name='L63'></a><a href='#L63'>63</a>
+<a name='L64'></a><a href='#L64'>64</a>
+<a name='L65'></a><a href='#L65'>65</a>
+<a name='L66'></a><a href='#L66'>66</a>
+<a name='L67'></a><a href='#L67'>67</a>
+<a name='L68'></a><a href='#L68'>68</a>
+<a name='L69'></a><a href='#L69'>69</a>
+<a name='L70'></a><a href='#L70'>70</a>
+<a name='L71'></a><a href='#L71'>71</a>
+<a name='L72'></a><a href='#L72'>72</a>
+<a name='L73'></a><a href='#L73'>73</a>
+<a name='L74'></a><a href='#L74'>74</a>
+<a name='L75'></a><a href='#L75'>75</a>
+<a name='L76'></a><a href='#L76'>76</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -160,6 +173,7 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">1x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -169,6 +183,18 @@
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-yes">2x</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -220,14 +246,27 @@ function RequireAuth({ children }: { children: React.ReactElement }) {
return children;
}
&nbsp;
+import FeedList from './components/FeedList';
+&nbsp;
function Dashboard() {
- // Placeholder for now
return (
- &lt;div&gt;
- &lt;h1&gt;Dashboard&lt;/h1&gt;
- &lt;p&gt;Welcome to the new Neko/v2 frontend.&lt;/p&gt;
+ &lt;div className="dashboard"&gt;
+ &lt;header className="dashboard-header"&gt;
+ &lt;h1&gt;Neko Reader&lt;/h1&gt;
+ &lt;nav&gt;
+ {/* Add logout later */}
+ &lt;/nav&gt;
+ &lt;/header&gt;
+ &lt;div className="dashboard-content"&gt;
+ &lt;aside className="dashboard-sidebar"&gt;
+ &lt;FeedList /&gt;
+ &lt;/aside&gt;
+ &lt;main className="dashboard-main"&gt;
+ &lt;p&gt;Select a feed to view items.&lt;/p&gt;
+ &lt;/main&gt;
+ &lt;/div&gt;
&lt;/div&gt;
- )
+ );
}
&nbsp;
function App() {
@@ -256,7 +295,7 @@ export default App;
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/FeedList.css.html b/frontend/coverage/src/components/FeedList.css.html
new file mode 100644
index 0000000..cd14b1b
--- /dev/null
+++ b/frontend/coverage/src/components/FeedList.css.html
@@ -0,0 +1,226 @@
+
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Code coverage report for src/components/FeedList.css</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" href="../../prettify.css" />
+ <link rel="stylesheet" href="../../base.css" />
+ <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <style type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../../sort-arrow-sprite.png);
+ }
+ </style>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedList.css</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">0% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>0/0</span>
+ </div>
+
+
+ </div>
+ <p class="quiet">
+ Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
+ </p>
+ <template id="filterTemplate">
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
+ </template>
+ </div>
+ <div class='status-line low'></div>
+ <pre><table class="coverage">
+<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
+<a name='L2'></a><a href='#L2'>2</a>
+<a name='L3'></a><a href='#L3'>3</a>
+<a name='L4'></a><a href='#L4'>4</a>
+<a name='L5'></a><a href='#L5'>5</a>
+<a name='L6'></a><a href='#L6'>6</a>
+<a name='L7'></a><a href='#L7'>7</a>
+<a name='L8'></a><a href='#L8'>8</a>
+<a name='L9'></a><a href='#L9'>9</a>
+<a name='L10'></a><a href='#L10'>10</a>
+<a name='L11'></a><a href='#L11'>11</a>
+<a name='L12'></a><a href='#L12'>12</a>
+<a name='L13'></a><a href='#L13'>13</a>
+<a name='L14'></a><a href='#L14'>14</a>
+<a name='L15'></a><a href='#L15'>15</a>
+<a name='L16'></a><a href='#L16'>16</a>
+<a name='L17'></a><a href='#L17'>17</a>
+<a name='L18'></a><a href='#L18'>18</a>
+<a name='L19'></a><a href='#L19'>19</a>
+<a name='L20'></a><a href='#L20'>20</a>
+<a name='L21'></a><a href='#L21'>21</a>
+<a name='L22'></a><a href='#L22'>22</a>
+<a name='L23'></a><a href='#L23'>23</a>
+<a name='L24'></a><a href='#L24'>24</a>
+<a name='L25'></a><a href='#L25'>25</a>
+<a name='L26'></a><a href='#L26'>26</a>
+<a name='L27'></a><a href='#L27'>27</a>
+<a name='L28'></a><a href='#L28'>28</a>
+<a name='L29'></a><a href='#L29'>29</a>
+<a name='L30'></a><a href='#L30'>30</a>
+<a name='L31'></a><a href='#L31'>31</a>
+<a name='L32'></a><a href='#L32'>32</a>
+<a name='L33'></a><a href='#L33'>33</a>
+<a name='L34'></a><a href='#L34'>34</a>
+<a name='L35'></a><a href='#L35'>35</a>
+<a name='L36'></a><a href='#L36'>36</a>
+<a name='L37'></a><a href='#L37'>37</a>
+<a name='L38'></a><a href='#L38'>38</a>
+<a name='L39'></a><a href='#L39'>39</a>
+<a name='L40'></a><a href='#L40'>40</a>
+<a name='L41'></a><a href='#L41'>41</a>
+<a name='L42'></a><a href='#L42'>42</a>
+<a name='L43'></a><a href='#L43'>43</a>
+<a name='L44'></a><a href='#L44'>44</a>
+<a name='L45'></a><a href='#L45'>45</a>
+<a name='L46'></a><a href='#L46'>46</a>
+<a name='L47'></a><a href='#L47'>47</a>
+<a name='L48'></a><a href='#L48'>48</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">.feed-list {
+ padding: 1rem;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+&nbsp;
+.feed-list h2 {
+ margin-top: 0;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 0.5rem;
+}
+&nbsp;
+.feed-list-items {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+&nbsp;
+.feed-item {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #f0f0f0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+&nbsp;
+.feed-item:last-child {
+ border-bottom: none;
+}
+&nbsp;
+.feed-title {
+ text-decoration: none;
+ color: #333;
+ font-weight: 500;
+}
+&nbsp;
+.feed-title:hover {
+ color: #007bff;
+}
+&nbsp;
+.feed-category {
+ background: #e9ecef;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ color: #666;
+}</pre></td></tr></table></pre>
+
+ <div class='push'></div><!-- for sticky footer -->
+ </div><!-- /wrapper -->
+ <div class='footer quiet pad2 space-top1 center small'>
+ Code coverage generated by
+ <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
+ at 2026-02-13T05:59:33.745Z
+ </div>
+ <script src="../../prettify.js"></script>
+ <script>
+ window.onload = function () {
+ prettyPrint();
+ };
+ </script>
+ <script src="../../sorter.js"></script>
+ <script src="../../block-navigation.js"></script>
+ </body>
+</html>
+ \ No newline at end of file
diff --git a/frontend/coverage/src/components/FeedList.tsx.html b/frontend/coverage/src/components/FeedList.tsx.html
new file mode 100644
index 0000000..d7344a2
--- /dev/null
+++ b/frontend/coverage/src/components/FeedList.tsx.html
@@ -0,0 +1,235 @@
+
+<!doctype html>
+<html lang="en">
+
+<head>
+ <title>Code coverage report for src/components/FeedList.tsx</title>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" href="../../prettify.css" />
+ <link rel="stylesheet" href="../../base.css" />
+ <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <style type='text/css'>
+ .coverage-summary .sorter {
+ background-image: url(../../sort-arrow-sprite.png);
+ }
+ </style>
+</head>
+
+<body>
+<div class='wrapper'>
+ <div class='pad1'>
+ <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> FeedList.tsx</h1>
+ <div class='clearfix'>
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">94.44% </span>
+ <span class="quiet">Statements</span>
+ <span class='fraction'>17/18</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">83.33% </span>
+ <span class="quiet">Branches</span>
+ <span class='fraction'>10/12</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">100% </span>
+ <span class="quiet">Functions</span>
+ <span class='fraction'>6/6</span>
+ </div>
+
+
+ <div class='fl pad1y space-right2'>
+ <span class="strong">93.75% </span>
+ <span class="quiet">Lines</span>
+ <span class='fraction'>15/16</span>
+ </div>
+
+
+ </div>
+ <p class="quiet">
+ Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
+ </p>
+ <template id="filterTemplate">
+ <div class="quiet">
+ Filter:
+ <input type="search" id="fileSearch">
+ </div>
+ </template>
+ </div>
+ <div class='status-line high'></div>
+ <pre><table class="coverage">
+<tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
+<a name='L2'></a><a href='#L2'>2</a>
+<a name='L3'></a><a href='#L3'>3</a>
+<a name='L4'></a><a href='#L4'>4</a>
+<a name='L5'></a><a href='#L5'>5</a>
+<a name='L6'></a><a href='#L6'>6</a>
+<a name='L7'></a><a href='#L7'>7</a>
+<a name='L8'></a><a href='#L8'>8</a>
+<a name='L9'></a><a href='#L9'>9</a>
+<a name='L10'></a><a href='#L10'>10</a>
+<a name='L11'></a><a href='#L11'>11</a>
+<a name='L12'></a><a href='#L12'>12</a>
+<a name='L13'></a><a href='#L13'>13</a>
+<a name='L14'></a><a href='#L14'>14</a>
+<a name='L15'></a><a href='#L15'>15</a>
+<a name='L16'></a><a href='#L16'>16</a>
+<a name='L17'></a><a href='#L17'>17</a>
+<a name='L18'></a><a href='#L18'>18</a>
+<a name='L19'></a><a href='#L19'>19</a>
+<a name='L20'></a><a href='#L20'>20</a>
+<a name='L21'></a><a href='#L21'>21</a>
+<a name='L22'></a><a href='#L22'>22</a>
+<a name='L23'></a><a href='#L23'>23</a>
+<a name='L24'></a><a href='#L24'>24</a>
+<a name='L25'></a><a href='#L25'>25</a>
+<a name='L26'></a><a href='#L26'>26</a>
+<a name='L27'></a><a href='#L27'>27</a>
+<a name='L28'></a><a href='#L28'>28</a>
+<a name='L29'></a><a href='#L29'>29</a>
+<a name='L30'></a><a href='#L30'>30</a>
+<a name='L31'></a><a href='#L31'>31</a>
+<a name='L32'></a><a href='#L32'>32</a>
+<a name='L33'></a><a href='#L33'>33</a>
+<a name='L34'></a><a href='#L34'>34</a>
+<a name='L35'></a><a href='#L35'>35</a>
+<a name='L36'></a><a href='#L36'>36</a>
+<a name='L37'></a><a href='#L37'>37</a>
+<a name='L38'></a><a href='#L38'>38</a>
+<a name='L39'></a><a href='#L39'>39</a>
+<a name='L40'></a><a href='#L40'>40</a>
+<a name='L41'></a><a href='#L41'>41</a>
+<a name='L42'></a><a href='#L42'>42</a>
+<a name='L43'></a><a href='#L43'>43</a>
+<a name='L44'></a><a href='#L44'>44</a>
+<a name='L45'></a><a href='#L45'>45</a>
+<a name='L46'></a><a href='#L46'>46</a>
+<a name='L47'></a><a href='#L47'>47</a>
+<a name='L48'></a><a href='#L48'>48</a>
+<a name='L49'></a><a href='#L49'>49</a>
+<a name='L50'></a><a href='#L50'>50</a>
+<a name='L51'></a><a href='#L51'>51</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">9x</span>
+<span class="cline-any cline-yes">9x</span>
+<span class="cline-any cline-yes">9x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">9x</span>
+<span class="cline-any cline-yes">5x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-no">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-yes">1x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">9x</span>
+<span class="cline-any cline-yes">4x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">3x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-yes">2x</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span>
+<span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useState } from 'react';
+import type { Feed } from '../types';
+import './FeedList.css';
+&nbsp;
+export default function FeedList() {
+ const [feeds, setFeeds] = useState&lt;Feed[]&gt;([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+&nbsp;
+ useEffect(() =&gt; {
+ fetch('/api/feed/')
+ .then((res) =&gt; {
+ <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) {
+<span class="cstat-no" title="statement not covered" > throw new Error('Failed to fetch feeds');</span>
+ }
+ return res.json();
+ })
+ .then((data) =&gt; {
+ setFeeds(data);
+ setLoading(false);
+ })
+ .catch((err) =&gt; {
+ setError(err.message);
+ setLoading(false);
+ });
+ }, []);
+&nbsp;
+ if (loading) return &lt;div className="feed-list-loading"&gt;Loading feeds...&lt;/div&gt;;
+ if (error) return &lt;div className="feed-list-error"&gt;Error: {error}&lt;/div&gt;;
+&nbsp;
+ return (
+ &lt;div className="feed-list"&gt;
+ &lt;h2&gt;Feeds&lt;/h2&gt;
+ {feeds.length === 0 ? (
+ &lt;p&gt;No feeds found.&lt;/p&gt;
+ ) : (
+ &lt;ul className="feed-list-items"&gt;
+ {feeds.map((feed) =&gt; (
+ &lt;li key={feed._id} className="feed-item"&gt;
+ &lt;a href={feed.web_url} target="_blank" rel="noopener noreferrer" className="feed-title"&gt;
+ {feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >feed.url}</span>
+ &lt;/a&gt;
+ {feed.category &amp;&amp; &lt;span className="feed-category"&gt;{feed.category}&lt;/span&gt;}
+ &lt;/li&gt;
+ ))}
+ &lt;/ul&gt;
+ )}
+ &lt;/div&gt;
+ );
+}
+&nbsp;</pre></td></tr></table></pre>
+
+ <div class='push'></div><!-- for sticky footer -->
+ </div><!-- /wrapper -->
+ <div class='footer quiet pad2 space-top1 center small'>
+ Code coverage generated by
+ <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
+ at 2026-02-13T05:59:33.745Z
+ </div>
+ <script src="../../prettify.js"></script>
+ <script>
+ window.onload = function () {
+ prettyPrint();
+ };
+ </script>
+ <script src="../../sorter.js"></script>
+ <script src="../../block-navigation.js"></script>
+ </body>
+</html>
+ \ No newline at end of file
diff --git a/frontend/coverage/src/components/Login.css.html b/frontend/coverage/src/components/Login.css.html
index 3c90f72..2fb506b 100644
--- a/frontend/coverage/src/components/Login.css.html
+++ b/frontend/coverage/src/components/Login.css.html
@@ -259,7 +259,7 @@ button[type="submit"]:hover {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/Login.tsx.html b/frontend/coverage/src/components/Login.tsx.html
index 9cecda8..9d1052f 100644
--- a/frontend/coverage/src/components/Login.tsx.html
+++ b/frontend/coverage/src/components/Login.tsx.html
@@ -232,7 +232,7 @@ export default function Login() {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/index.html b/frontend/coverage/src/components/index.html
index 2328bb6..d9947b6 100644
--- a/frontend/coverage/src/components/index.html
+++ b/frontend/coverage/src/components/index.html
@@ -23,30 +23,30 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
- <span class="strong">100% </span>
+ <span class="strong">97.14% </span>
<span class="quiet">Statements</span>
- <span class='fraction'>17/17</span>
+ <span class='fraction'>34/35</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">83.33% </span>
<span class="quiet">Branches</span>
- <span class='fraction'>5/6</span>
+ <span class='fraction'>15/18</span>
</div>
<div class='fl pad1y space-right2'>
<span class="strong">100% </span>
<span class="quiet">Functions</span>
- <span class='fraction'>3/3</span>
+ <span class='fraction'>9/9</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">100% </span>
+ <span class="strong">96.96% </span>
<span class="quiet">Lines</span>
- <span class='fraction'>17/17</span>
+ <span class='fraction'>32/33</span>
</div>
@@ -79,6 +79,36 @@
</tr>
</thead>
<tbody><tr>
+ <td class="file empty" data-value="FeedList.css"><a href="FeedList.css.html">FeedList.css</a></td>
+ <td data-value="0" class="pic empty">
+ <div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
+ </td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ <td data-value="0" class="pct empty">0%</td>
+ <td data-value="0" class="abs empty">0/0</td>
+ </tr>
+
+<tr>
+ <td class="file high" data-value="FeedList.tsx"><a href="FeedList.tsx.html">FeedList.tsx</a></td>
+ <td data-value="94.44" class="pic high">
+ <div class="chart"><div class="cover-fill" style="width: 94%"></div><div class="cover-empty" style="width: 6%"></div></div>
+ </td>
+ <td data-value="94.44" class="pct high">94.44%</td>
+ <td data-value="18" class="abs high">17/18</td>
+ <td data-value="83.33" class="pct high">83.33%</td>
+ <td data-value="12" class="abs high">10/12</td>
+ <td data-value="100" class="pct high">100%</td>
+ <td data-value="6" class="abs high">6/6</td>
+ <td data-value="93.75" class="pct high">93.75%</td>
+ <td data-value="16" class="abs high">15/16</td>
+ </tr>
+
+<tr>
<td class="file empty" data-value="Login.css"><a href="Login.css.html">Login.css</a></td>
<td data-value="0" class="pic empty">
<div class="chart"><div class="cover-fill" style="width: 0%"></div><div class="cover-empty" style="width: 100%"></div></div>
@@ -116,7 +146,7 @@
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/index.html b/frontend/coverage/src/index.html
index a0ac8bf..864b5d1 100644
--- a/frontend/coverage/src/index.html
+++ b/frontend/coverage/src/index.html
@@ -116,7 +116,7 @@
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
- at 2026-02-13T05:50:17.889Z
+ at 2026-02-13T05:59:33.745Z
</div>
<script src="../prettify.js"></script>
<script>
diff --git a/frontend/src/App.css b/frontend/src/App.css
index b9d355d..57800a4 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,42 +1,60 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
+/* Resets and Base Styles */
+* {
+ box-sizing: border-box;
}
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
+
+/* Dashboard Layout */
+.dashboard {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: hidden;
+ /* Prevent body scroll */
}
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+.dashboard-header {
+ background: #2c3e50;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-shrink: 0;
}
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
+.dashboard-header h1 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
}
-.card {
- padding: 2em;
+.dashboard-content {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
}
-.read-the-docs {
- color: #888;
+.dashboard-sidebar {
+ width: 300px;
+ background: #f8f9fa;
+ border-right: 1px solid #ddd;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
}
+
+.dashboard-main {
+ flex: 1;
+ padding: 2rem;
+ overflow-y: auto;
+ background: #fff;
+} \ No newline at end of file
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 8e3d805..37a7fab 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -1,3 +1,5 @@
+import React from 'react';
+import '@testing-library/jest-dom';
import { render, screen, waitFor } from '@testing-library/react';
import App from './App';
import { describe, it, expect, vi, beforeEach } from 'vitest';
@@ -18,15 +20,15 @@ describe('App', () => {
});
it('renders dashboard when authenticated', async () => {
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- });
+ (global.fetch as any)
+ .mockResolvedValueOnce({ ok: true }) // /api/auth
+ .mockResolvedValueOnce({ ok: true, json: async () => [] }); // /api/feed/
window.history.pushState({}, 'Test page', '/v2/');
render(<App />);
await waitFor(() => {
- expect(screen.getByText(/dashboard/i)).toBeInTheDocument();
+ expect(screen.getByText(/neko reader/i)).toBeInTheDocument();
});
});
});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index b986198..7d26025 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -31,14 +31,27 @@ function RequireAuth({ children }: { children: React.ReactElement }) {
return children;
}
+import FeedList from './components/FeedList';
+
function Dashboard() {
- // Placeholder for now
return (
- <div>
- <h1>Dashboard</h1>
- <p>Welcome to the new Neko/v2 frontend.</p>
+ <div className="dashboard">
+ <header className="dashboard-header">
+ <h1>Neko Reader</h1>
+ <nav>
+ {/* Add logout later */}
+ </nav>
+ </header>
+ <div className="dashboard-content">
+ <aside className="dashboard-sidebar">
+ <FeedList />
+ </aside>
+ <main className="dashboard-main">
+ <p>Select a feed to view items.</p>
+ </main>
+ </div>
</div>
- )
+ );
}
function App() {
diff --git a/frontend/src/components/FeedList.css b/frontend/src/components/FeedList.css
new file mode 100644
index 0000000..f35ed59
--- /dev/null
+++ b/frontend/src/components/FeedList.css
@@ -0,0 +1,48 @@
+.feed-list {
+ padding: 1rem;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.feed-list h2 {
+ margin-top: 0;
+ border-bottom: 1px solid #eee;
+ padding-bottom: 0.5rem;
+}
+
+.feed-list-items {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.feed-item {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #f0f0f0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.feed-item:last-child {
+ border-bottom: none;
+}
+
+.feed-title {
+ text-decoration: none;
+ color: #333;
+ font-weight: 500;
+}
+
+.feed-title:hover {
+ color: #007bff;
+}
+
+.feed-category {
+ background: #e9ecef;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ color: #666;
+} \ No newline at end of file
diff --git a/frontend/src/components/FeedList.test.tsx b/frontend/src/components/FeedList.test.tsx
new file mode 100644
index 0000000..578e3c2
--- /dev/null
+++ b/frontend/src/components/FeedList.test.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import '@testing-library/jest-dom';
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import FeedList from './FeedList';
+
+describe('FeedList Component', () => {
+ beforeEach(() => {
+ vi.resetAllMocks();
+ global.fetch = vi.fn();
+ });
+
+ it('renders loading state initially', () => {
+ (global.fetch as any).mockImplementation(() => new Promise(() => { }));
+ render(<FeedList />);
+ expect(screen.getByText(/loading feeds/i)).toBeInTheDocument();
+ });
+
+ it('renders list of feeds', async () => {
+ const mockFeeds = [
+ { _id: 1, title: 'Feed One', url: 'http://example.com/rss', web_url: 'http://example.com', category: 'Tech' },
+ { _id: 2, title: 'Feed Two', url: 'http://test.com/rss', web_url: 'http://test.com', category: 'News' },
+ ];
+
+ (global.fetch as any).mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockFeeds,
+ });
+
+ render(<FeedList />);
+
+ await waitFor(() => {
+ expect(screen.getByText('Feed One')).toBeInTheDocument();
+ expect(screen.getByText('Feed Two')).toBeInTheDocument();
+ expect(screen.getByText('Tech')).toBeInTheDocument();
+ });
+ });
+
+ it('handles fetch error', async () => {
+ (global.fetch as any).mockRejectedValueOnce(new Error('API Error'));
+
+ render(<FeedList />);
+
+ await waitFor(() => {
+ expect(screen.getByText(/error: api error/i)).toBeInTheDocument();
+ });
+ });
+
+ it('handles empty feed list', async () => {
+ (global.fetch as any).mockResolvedValueOnce({
+ ok: true,
+ json: async () => [],
+ });
+
+ render(<FeedList />);
+
+ await waitFor(() => {
+ expect(screen.getByText(/no feeds found/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
new file mode 100644
index 0000000..fb7c1de
--- /dev/null
+++ b/frontend/src/components/FeedList.tsx
@@ -0,0 +1,50 @@
+import { useEffect, useState } from 'react';
+import type { Feed } from '../types';
+import './FeedList.css';
+
+export default function FeedList() {
+ const [feeds, setFeeds] = useState<Feed[]>([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ fetch('/api/feed/')
+ .then((res) => {
+ if (!res.ok) {
+ throw new Error('Failed to fetch feeds');
+ }
+ return res.json();
+ })
+ .then((data) => {
+ setFeeds(data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ setError(err.message);
+ setLoading(false);
+ });
+ }, []);
+
+ if (loading) return <div className="feed-list-loading">Loading feeds...</div>;
+ if (error) return <div className="feed-list-error">Error: {error}</div>;
+
+ return (
+ <div className="feed-list">
+ <h2>Feeds</h2>
+ {feeds.length === 0 ? (
+ <p>No feeds found.</p>
+ ) : (
+ <ul className="feed-list-items">
+ {feeds.map((feed) => (
+ <li key={feed._id} className="feed-item">
+ <a href={feed.web_url} target="_blank" rel="noopener noreferrer" className="feed-title">
+ {feed.title || feed.url}
+ </a>
+ {feed.category && <span className="feed-category">{feed.category}</span>}
+ </li>
+ ))}
+ </ul>
+ )}
+ </div>
+ );
+}
diff --git a/frontend/src/components/Login.test.tsx b/frontend/src/components/Login.test.tsx
index 44d1371..ef946e2 100644
--- a/frontend/src/components/Login.test.tsx
+++ b/frontend/src/components/Login.test.tsx
@@ -1,3 +1,5 @@
+import React from 'react';
+import '@testing-library/jest-dom';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { describe, it, expect, vi, beforeEach } from 'vitest';
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
new file mode 100644
index 0000000..905b1dc
--- /dev/null
+++ b/frontend/src/types.ts
@@ -0,0 +1,7 @@
+export interface Feed {
+ _id: number;
+ url: string;
+ web_url: string;
+ title: string;
+ category: string;
+}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
index a9b5a59..f03834c 100644
--- a/frontend/tsconfig.app.json
+++ b/frontend/tsconfig.app.json
@@ -3,11 +3,16 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
"module": "ESNext",
- "types": ["vite/client"],
+ "types": [
+ "vite/client"
+ ],
"skipLibCheck": true,
-
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -15,7 +20,6 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
-
/* Linting */
"strict": true,
"noUnusedLocals": true,
@@ -24,5 +28,11 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
- "include": ["src"]
-}
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "**/*.test.tsx",
+ "**/*.test.ts"
+ ]
+} \ No newline at end of file