aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.thicket/tickets.jsonl4
-rw-r--r--frontend/coverage/clover.xml28
-rw-r--r--frontend/coverage/coverage-final.json4
-rw-r--r--frontend/coverage/index.html30
-rw-r--r--frontend/coverage/src/App.css.html52
-rw-r--r--frontend/coverage/src/App.tsx.html33
-rw-r--r--frontend/coverage/src/components/FeedItem.css.html2
-rw-r--r--frontend/coverage/src/components/FeedItem.tsx.html2
-rw-r--r--frontend/coverage/src/components/FeedItems.css.html2
-rw-r--r--frontend/coverage/src/components/FeedItems.tsx.html2
-rw-r--r--frontend/coverage/src/components/FeedList.css.html2
-rw-r--r--frontend/coverage/src/components/FeedList.tsx.html16
-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.html2
-rw-r--r--frontend/coverage/src/index.html30
-rw-r--r--frontend/src/App.css16
-rw-r--r--frontend/src/App.test.tsx21
-rw-r--r--frontend/src/App.tsx7
-rw-r--r--web/web.go8
-rw-r--r--web/web_test.go26
21 files changed, 216 insertions, 75 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl
index 1960c08..312d198 100644
--- a/.thicket/tickets.jsonl
+++ b/.thicket/tickets.jsonl
@@ -3,7 +3,7 @@
{"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":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":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T05:59:46.161356437Z","updated":"2026-02-13T14:55:14.795643835Z"}
-{"id":"NK-59kbij","title":"Implement Frontend Logout","description":"Add logout button to dashboard header. Call /api/logout (need to create this potentially?). Redirect to /login","type":"","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:58:18.343464645Z","updated":"2026-02-13T14:58:18.343464645Z"}
+{"id":"NK-59kbij","title":"Implement Frontend Logout","description":"Add logout button to dashboard header. Call /api/logout (need to create this potentially?). Redirect to /login","type":"","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T14:58:18.343464645Z","updated":"2026-02-13T15:01:33.783216589Z"}
{"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"}
@@ -14,12 +14,14 @@
{"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":"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-tw0nga","title":"E2E Testing","description":"Set up E2E testing with Playwright or Cypress to verify full flows: Login -\u003e View Feeds -\u003e View Items -\u003e Logout","type":"","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-13T15:01:33.817314728Z","updated":"2026-02-13T15:01:33.817314728Z"}
{"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":"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"}
{"id":"NK-d86tgcs","from_ticket_id":"NK-ed1iah","to_ticket_id":"NK-1phdpf","type":"created_from","created":"2026-02-13T04:26:55.917754798Z"}
+{"id":"NK-dew7hvb","from_ticket_id":"NK-tw0nga","to_ticket_id":"NK-59kbij","type":"created_from","created":"2026-02-13T15:01:33.825547908Z"}
{"id":"NK-dgbrb79","from_ticket_id":"NK-9hx0y7","to_ticket_id":"NK-t0nmbj","type":"created_from","created":"2026-02-13T05:44:01.556027956Z"}
{"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"}
diff --git a/frontend/coverage/clover.xml b/frontend/coverage/clover.xml
index 415b336..a6ecc62 100644
--- a/frontend/coverage/clover.xml
+++ b/frontend/coverage/clover.xml
@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
-<coverage generated="1770994662747" clover="3.2.0">
- <project timestamp="1770994662747" name="All files">
- <metrics statements="87" coveredstatements="79" conditionals="58" coveredconditionals="47" methods="28" coveredmethods="26" elements="173" coveredelements="152" complexity="0" loc="87" ncloc="87" packages="2" files="10" classes="10"/>
+<coverage generated="1770994870810" clover="3.2.0">
+ <project timestamp="1770994870810" name="All files">
+ <metrics statements="89" coveredstatements="81" conditionals="58" coveredconditionals="47" methods="30" coveredmethods="28" elements="177" coveredelements="156" complexity="0" loc="89" ncloc="89" packages="2" files="10" classes="10"/>
<package name="src">
- <metrics statements="15" coveredstatements="12" conditionals="6" coveredconditionals="4" methods="6" coveredmethods="5"/>
+ <metrics statements="17" coveredstatements="14" conditionals="6" coveredconditionals="4" methods="8" coveredmethods="7"/>
<file name="App.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/App.css">
<metrics statements="0" coveredstatements="0" conditionals="0" coveredconditionals="0" methods="0" coveredmethods="0"/>
</file>
<file name="App.tsx" path="/Users/adam/workspace/vibecode/neko/frontend/src/App.tsx">
- <metrics statements="15" coveredstatements="12" conditionals="6" coveredconditionals="4" methods="6" coveredmethods="5"/>
+ <metrics statements="17" coveredstatements="14" conditionals="6" coveredconditionals="4" methods="8" coveredmethods="7"/>
<line num="8" count="2" type="stmt"/>
<line num="9" count="2" type="stmt"/>
<line num="11" count="2" type="stmt"/>
@@ -23,7 +23,9 @@
<line num="28" count="0" type="stmt"/>
<line num="31" count="1" type="stmt"/>
<line num="38" count="1" type="stmt"/>
- <line num="62" count="2" type="stmt"/>
+ <line num="44" count="1" type="stmt"/>
+ <line num="45" count="1" type="stmt"/>
+ <line num="67" count="2" type="stmt"/>
</file>
</package>
<package name="src.components">
@@ -84,10 +86,10 @@
</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="7" count="8" type="stmt"/>
- <line num="8" count="8" type="stmt"/>
- <line num="9" count="8" type="stmt"/>
- <line num="11" count="8" type="stmt"/>
+ <line num="7" count="9" type="stmt"/>
+ <line num="8" count="9" type="stmt"/>
+ <line num="9" count="9" type="stmt"/>
+ <line num="11" count="9" type="stmt"/>
<line num="12" count="5" type="stmt"/>
<line num="14" count="3" type="cond" truecount="1" falsecount="1"/>
<line num="15" count="0" type="stmt"/>
@@ -96,9 +98,9 @@
<line num="21" count="3" type="stmt"/>
<line num="24" count="1" type="stmt"/>
<line num="25" count="1" type="stmt"/>
- <line num="29" count="8" type="cond" truecount="2" falsecount="0"/>
- <line num="30" count="3" type="cond" truecount="2" falsecount="0"/>
- <line num="32" count="2" type="stmt"/>
+ <line num="29" count="9" type="cond" truecount="2" falsecount="0"/>
+ <line num="30" count="4" type="cond" truecount="2" falsecount="0"/>
+ <line num="32" count="3" type="stmt"/>
<line num="40" count="2" type="stmt"/>
</file>
<file name="Login.css" path="/Users/adam/workspace/vibecode/neko/frontend/src/components/Login.css">
diff --git a/frontend/coverage/coverage-final.json b/frontend/coverage/coverage-final.json
index 7ec3f92..2a5fa12 100644
--- a/frontend/coverage/coverage-final.json
+++ b/frontend/coverage/coverage-final.json
@@ -1,11 +1,11 @@
{"/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":38,"column":2},"end":{"line":57,"column":null}},"14":{"start":{"line":62,"column":2},"end":{"line":75,"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":37,"column":9},"end":{"line":37,"column":21}},"loc":{"start":{"line":37,"column":21},"end":{"line":59,"column":null}},"line":37},"5":{"name":"App","decl":{"start":{"line":61,"column":9},"end":{"line":61,"column":15}},"loc":{"start":{"line":61,"column":15},"end":{"line":77,"column":null}},"line":61}},"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:37:9:37:21":4,"s:38:2:57:Infinity":13,"f:61:9:61:15":5,"s:62:2:75: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":38,"column":2},"end":{"line":62,"column":null}},"14":{"start":{"line":44,"column":12},"end":{"line":45,"column":null}},"15":{"start":{"line":45,"column":26},"end":{"line":45,"column":58}},"16":{"start":{"line":67,"column":2},"end":{"line":80,"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":37,"column":9},"end":{"line":37,"column":21}},"loc":{"start":{"line":37,"column":21},"end":{"line":64,"column":null}},"line":37},"5":{"name":"(anonymous_5)","decl":{"start":{"line":43,"column":27},"end":{"line":43,"column":33}},"loc":{"start":{"line":43,"column":33},"end":{"line":46,"column":13}},"line":43},"6":{"name":"(anonymous_6)","decl":{"start":{"line":45,"column":20},"end":{"line":45,"column":26}},"loc":{"start":{"line":45,"column":26},"end":{"line":45,"column":58}},"line":45},"7":{"name":"App","decl":{"start":{"line":66,"column":9},"end":{"line":66,"column":15}},"loc":{"start":{"line":66,"column":15},"end":{"line":82,"column":null}},"line":66}},"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":1,"15":1,"16":2},"f":{"0":2,"1":1,"2":1,"3":0,"4":1,"5":1,"6":1,"7":2},"b":{"0":[1,0],"1":[1,1],"2":[0,1]},"meta":{"lastBranch":3,"lastFunction":8,"lastStatement":17,"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:37:9:37:21":4,"s:38:2:62:Infinity":13,"f:43:27:43:33":5,"s:44:12:45:Infinity":14,"f:45:20:45:26":6,"s:45:26:45:58":15,"f:66:9:66:15":7,"s:67:2:80:Infinity":16}}}
,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}}
,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItem.tsx","statementMap":{"0":{"start":{"line":10,"column":24},"end":{"line":10,"column":null}},"1":{"start":{"line":11,"column":30},"end":{"line":11,"column":null}},"2":{"start":{"line":13,"column":23},"end":{"line":15,"column":null}},"3":{"start":{"line":14,"column":8},"end":{"line":14,"column":null}},"4":{"start":{"line":17,"column":23},"end":{"line":19,"column":null}},"5":{"start":{"line":18,"column":8},"end":{"line":18,"column":null}},"6":{"start":{"line":21,"column":23},"end":{"line":55,"column":null}},"7":{"start":{"line":22,"column":8},"end":{"line":22,"column":null}},"8":{"start":{"line":24,"column":29},"end":{"line":24,"column":null}},"9":{"start":{"line":25,"column":8},"end":{"line":25,"column":null}},"10":{"start":{"line":27,"column":8},"end":{"line":54,"column":null}},"11":{"start":{"line":39,"column":16},"end":{"line":41,"column":null}},"12":{"start":{"line":40,"column":20},"end":{"line":40,"column":null}},"13":{"start":{"line":42,"column":16},"end":{"line":42,"column":null}},"14":{"start":{"line":47,"column":16},"end":{"line":47,"column":null}},"15":{"start":{"line":50,"column":16},"end":{"line":50,"column":null}},"16":{"start":{"line":52,"column":16},"end":{"line":52,"column":null}},"17":{"start":{"line":53,"column":16},"end":{"line":53,"column":null}},"18":{"start":{"line":57,"column":4},"end":{"line":87,"column":null}}},"fnMap":{"0":{"name":"FeedItem","decl":{"start":{"line":9,"column":24},"end":{"line":9,"column":33}},"loc":{"start":{"line":9,"column":71},"end":{"line":89,"column":null}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":13,"column":23},"end":{"line":13,"column":29}},"loc":{"start":{"line":13,"column":29},"end":{"line":15,"column":null}},"line":13},"2":{"name":"(anonymous_2)","decl":{"start":{"line":17,"column":23},"end":{"line":17,"column":29}},"loc":{"start":{"line":17,"column":29},"end":{"line":19,"column":null}},"line":17},"3":{"name":"(anonymous_3)","decl":{"start":{"line":21,"column":23},"end":{"line":21,"column":24}},"loc":{"start":{"line":21,"column":42},"end":{"line":55,"column":null}},"line":21},"4":{"name":"(anonymous_4)","decl":{"start":{"line":38,"column":18},"end":{"line":38,"column":19}},"loc":{"start":{"line":38,"column":27},"end":{"line":43,"column":13}},"line":38},"5":{"name":"(anonymous_5)","decl":{"start":{"line":44,"column":18},"end":{"line":44,"column":24}},"loc":{"start":{"line":44,"column":24},"end":{"line":48,"column":13}},"line":44},"6":{"name":"(anonymous_6)","decl":{"start":{"line":49,"column":19},"end":{"line":49,"column":20}},"loc":{"start":{"line":49,"column":28},"end":{"line":54,"column":13}},"line":49}},"branchMap":{"0":{"loc":{"start":{"line":39,"column":16},"end":{"line":41,"column":null}},"type":"if","locations":[{"start":{"line":39,"column":16},"end":{"line":41,"column":null}},{"start":{},"end":{}}],"line":39},"1":{"loc":{"start":{"line":58,"column":36},"end":{"line":58,"column":65}},"type":"cond-expr","locations":[{"start":{"line":58,"column":48},"end":{"line":58,"column":57}},{"start":{"line":58,"column":57},"end":{"line":58,"column":65}}],"line":58},"2":{"loc":{"start":{"line":58,"column":69},"end":{"line":58,"column":93}},"type":"cond-expr","locations":[{"start":{"line":58,"column":79},"end":{"line":58,"column":91}},{"start":{"line":58,"column":91},"end":{"line":58,"column":93}}],"line":58},"3":{"loc":{"start":{"line":61,"column":21},"end":{"line":61,"column":null}},"type":"binary-expr","locations":[{"start":{"line":61,"column":21},"end":{"line":61,"column":35}},{"start":{"line":61,"column":35},"end":{"line":61,"column":null}}],"line":61},"4":{"loc":{"start":{"line":66,"column":49},"end":{"line":66,"column":84}},"type":"cond-expr","locations":[{"start":{"line":66,"column":61},"end":{"line":66,"column":73}},{"start":{"line":66,"column":73},"end":{"line":66,"column":84}}],"line":66},"5":{"loc":{"start":{"line":67,"column":31},"end":{"line":67,"column":null}},"type":"cond-expr","locations":[{"start":{"line":67,"column":43},"end":{"line":67,"column":62}},{"start":{"line":67,"column":62},"end":{"line":67,"column":null}}],"line":67},"6":{"loc":{"start":{"line":69,"column":25},"end":{"line":69,"column":null}},"type":"cond-expr","locations":[{"start":{"line":69,"column":37},"end":{"line":69,"column":44}},{"start":{"line":69,"column":44},"end":{"line":69,"column":null}}],"line":69},"7":{"loc":{"start":{"line":73,"column":49},"end":{"line":73,"column":93}},"type":"cond-expr","locations":[{"start":{"line":73,"column":64},"end":{"line":73,"column":79}},{"start":{"line":73,"column":79},"end":{"line":73,"column":93}}],"line":73},"8":{"loc":{"start":{"line":74,"column":31},"end":{"line":74,"column":null}},"type":"cond-expr","locations":[{"start":{"line":74,"column":46},"end":{"line":74,"column":57}},{"start":{"line":74,"column":57},"end":{"line":74,"column":null}}],"line":74},"9":{"loc":{"start":{"line":76,"column":25},"end":{"line":76,"column":null}},"type":"cond-expr","locations":[{"start":{"line":76,"column":40},"end":{"line":76,"column":46}},{"start":{"line":76,"column":46},"end":{"line":76,"column":null}}],"line":76},"10":{"loc":{"start":{"line":82,"column":17},"end":{"line":82,"column":null}},"type":"binary-expr","locations":[{"start":{"line":82,"column":17},"end":{"line":82,"column":36}},{"start":{"line":82,"column":36},"end":{"line":82,"column":null}}],"line":82},"11":{"loc":{"start":{"line":84,"column":13},"end":{"line":85,"column":null}},"type":"binary-expr","locations":[{"start":{"line":84,"column":13},"end":{"line":84,"column":null}},{"start":{"line":85,"column":16},"end":{"line":85,"column":null}}],"line":84}},"s":{"0":12,"1":12,"2":12,"3":2,"4":12,"5":1,"6":12,"7":3,"8":3,"9":3,"10":3,"11":2,"12":0,"13":2,"14":2,"15":1,"16":1,"17":1,"18":12},"f":{"0":12,"1":2,"2":1,"3":3,"4":2,"5":2,"6":1},"b":{"0":[0,2],"1":[4,8],"2":[3,9],"3":[12,0],"4":[4,8],"5":[4,8],"6":[4,8],"7":[2,10],"8":[2,10],"9":[2,10],"10":[12,10],"11":[12,10]},"meta":{"lastBranch":12,"lastFunction":7,"lastStatement":19,"seen":{"f:9:24:9:33":0,"s:10:24:10:Infinity":0,"s:11:30:11:Infinity":1,"s:13:23:15:Infinity":2,"f:13:23:13:29":1,"s:14:8:14:Infinity":3,"s:17:23:19:Infinity":4,"f:17:23:17:29":2,"s:18:8:18:Infinity":5,"s:21:23:55:Infinity":6,"f:21:23:21:24":3,"s:22:8:22:Infinity":7,"s:24:29:24:Infinity":8,"s:25:8:25:Infinity":9,"s:27:8:54:Infinity":10,"f:38:18:38:19":4,"b:39:16:41:Infinity:undefined:undefined:undefined:undefined":0,"s:39:16:41:Infinity":11,"s:40:20:40:Infinity":12,"s:42:16:42:Infinity":13,"f:44:18:44:24":5,"s:47:16:47:Infinity":14,"f:49:19:49:20":6,"s:50:16:50:Infinity":15,"s:52:16:52:Infinity":16,"s:53:16:53:Infinity":17,"s:57:4:87:Infinity":18,"b:58:48:58:57:58:57:58:65":1,"b:58:79:58:91:58:91:58:93":2,"b:61:21:61:35:61:35:61:Infinity":3,"b:66:61:66:73:66:73:66:84":4,"b:67:43:67:62:67:62:67:Infinity":5,"b:69:37:69:44:69:44:69:Infinity":6,"b:73:64:73:79:73:79:73:93":7,"b:74:46:74:57:74:57:74:Infinity":8,"b:76:40:76:46:76:46:76:Infinity":9,"b:82:17:82:36:82:36:82:Infinity":10,"b:84:13:84:Infinity:85:16:85:Infinity":11}}}
,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.css","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}}
,"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx": {"path":"/Users/adam/workspace/vibecode/neko/frontend/src/components/FeedItems.tsx","statementMap":{"0":{"start":{"line":8,"column":19},"end":{"line":8,"column":null}},"1":{"start":{"line":9,"column":26},"end":{"line":9,"column":null}},"2":{"start":{"line":10,"column":30},"end":{"line":10,"column":null}},"3":{"start":{"line":11,"column":26},"end":{"line":11,"column":null}},"4":{"start":{"line":13,"column":4},"end":{"line":36,"column":null}},"5":{"start":{"line":14,"column":8},"end":{"line":14,"column":null}},"6":{"start":{"line":15,"column":8},"end":{"line":15,"column":null}},"7":{"start":{"line":17,"column":20},"end":{"line":19,"column":null}},"8":{"start":{"line":21,"column":8},"end":{"line":35,"column":null}},"9":{"start":{"line":23,"column":16},"end":{"line":25,"column":null}},"10":{"start":{"line":24,"column":20},"end":{"line":24,"column":null}},"11":{"start":{"line":26,"column":16},"end":{"line":26,"column":null}},"12":{"start":{"line":29,"column":16},"end":{"line":29,"column":null}},"13":{"start":{"line":30,"column":16},"end":{"line":30,"column":null}},"14":{"start":{"line":33,"column":16},"end":{"line":33,"column":null}},"15":{"start":{"line":34,"column":16},"end":{"line":34,"column":null}},"16":{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},"17":{"start":{"line":38,"column":17},"end":{"line":38,"column":null}},"18":{"start":{"line":39,"column":4},"end":{"line":39,"column":null}},"19":{"start":{"line":39,"column":15},"end":{"line":39,"column":null}},"20":{"start":{"line":41,"column":4},"end":{"line":54,"column":null}},"21":{"start":{"line":50,"column":24},"end":{"line":50,"column":null}}},"fnMap":{"0":{"name":"FeedItems","decl":{"start":{"line":7,"column":24},"end":{"line":7,"column":36}},"loc":{"start":{"line":7,"column":36},"end":{"line":56,"column":null}},"line":7},"1":{"name":"(anonymous_1)","decl":{"start":{"line":13,"column":14},"end":{"line":13,"column":20}},"loc":{"start":{"line":13,"column":20},"end":{"line":36,"column":7}},"line":13},"2":{"name":"(anonymous_2)","decl":{"start":{"line":22,"column":18},"end":{"line":22,"column":19}},"loc":{"start":{"line":22,"column":27},"end":{"line":27,"column":13}},"line":22},"3":{"name":"(anonymous_3)","decl":{"start":{"line":28,"column":18},"end":{"line":28,"column":19}},"loc":{"start":{"line":28,"column":28},"end":{"line":31,"column":13}},"line":28},"4":{"name":"(anonymous_4)","decl":{"start":{"line":32,"column":19},"end":{"line":32,"column":20}},"loc":{"start":{"line":32,"column":28},"end":{"line":35,"column":13}},"line":32},"5":{"name":"(anonymous_5)","decl":{"start":{"line":49,"column":31},"end":{"line":49,"column":32}},"loc":{"start":{"line":50,"column":24},"end":{"line":50,"column":null}},"line":50}},"branchMap":{"0":{"loc":{"start":{"line":17,"column":20},"end":{"line":19,"column":null}},"type":"cond-expr","locations":[{"start":{"line":18,"column":14},"end":{"line":18,"column":null}},{"start":{"line":19,"column":14},"end":{"line":19,"column":null}}],"line":17},"1":{"loc":{"start":{"line":23,"column":16},"end":{"line":25,"column":null}},"type":"if","locations":[{"start":{"line":23,"column":16},"end":{"line":25,"column":null}},{"start":{},"end":{}}],"line":23},"2":{"loc":{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},"type":"if","locations":[{"start":{"line":38,"column":4},"end":{"line":38,"column":null}},{"start":{},"end":{}}],"line":38},"3":{"loc":{"start":{"line":39,"column":4},"end":{"line":39,"column":null}},"type":"if","locations":[{"start":{"line":39,"column":4},"end":{"line":39,"column":null}},{"start":{},"end":{}}],"line":39},"4":{"loc":{"start":{"line":45,"column":13},"end":{"line":52,"column":null}},"type":"cond-expr","locations":[{"start":{"line":46,"column":16},"end":{"line":46,"column":null}},{"start":{"line":48,"column":16},"end":{"line":52,"column":null}}],"line":45}},"s":{"0":3,"1":3,"2":3,"3":3,"4":3,"5":2,"6":2,"7":2,"8":2,"9":1,"10":0,"11":1,"12":1,"13":1,"14":0,"15":0,"16":3,"17":2,"18":1,"19":1,"20":1,"21":2},"f":{"0":3,"1":2,"2":1,"3":1,"4":0,"5":2},"b":{"0":[2,0],"1":[0,1],"2":[2,1],"3":[0,1],"4":[0,1]},"meta":{"lastBranch":5,"lastFunction":6,"lastStatement":22,"seen":{"f:7:24:7:36":0,"s:8:19:8:Infinity":0,"s:9:26:9:Infinity":1,"s:10:30:10:Infinity":2,"s:11:26:11:Infinity":3,"s:13:4:36:Infinity":4,"f:13:14:13:20":1,"s:14:8:14:Infinity":5,"s:15:8:15:Infinity":6,"s:17:20:19:Infinity":7,"b:18:14:18:Infinity:19:14:19:Infinity":0,"s:21:8:35:Infinity":8,"f:22:18:22:19":2,"b:23:16:25:Infinity:undefined:undefined:undefined:undefined":1,"s:23:16:25:Infinity":9,"s:24:20:24:Infinity":10,"s:26:16:26:Infinity":11,"f:28:18:28:19":3,"s:29:16:29:Infinity":12,"s:30:16:30:Infinity":13,"f:32:19:32:20":4,"s:33:16:33:Infinity":14,"s:34:16:34:Infinity":15,"b:38:4:38:Infinity:undefined:undefined:undefined:undefined":2,"s:38:4:38:Infinity":16,"s:38:17:38:Infinity":17,"b:39:4:39:Infinity:undefined:undefined:undefined:undefined":3,"s:39:4:39:Infinity":18,"s:39:15:39:Infinity":19,"s:41:4:54:Infinity":20,"b:46:16:46:Infinity:48:16:52:Infinity":4,"f:49:31:49:32":5,"s:50:24:50:Infinity":21}}}
,"/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":7,"column":26},"end":{"line":7,"column":null}},"1":{"start":{"line":8,"column":30},"end":{"line":8,"column":null}},"2":{"start":{"line":9,"column":26},"end":{"line":9,"column":null}},"3":{"start":{"line":11,"column":4},"end":{"line":27,"column":null}},"4":{"start":{"line":12,"column":8},"end":{"line":26,"column":null}},"5":{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},"6":{"start":{"line":15,"column":20},"end":{"line":15,"column":null}},"7":{"start":{"line":17,"column":16},"end":{"line":17,"column":null}},"8":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"9":{"start":{"line":21,"column":16},"end":{"line":21,"column":null}},"10":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"13":{"start":{"line":29,"column":17},"end":{"line":29,"column":null}},"14":{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},"15":{"start":{"line":30,"column":15},"end":{"line":30,"column":null}},"16":{"start":{"line":32,"column":4},"end":{"line":49,"column":null}},"17":{"start":{"line":40,"column":24},"end":{"line":45,"column":null}}},"fnMap":{"0":{"name":"FeedList","decl":{"start":{"line":6,"column":24},"end":{"line":6,"column":35}},"loc":{"start":{"line":6,"column":35},"end":{"line":51,"column":null}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":14},"end":{"line":11,"column":20}},"loc":{"start":{"line":11,"column":20},"end":{"line":27,"column":7}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":18},"end":{"line":13,"column":19}},"loc":{"start":{"line":13,"column":27},"end":{"line":18,"column":13}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":19,"column":18},"end":{"line":19,"column":19}},"loc":{"start":{"line":19,"column":28},"end":{"line":22,"column":13}},"line":19},"4":{"name":"(anonymous_4)","decl":{"start":{"line":23,"column":19},"end":{"line":23,"column":20}},"loc":{"start":{"line":23,"column":28},"end":{"line":26,"column":13}},"line":23},"5":{"name":"(anonymous_5)","decl":{"start":{"line":39,"column":31},"end":{"line":39,"column":32}},"loc":{"start":{"line":40,"column":24},"end":{"line":45,"column":null}},"line":40}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},{"start":{},"end":{}}],"line":14},"1":{"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},"2":{"loc":{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},"type":"if","locations":[{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},{"start":{},"end":{}}],"line":30},"3":{"loc":{"start":{"line":35,"column":13},"end":{"line":47,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":16},"end":{"line":36,"column":null}},{"start":{"line":38,"column":16},"end":{"line":47,"column":null}}],"line":35},"4":{"loc":{"start":{"line":42,"column":33},"end":{"line":42,"column":null}},"type":"binary-expr","locations":[{"start":{"line":42,"column":33},"end":{"line":42,"column":47}},{"start":{"line":42,"column":47},"end":{"line":42,"column":null}}],"line":42},"5":{"loc":{"start":{"line":44,"column":29},"end":{"line":44,"column":null}},"type":"binary-expr","locations":[{"start":{"line":44,"column":29},"end":{"line":44,"column":46}},{"start":{"line":44,"column":46},"end":{"line":44,"column":null}}],"line":44}},"s":{"0":8,"1":8,"2":8,"3":8,"4":5,"5":3,"6":0,"7":3,"8":3,"9":3,"10":1,"11":1,"12":8,"13":5,"14":3,"15":3,"16":2,"17":2},"f":{"0":8,"1":5,"2":3,"3":3,"4":1,"5":2},"b":{"0":[0,3],"1":[5,3],"2":[1,2],"3":[1,1],"4":[2,0],"5":[2,2]},"meta":{"lastBranch":6,"lastFunction":6,"lastStatement":18,"seen":{"f:6:24:6:35":0,"s:7:26:7:Infinity":0,"s:8:30:8:Infinity":1,"s:9:26:9:Infinity":2,"s:11:4:27:Infinity":3,"f:11:14:11:20":1,"s:12:8:26:Infinity":4,"f:13:18:13:19":2,"b:14:16:16:Infinity:undefined:undefined:undefined:undefined":0,"s:14:16:16:Infinity":5,"s:15:20:15:Infinity":6,"s:17:16:17:Infinity":7,"f:19:18:19:19":3,"s:20:16:20:Infinity":8,"s:21:16:21:Infinity":9,"f:23:19:23:20":4,"s:24:16:24:Infinity":10,"s:25:16:25:Infinity":11,"b:29:4:29:Infinity:undefined:undefined:undefined:undefined":1,"s:29:4:29:Infinity":12,"s:29:17:29:Infinity":13,"b:30:4:30:Infinity:undefined:undefined:undefined:undefined":2,"s:30:4:30:Infinity":14,"s:30:15:30:Infinity":15,"s:32:4:49:Infinity":16,"b:36:16:36:Infinity:38:16:47:Infinity":3,"f:39:31:39:32":5,"s:40:24:45:Infinity":17,"b:42:33:42:47:42:47:42:Infinity":4,"b:44:29:44:46:44:46:44:Infinity":5}}}
+,"/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":7,"column":26},"end":{"line":7,"column":null}},"1":{"start":{"line":8,"column":30},"end":{"line":8,"column":null}},"2":{"start":{"line":9,"column":26},"end":{"line":9,"column":null}},"3":{"start":{"line":11,"column":4},"end":{"line":27,"column":null}},"4":{"start":{"line":12,"column":8},"end":{"line":26,"column":null}},"5":{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},"6":{"start":{"line":15,"column":20},"end":{"line":15,"column":null}},"7":{"start":{"line":17,"column":16},"end":{"line":17,"column":null}},"8":{"start":{"line":20,"column":16},"end":{"line":20,"column":null}},"9":{"start":{"line":21,"column":16},"end":{"line":21,"column":null}},"10":{"start":{"line":24,"column":16},"end":{"line":24,"column":null}},"11":{"start":{"line":25,"column":16},"end":{"line":25,"column":null}},"12":{"start":{"line":29,"column":4},"end":{"line":29,"column":null}},"13":{"start":{"line":29,"column":17},"end":{"line":29,"column":null}},"14":{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},"15":{"start":{"line":30,"column":15},"end":{"line":30,"column":null}},"16":{"start":{"line":32,"column":4},"end":{"line":49,"column":null}},"17":{"start":{"line":40,"column":24},"end":{"line":45,"column":null}}},"fnMap":{"0":{"name":"FeedList","decl":{"start":{"line":6,"column":24},"end":{"line":6,"column":35}},"loc":{"start":{"line":6,"column":35},"end":{"line":51,"column":null}},"line":6},"1":{"name":"(anonymous_1)","decl":{"start":{"line":11,"column":14},"end":{"line":11,"column":20}},"loc":{"start":{"line":11,"column":20},"end":{"line":27,"column":7}},"line":11},"2":{"name":"(anonymous_2)","decl":{"start":{"line":13,"column":18},"end":{"line":13,"column":19}},"loc":{"start":{"line":13,"column":27},"end":{"line":18,"column":13}},"line":13},"3":{"name":"(anonymous_3)","decl":{"start":{"line":19,"column":18},"end":{"line":19,"column":19}},"loc":{"start":{"line":19,"column":28},"end":{"line":22,"column":13}},"line":19},"4":{"name":"(anonymous_4)","decl":{"start":{"line":23,"column":19},"end":{"line":23,"column":20}},"loc":{"start":{"line":23,"column":28},"end":{"line":26,"column":13}},"line":23},"5":{"name":"(anonymous_5)","decl":{"start":{"line":39,"column":31},"end":{"line":39,"column":32}},"loc":{"start":{"line":40,"column":24},"end":{"line":45,"column":null}},"line":40}},"branchMap":{"0":{"loc":{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},"type":"if","locations":[{"start":{"line":14,"column":16},"end":{"line":16,"column":null}},{"start":{},"end":{}}],"line":14},"1":{"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},"2":{"loc":{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},"type":"if","locations":[{"start":{"line":30,"column":4},"end":{"line":30,"column":null}},{"start":{},"end":{}}],"line":30},"3":{"loc":{"start":{"line":35,"column":13},"end":{"line":47,"column":null}},"type":"cond-expr","locations":[{"start":{"line":36,"column":16},"end":{"line":36,"column":null}},{"start":{"line":38,"column":16},"end":{"line":47,"column":null}}],"line":35},"4":{"loc":{"start":{"line":42,"column":33},"end":{"line":42,"column":null}},"type":"binary-expr","locations":[{"start":{"line":42,"column":33},"end":{"line":42,"column":47}},{"start":{"line":42,"column":47},"end":{"line":42,"column":null}}],"line":42},"5":{"loc":{"start":{"line":44,"column":29},"end":{"line":44,"column":null}},"type":"binary-expr","locations":[{"start":{"line":44,"column":29},"end":{"line":44,"column":46}},{"start":{"line":44,"column":46},"end":{"line":44,"column":null}}],"line":44}},"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:6:24:6:35":0,"s:7:26:7:Infinity":0,"s:8:30:8:Infinity":1,"s:9:26:9:Infinity":2,"s:11:4:27:Infinity":3,"f:11:14:11:20":1,"s:12:8:26:Infinity":4,"f:13:18:13:19":2,"b:14:16:16:Infinity:undefined:undefined:undefined:undefined":0,"s:14:16:16:Infinity":5,"s:15:20:15:Infinity":6,"s:17:16:17:Infinity":7,"f:19:18:19:19":3,"s:20:16:20:Infinity":8,"s:21:16:21:Infinity":9,"f:23:19:23:20":4,"s:24:16:24:Infinity":10,"s:25:16:25:Infinity":11,"b:29:4:29:Infinity:undefined:undefined:undefined:undefined":1,"s:29:4:29:Infinity":12,"s:29:17:29:Infinity":13,"b:30:4:30:Infinity:undefined:undefined:undefined:undefined":2,"s:30:4:30:Infinity":14,"s:30:15:30:Infinity":15,"s:32:4:49:Infinity":16,"b:36:16:36:Infinity:38:16:47:Infinity":3,"f:39:31:39:32":5,"s:40:24:45:Infinity":17,"b:42:33:42:47:42:47:42:Infinity":4,"b:44:29:44:46:44:46:44: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 b2f47b5..51ed5a6 100644
--- a/frontend/coverage/index.html
+++ b/frontend/coverage/index.html
@@ -23,9 +23,9 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
- <span class="strong">91.2% </span>
+ <span class="strong">91.39% </span>
<span class="quiet">Statements</span>
- <span class='fraction'>83/91</span>
+ <span class='fraction'>85/93</span>
</div>
@@ -37,16 +37,16 @@
<div class='fl pad1y space-right2'>
- <span class="strong">92.85% </span>
+ <span class="strong">93.33% </span>
<span class="quiet">Functions</span>
- <span class='fraction'>26/28</span>
+ <span class='fraction'>28/30</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">90.8% </span>
+ <span class="strong">91.01% </span>
<span class="quiet">Lines</span>
- <span class='fraction'>79/87</span>
+ <span class='fraction'>81/89</span>
</div>
@@ -80,17 +80,17 @@
</thead>
<tbody><tr>
<td class="file high" data-value="src"><a href="src/index.html">src</a></td>
- <td data-value="80" class="pic high">
- <div class="chart"><div class="cover-fill" style="width: 80%"></div><div class="cover-empty" style="width: 20%"></div></div>
+ <td data-value="82.35" class="pic high">
+ <div class="chart"><div class="cover-fill" style="width: 82%"></div><div class="cover-empty" style="width: 18%"></div></div>
</td>
- <td data-value="80" class="pct high">80%</td>
- <td data-value="15" class="abs high">12/15</td>
+ <td data-value="82.35" class="pct high">82.35%</td>
+ <td data-value="17" class="abs high">14/17</td>
<td data-value="66.66" class="pct medium">66.66%</td>
<td data-value="6" class="abs medium">4/6</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="80" class="pct high">80%</td>
- <td data-value="15" class="abs high">12/15</td>
+ <td data-value="87.5" class="pct high">87.5%</td>
+ <td data-value="8" class="abs high">7/8</td>
+ <td data-value="82.35" class="pct high">82.35%</td>
+ <td data-value="17" class="abs high">14/17</td>
</tr>
<tr>
@@ -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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/App.css.html b/frontend/coverage/src/App.css.html
index 6ef3c1f..d927374 100644
--- a/frontend/coverage/src/App.css.html
+++ b/frontend/coverage/src/App.css.html
@@ -122,7 +122,39 @@
<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>
+<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>
+<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>
+<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>
@@ -240,6 +272,22 @@ body {
padding: 2rem;
overflow-y: auto;
background: #fff;
+}
+&nbsp;
+.logout-btn {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 0.9rem;
+}
+&nbsp;
+.logout-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.5);
}</pre></td></tr></table></pre>
<div class='push'></div><!-- for sticky footer -->
@@ -247,7 +295,7 @@ body {
<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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/App.tsx.html b/frontend/coverage/src/App.tsx.html
index 980313b..cdf0b6b 100644
--- a/frontend/coverage/src/App.tsx.html
+++ b/frontend/coverage/src/App.tsx.html
@@ -23,9 +23,9 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
- <span class="strong">80% </span>
+ <span class="strong">82.35% </span>
<span class="quiet">Statements</span>
- <span class='fraction'>12/15</span>
+ <span class='fraction'>14/17</span>
</div>
@@ -37,16 +37,16 @@
<div class='fl pad1y space-right2'>
- <span class="strong">83.33% </span>
+ <span class="strong">87.5% </span>
<span class="quiet">Functions</span>
- <span class='fraction'>5/6</span>
+ <span class='fraction'>7/8</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">80% </span>
+ <span class="strong">82.35% </span>
<span class="quiet">Lines</span>
- <span class='fraction'>12/15</span>
+ <span class='fraction'>14/17</span>
</div>
@@ -142,7 +142,12 @@
<a name='L77'></a><a href='#L77'>77</a>
<a name='L78'></a><a href='#L78'>78</a>
<a name='L79'></a><a href='#L79'>79</a>
-<a name='L80'></a><a href='#L80'>80</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
+<a name='L80'></a><a href='#L80'>80</a>
+<a name='L81'></a><a href='#L81'>81</a>
+<a name='L82'></a><a href='#L82'>82</a>
+<a name='L83'></a><a href='#L83'>83</a>
+<a name='L84'></a><a href='#L84'>84</a>
+<a name='L85'></a><a href='#L85'>85</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>
@@ -185,6 +190,11 @@
<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-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-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
<span class="cline-any cline-neutral">&nbsp;</span>
@@ -263,7 +273,12 @@ function Dashboard() {
&lt;header className="dashboard-header"&gt;
&lt;h1&gt;Neko Reader&lt;/h1&gt;
&lt;nav&gt;
- {/* Add logout later */}
+ &lt;button onClick={() =&gt; {
+ fetch('/api/logout', { method: 'POST' })
+ .then(() =&gt; window.location.href = '/login/');
+ }} className="logout-btn"&gt;
+ Logout
+ &lt;/button&gt;
&lt;/nav&gt;
&lt;/header&gt;
&lt;div className="dashboard-content"&gt;
@@ -307,7 +322,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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/FeedItem.css.html b/frontend/coverage/src/components/FeedItem.css.html
index 420b55b..7cd7331 100644
--- a/frontend/coverage/src/components/FeedItem.css.html
+++ b/frontend/coverage/src/components/FeedItem.css.html
@@ -310,7 +310,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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/FeedItem.tsx.html b/frontend/coverage/src/components/FeedItem.tsx.html
index 418ab70..9f34545 100644
--- a/frontend/coverage/src/components/FeedItem.tsx.html
+++ b/frontend/coverage/src/components/FeedItem.tsx.html
@@ -337,7 +337,7 @@ export default function FeedItem({ item: initialItem }: FeedItemProps) {
<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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/FeedItems.css.html b/frontend/coverage/src/components/FeedItems.css.html
index 2140fe0..7cdb076 100644
--- a/frontend/coverage/src/components/FeedItems.css.html
+++ b/frontend/coverage/src/components/FeedItems.css.html
@@ -109,7 +109,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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/FeedItems.tsx.html b/frontend/coverage/src/components/FeedItems.tsx.html
index ed7bf57..5ff592e 100644
--- a/frontend/coverage/src/components/FeedItems.tsx.html
+++ b/frontend/coverage/src/components/FeedItems.tsx.html
@@ -238,7 +238,7 @@ export default function FeedItems() {
<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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</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
index baacb65..e44fb5c 100644
--- a/frontend/coverage/src/components/FeedList.css.html
+++ b/frontend/coverage/src/components/FeedList.css.html
@@ -211,7 +211,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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/FeedList.tsx.html b/frontend/coverage/src/components/FeedList.tsx.html
index 3eb8c95..c858d56 100644
--- a/frontend/coverage/src/components/FeedList.tsx.html
+++ b/frontend/coverage/src/components/FeedList.tsx.html
@@ -120,11 +120,11 @@
<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">8x</span>
-<span class="cline-any cline-yes">8x</span>
-<span class="cline-any cline-yes">8x</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">8x</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>
@@ -142,10 +142,10 @@
<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">8x</span>
-<span class="cline-any cline-yes">3x</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">2x</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>
@@ -223,7 +223,7 @@ export default function FeedList() {
<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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/Login.css.html b/frontend/coverage/src/components/Login.css.html
index 2dab905..b828e75 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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</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 7fc019c..ef24b05 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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/components/index.html b/frontend/coverage/src/components/index.html
index f42c6e2..6af23d0 100644
--- a/frontend/coverage/src/components/index.html
+++ b/frontend/coverage/src/components/index.html
@@ -206,7 +206,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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../../prettify.js"></script>
<script>
diff --git a/frontend/coverage/src/index.html b/frontend/coverage/src/index.html
index cd50f96..dc2e167 100644
--- a/frontend/coverage/src/index.html
+++ b/frontend/coverage/src/index.html
@@ -23,9 +23,9 @@
<div class='clearfix'>
<div class='fl pad1y space-right2'>
- <span class="strong">80% </span>
+ <span class="strong">82.35% </span>
<span class="quiet">Statements</span>
- <span class='fraction'>12/15</span>
+ <span class='fraction'>14/17</span>
</div>
@@ -37,16 +37,16 @@
<div class='fl pad1y space-right2'>
- <span class="strong">83.33% </span>
+ <span class="strong">87.5% </span>
<span class="quiet">Functions</span>
- <span class='fraction'>5/6</span>
+ <span class='fraction'>7/8</span>
</div>
<div class='fl pad1y space-right2'>
- <span class="strong">80% </span>
+ <span class="strong">82.35% </span>
<span class="quiet">Lines</span>
- <span class='fraction'>12/15</span>
+ <span class='fraction'>14/17</span>
</div>
@@ -95,17 +95,17 @@
<tr>
<td class="file high" data-value="App.tsx"><a href="App.tsx.html">App.tsx</a></td>
- <td data-value="80" class="pic high">
- <div class="chart"><div class="cover-fill" style="width: 80%"></div><div class="cover-empty" style="width: 20%"></div></div>
+ <td data-value="82.35" class="pic high">
+ <div class="chart"><div class="cover-fill" style="width: 82%"></div><div class="cover-empty" style="width: 18%"></div></div>
</td>
- <td data-value="80" class="pct high">80%</td>
- <td data-value="15" class="abs high">12/15</td>
+ <td data-value="82.35" class="pct high">82.35%</td>
+ <td data-value="17" class="abs high">14/17</td>
<td data-value="66.66" class="pct medium">66.66%</td>
<td data-value="6" class="abs medium">4/6</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="80" class="pct high">80%</td>
- <td data-value="15" class="abs high">12/15</td>
+ <td data-value="87.5" class="pct high">87.5%</td>
+ <td data-value="8" class="abs high">7/8</td>
+ <td data-value="82.35" class="pct high">82.35%</td>
+ <td data-value="17" class="abs high">14/17</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-13T14:57:42.722Z
+ at 2026-02-13T15:01:10.781Z
</div>
<script src="../prettify.js"></script>
<script>
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 57800a4..69edabd 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -57,4 +57,20 @@ body {
padding: 2rem;
overflow-y: auto;
background: #fff;
+}
+
+.logout-btn {
+ background: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-size: 0.9rem;
+}
+
+.logout-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.5);
} \ No newline at end of file
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 37a7fab..5614d7d 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import '@testing-library/jest-dom';
-import { render, screen, waitFor } from '@testing-library/react';
+import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import App from './App';
import { describe, it, expect, vi, beforeEach } from 'vitest';
@@ -30,5 +30,24 @@ describe('App', () => {
await waitFor(() => {
expect(screen.getByText(/neko reader/i)).toBeInTheDocument();
});
+
+ // Test Logout
+ const logoutBtn = screen.getByText(/logout/i);
+ expect(logoutBtn).toBeInTheDocument();
+
+ // Mock window.location
+ Object.defineProperty(window, 'location', {
+ configurable: true,
+ value: { href: '' },
+ });
+
+ (global.fetch as any).mockResolvedValueOnce({ ok: true });
+
+ fireEvent.click(logoutBtn);
+
+ await waitFor(() => {
+ expect(global.fetch).toHaveBeenCalledWith('/api/logout', expect.objectContaining({ method: 'POST' }));
+ expect(window.location.href).toBe('/login/');
+ });
});
});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index bc4e097..7c9d555 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -40,7 +40,12 @@ function Dashboard() {
<header className="dashboard-header">
<h1>Neko Reader</h1>
<nav>
- {/* Add logout later */}
+ <button onClick={() => {
+ fetch('/api/logout', { method: 'POST' })
+ .then(() => window.location.href = '/login/');
+ }} className="logout-btn">
+ Logout
+ </button>
</nav>
</header>
<div className="dashboard-content">
diff --git a/web/web.go b/web/web.go
index ca5e2a2..10e9b2f 100644
--- a/web/web.go
+++ b/web/web.go
@@ -205,9 +205,17 @@ func Serve() {
http.HandleFunc("/login/", loginHandler)
http.HandleFunc("/logout/", logoutHandler)
http.HandleFunc("/api/login", apiLoginHandler)
+ http.HandleFunc("/api/logout", apiLogoutHandler)
http.HandleFunc("/api/auth", apiAuthStatusHandler)
http.HandleFunc("/", AuthWrap(indexHandler))
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(config.Config.Port), nil))
}
+
+func apiLogoutHandler(w http.ResponseWriter, r *http.Request) {
+ c := http.Cookie{Name: AuthCookie, Value: "", Path: "/", MaxAge: -1, HttpOnly: false}
+ http.SetCookie(w, &c)
+ w.Header().Set("Content-Type", "application/json")
+ fmt.Fprintf(w, `{"status":"ok"}`)
+}
diff --git a/web/web_test.go b/web/web_test.go
index a73a6c9..156bbef 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -356,6 +356,32 @@ func TestApiAuthStatusHandlerAuthenticated(t *testing.T) {
if body != `{"status":"ok", "authenticated":true}` {
t.Errorf("Expected authenticated true, got %q", body)
}
+
+ // Test Logout
+ req, _ = http.NewRequest("POST", "/api/logout", nil)
+ rr = httptest.NewRecorder()
+ handler := http.HandlerFunc(apiLogoutHandler)
+ handler.ServeHTTP(rr, req)
+
+ if status := rr.Code; status != http.StatusOK {
+ t.Errorf("logout handler returned wrong status code: got %v want %v",
+ status, http.StatusOK)
+ }
+
+ // Verify cookie is cleared
+ cookies := rr.Result().Cookies()
+ found := false
+ for _, c := range cookies {
+ if c.Name == AuthCookie {
+ found = true
+ if c.MaxAge != -1 {
+ t.Errorf("auth cookie not expired: got MaxAge %v want -1", c.MaxAge)
+ }
+ }
+ }
+ if !found {
+ t.Errorf("auth cookie not found in response")
+ }
}
func TestApiAuthStatusHandlerUnauthenticated(t *testing.T) {