diff options
| -rw-r--r-- | .thicket/tickets.jsonl | 7 | ||||
| -rw-r--r-- | frontend-vanilla/src/main.test.ts | 39 | ||||
| -rw-r--r-- | frontend-vanilla/src/main.ts | 26 |
3 files changed, 71 insertions, 1 deletions
diff --git a/.thicket/tickets.jsonl b/.thicket/tickets.jsonl index ca72f4e..8c20e9c 100644 --- a/.thicket/tickets.jsonl +++ b/.thicket/tickets.jsonl @@ -29,6 +29,7 @@ {"id":"NK-7u97bb","title":"Freeing up space by purging very old items","description":"I have been running neko for so long that my production database is 1.4GB. Come up with a tool (ok to run it from command line) that purges some super old feed items to save space. Probably needs some variables on age, etc. Think carefully about the algorithm! it should be accessible from the CLI to start, although maybe we should show \"db size\" in settings too with an option to clean up.","type":"feature","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-15T03:01:05.643515805Z","updated":"2026-02-15T18:51:26.631274215Z"} {"id":"NK-7xuajb","title":"[security] Add HTTP Security Headers","description":"Add middleware to set standard security headers: Content-Security-Policy (restrict sources), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin.","type":"","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T16:35:59.320775688Z","updated":"2026-02-14T17:20:46.397582923Z"} {"id":"NK-897v23","title":"Enhance UI with better loading indicators and error states","description":"The application should have a consistent and premium feel for loading and error states. Currently, it uses simple text. We should implement skeleton screens or more polished animations.","type":"task","status":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-14T22:49:05.942464799Z","updated":"2026-02-14T22:49:05.942464799Z"} +{"id":"NK-89za3s","title":"v3ui: neko button fixed position","description":"the neko cat button keeps moving, I kind of just want it to stay in the same position at the top left like a logo. move the search box down a bit so it won't overlap","type":"task","status":"open","priority":2,"labels":null,"assignee":"","created":"2026-02-16T15:38:25.158968209Z","updated":"2026-02-16T15:38:25.158968209Z"} {"id":"NK-8d1uzw","title":"Clean up unused font CSS variables","description":"The font CSS variables might have duplicates or unused entries after the fix. Audit them.","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-15T02:24:06.398437323Z","updated":"2026-02-15T17:28:58.42125577Z"} {"id":"NK-8hu7z1","title":"scrape full text button","description":"add this feature back in to the v2 ui and verify it is still working (not sure if we have any tests)","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:27:49.815938946Z","updated":"2026-02-14T17:58:19.083695149Z"} {"id":"NK-8rhpp3","title":"v2 frontend: when selected, don't change style of feed items","description":"Just leave them the same when j/k \"selects\" an item. No blue side thing, no change in background, it's distracting. Just scroll it to the right place.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T00:39:50.01934312Z","updated":"2026-02-14T01:02:54.204739756Z"} @@ -87,7 +88,9 @@ {"id":"NK-jyw7lb","title":"add neko cat back to hide left navivation","description":"Change the \"neko reader\" to the cat emoji like in the legacy and have it toggle visibility of the left nav","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T01:01:08.430978911Z","updated":"2026-02-14T01:21:11.002320114Z"} {"id":"NK-k04tet","title":"Fix Playwright E2E Tests","description":"The e2e tests in tests/e2e.spec.ts are failing when run with vitest. They should be run with playwright test runner, or configured correctly. Currently excluded from vitest.","type":"bug","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-13T21:50:14.152486771Z","updated":"2026-02-14T02:43:56.02734439Z"} {"id":"NK-k1p9ij","title":"Evaluate E2E test harness","description":"Evaluate the current E2E test harness (Cleaning up test environment...\nCleanup complete.\nBuilding backend...\nCreating data directory...\nStarting mock feed server on port 9090...\nMock Server PID: 166494\nMock server is up.\nStarting backend on port 4994...\nBackend PID: 166520\nWaiting for backend to start...\nWaiting...\nBackend is up!\nRunning E2E tests...\n\n\u003e frontend@0.0.0 test:e2e\n\u003e playwright test\n\n\nRunning 13 tests using 1 worker\n\n\u001b[1A\u001b[2K[1/13] [chromium] › tests/auth.spec.ts:12:5 › Authentication - No Password Required › should allow direct access to dashboard without login\n\u001b[1A\u001b[2K[2/13] [chromium] › tests/auth.spec.ts:25:5 › Authentication - No Password Required › should allow login with empty password\n\u001b[1A\u001b[2K[3/13] [chromium] › tests/auth.spec.ts:38:5 › Authentication - No Password Required › should report authenticated status via API when no password\n\u001b[1A\u001b[2K[4/13] [chromium] › tests/auth.spec.ts:53:10 › Authentication - Password Required › should redirect to login when accessing protected routes\n\u001b[1A\u001b[2K[5/13] [chromium] › tests/auth.spec.ts:64:10 › Authentication - Password Required › should reject incorrect password\n\u001b[1A\u001b[2K[6/13] [chromium] › tests/auth.spec.ts:79:10 › Authentication - Password Required › should accept correct password and redirect to dashboard\n\u001b[1A\u001b[2K[7/13] [chromium] › tests/auth.spec.ts:93:10 › Authentication - Password Required › should persist authentication across page reloads\n\u001b[1A\u001b[2K[8/13] [chromium] › tests/auth.spec.ts:109:10 › Authentication - Password Required › should logout and redirect to login page\n\u001b[1A\u001b[2K[9/13] [chromium] › tests/auth.spec.ts:128:10 › Authentication - Password Required › should report unauthenticated status via API\n\u001b[1A\u001b[2K[10/13] [chromium] › tests/auth.spec.ts:142:5 › Authentication - Complete Flow › should handle complete user flow without password\n\u001b[1A\u001b[2K[11/13] [chromium] › tests/crawl.spec.ts:4:5 › Crawl Integration › should add a feed and see items after crawl\n\u001b[1A\u001b[2K[12/13] [chromium] › tests/e2e.spec.ts:4:3 › Neko Reader E2E › should allow login, viewing feeds, and logout\n\u001b[1A\u001b[2K[chromium] › tests/e2e.spec.ts:4:3 › Neko Reader E2E › should allow login, viewing feeds, and logout\nStep 5: Navigate to Home\n\n\u001b[1A\u001b[2KStep 6: Logout\n\n\u001b[1A\u001b[2K[13/13] [chromium] › tests/font.spec.ts:4:5 › Font Theme Settings › should change font family when theme starts\n\u001b[1A\u001b[2K 6 skipped\n 7 passed (5.2s)\n\nTo open last HTML report run:\n\u001b[36m\u001b[39m\n\u001b[36m npx playwright show-report\u001b[39m\n\u001b[36m\u001b[39m\nTests passed!\nCleaning up...\nCleaning up test environment...\nCleanup complete.) which is brittle and resource intensive. Consider alternative approaches or refactoring.","type":"task","status":"closed","priority":4,"labels":null,"assignee":"","created":"2026-02-15T16:23:52.252436663Z","updated":"2026-02-15T19:14:31.772890559Z"} +{"id":"NK-k2fh32","title":"scroll mark as read broken in V3 UI","description":"j/k properly select and mark an item as read, but scrolling it off the screen on desktop and mobile don't seem to mark it as read.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-16T15:32:42.579032595Z","updated":"2026-02-16T15:37:20.993241382Z"} {"id":"NK-k4y597","title":"[feature] light/dark/black toggle","description":"Add in a simple [light | dark | black] theme toggler like in the legacy version.","type":"feature","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T00:45:55.312953906Z","updated":"2026-02-14T01:29:20.073659889Z"} +{"id":"NK-kdn1m2","title":"Optimize observer creation in renderItems","description":"We currently recreate IntersectionObservers every time renderItems is called. We should investigate if this impacts performance and potentially reuse the observer or optimize the logic.","type":"task","status":"open","priority":4,"labels":null,"assignee":"","created":"2026-02-16T15:37:41.915993277Z","updated":"2026-02-16T15:37:41.915993277Z"} {"id":"NK-kqt9oc","title":"docker support","description":"add support so people can self-host this in docker and (maybe) test it yourself. maybe keep it in a docker directory with separate docs etc.","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T20:19:10.70328135Z","updated":"2026-02-14T01:03:35.363466842Z"} {"id":"NK-kra45a","title":"enhance github ci/cq","description":"Make sure we have the right CI/CQ things in the github workflow. Can it test our docker compose for us too maybe","type":"feature","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-15T00:08:25.732991582Z","updated":"2026-02-15T01:04:54.350079542Z"} {"id":"NK-lo7l8g","title":"Vanilla JS (v3): Achieve 80% test coverage","description":"","type":"task","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-16T02:43:17.405569291Z","updated":"2026-02-16T03:36:14.221291336Z"} @@ -121,6 +124,8 @@ {"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":"closed","priority":3,"labels":null,"assignee":"","created":"2026-02-13T04:26:55.864725765Z","updated":"2026-02-13T04:26:55.864725765Z"} {"id":"NK-rn4nzp","title":"font themes","description":"in the v2 ui, let's offer a few different font stacks the user can switch through. primarily this should just change font-face, maybe size, but don't worry about colors or anything right now.\n\nthe current default (helvetica neue, palatino)\na fancy all serif stack\na no-nonsense modern san-serif stack\na terminal inspired fixed width stack","type":"feature","status":"closed","priority":2,"labels":null,"assignee":"","created":"2026-02-14T17:10:02.185477382Z","updated":"2026-02-14T18:12:30.253145852Z"} {"id":"NK-rohuiq","title":"titles changing on read state and hover","description":"Titles are changing on read state from blue to grey. They should just stay blue all the time.\n\nTitles are getting underlined on hover. They should have no underline regardless of hover state.","type":"bug","status":"closed","priority":0,"labels":null,"assignee":"","created":"2026-02-14T03:36:26.36373162Z","updated":"2026-02-14T03:37:50.73870586Z"} +{"id":"NK-s2g59a","title":"Add delay to scroll mark-as-read","description":"Currently items are marked as read immediately when 50% visible. This might be annoying if users are scrolling fast. We should add a short delay (e.g. 1s) to ensure the user actually viewed it.","type":"feature","status":"open","priority":3,"labels":null,"assignee":"","created":"2026-02-16T15:37:40.617999342Z","updated":"2026-02-16T15:37:40.617999342Z"} +{"id":"NK-s8nytj","title":"v3: close settings","description":"when settings page is shown, clicking settings again should close it and go back to the \"unread items\" state\n\nsimilarly, clicking \"unread\" or \"all\" or \"starred\" should close settings and take you to those.","type":"bug","status":"open","priority":1,"labels":null,"assignee":"","created":"2026-02-16T15:40:59.195773828Z","updated":"2026-02-16T15:40:59.195773828Z"} {"id":"NK-sdxq5p","title":"update documentation based on all the recent changes","description":"update README.md based on what's been changed given all the CLs and tickets recently","type":"task","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-15T00:07:09.08704631Z","updated":"2026-02-15T00:24:50.622207852Z"} {"id":"NK-shpyxh","title":"add search to new ui","description":"","type":"epic","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-13T19:29:44.251257089Z","updated":"2026-02-14T01:02:58.547025683Z"} {"id":"NK-sk6pym","title":"fix docker-compose","description":"bug when trying to build in docker -- we may want to add an automated test for this later (though it may be hard since we're building in a vm to nest these)\n\n--\u003e 668e2e2c4b44\n[2/3] STEP 3/9: WORKDIR /app\n--\u003e b42c7c265b7f\n[2/3] STEP 4/9: COPY go.mod go.sum ./\n--\u003e 093e4d9b623e\n[2/3] STEP 5/9: RUN go mod download\n--\u003e 208c8aaac5eb\n[2/3] STEP 6/9: COPY . .\n--\u003e 7c44260c3ac0\n[2/3] STEP 7/9: COPY --from=frontend-builder /app/frontend/dist ./frontend/dist\n--\u003e 09749e6660e1\n[2/3] STEP 8/9: RUN rice -i ./web embed-go\n2026/02/14 17:34:13 no calls to rice.FindBox() found\n--\u003e cdc88c64da36\n[2/3] STEP 9/9: RUN go build -o neko .\nno Go files in /app\nError: building at STEP \"RUN go build -o neko .\": while running runtime: exit status 1","type":"bug","status":"closed","priority":1,"labels":null,"assignee":"","created":"2026-02-14T17:38:05.696339994Z","updated":"2026-02-14T18:06:42.659012133Z"} @@ -177,12 +182,14 @@ {"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-dm35o6g","from_ticket_id":"NK-d4c8jv","to_ticket_id":"NK-chns2b","type":"created_from","created":"2026-02-14T04:46:32.151239137Z"} {"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-dmdluco","from_ticket_id":"NK-s2g59a","to_ticket_id":"NK-k2fh32","type":"created_from","created":"2026-02-16T15:37:40.651054188Z"} {"id":"NK-dmmxnj3","from_ticket_id":"NK-lrihov","to_ticket_id":"NK-ak4om3","type":"created_from","created":"2026-02-15T16:41:08.42415823Z"} {"id":"NK-dmow9sy","from_ticket_id":"NK-iw9l7h","to_ticket_id":"NK-uywybr","type":"created_from","created":"2026-02-14T01:04:11.599126072Z"} {"id":"NK-dnspb2r","from_ticket_id":"NK-6o87rr","to_ticket_id":"NK-d4c8jv","type":"created_from","created":"2026-02-14T04:47:40.652696057Z"} {"id":"NK-dnw8qnj","from_ticket_id":"NK-qwef98","to_ticket_id":"NK-mwf9q2","type":"created_from","created":"2026-02-13T18:05:18.469080925Z"} {"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-ds03bw7","from_ticket_id":"NK-0oti10","to_ticket_id":"NK-pwogze","type":"created_from","created":"2026-02-14T20:44:37.268882219Z"} +{"id":"NK-dspotwc","from_ticket_id":"NK-kdn1m2","to_ticket_id":"NK-k2fh32","type":"created_from","created":"2026-02-16T15:37:41.946633536Z"} {"id":"NK-dumpdcp","from_ticket_id":"NK-59kbij","to_ticket_id":"NK-ek0cox","type":"created_from","created":"2026-02-13T14:58:18.351925575Z"} {"id":"NK-dvxunsp","from_ticket_id":"NK-rhelrq","to_ticket_id":"NK-pwogze","type":"created_from","created":"2026-02-14T20:44:31.085216701Z"} {"id":"NK-dw8luqe","from_ticket_id":"NK-ek0cox","to_ticket_id":"NK-3om7x2","type":"created_from","created":"2026-02-13T14:55:14.832352853Z"} diff --git a/frontend-vanilla/src/main.test.ts b/frontend-vanilla/src/main.test.ts index 7cae34b..a8b6969 100644 --- a/frontend-vanilla/src/main.test.ts +++ b/frontend-vanilla/src/main.test.ts @@ -22,7 +22,11 @@ vi.mock('./api', () => ({ })); // Mock IntersectionObserver as a constructor +let observerCallback: IntersectionObserverCallback; class MockIntersectionObserver { + constructor(callback: IntersectionObserverCallback) { + observerCallback = callback; + } observe = vi.fn(); unobserve = vi.fn(); disconnect = vi.fn(); @@ -254,4 +258,39 @@ describe('main application logic', () => { toggleBtn.click(); expect(store.sidebarVisible).toBe(!initialVisible); }); + + it('should mark item as read when scrolled into view', () => { + const mockItem = { + _id: 123, + title: 'Scroll Test Item', + read: false, + url: 'http://example.com', + publish_date: '2023-01-01' + } as any; + + store.setItems([mockItem]); + renderLayout(); + renderItems(); + + const itemEl = document.querySelector(`.feed-item[data-id="123"]`); + expect(itemEl).not.toBeNull(); + + vi.mocked(apiFetch).mockResolvedValue({ ok: true } as Response); + + // Simulate intersection + const entry = { + target: itemEl, + isIntersecting: true + } as IntersectionObserverEntry; + + // This relies on the LAST created observer's callback being captured. + expect(observerCallback).toBeDefined(); + // @ts-ignore + observerCallback([entry], {} as IntersectionObserver); + + expect(apiFetch).toHaveBeenCalledWith(expect.stringContaining('/api/item/123'), expect.objectContaining({ + method: 'PUT', + body: expect.stringContaining('"read":true') + })); + }); }); diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts index 93bee63..b167a18 100644 --- a/frontend-vanilla/src/main.ts +++ b/frontend-vanilla/src/main.ts @@ -18,6 +18,7 @@ let activeItemId: number | null = null; // Cache elements (initialized in renderLayout) let appEl: HTMLDivElement | null = null; +let itemObserver: IntersectionObserver | null = null; // Initial Layout (v2-style 2-pane) export function renderLayout() { @@ -216,6 +217,11 @@ export function renderFilters() { export function renderItems() { const { items, loading } = store; + + if (itemObserver) { + itemObserver.disconnect(); + itemObserver = null; + } const contentArea = document.getElementById('content-area'); if (!contentArea || router.getCurrentRoute().path === '/settings') return; @@ -246,6 +252,25 @@ export function renderItems() { }, { threshold: 0.1 }); observer.observe(sentinel); } + + // Setup item observer for marking read + itemObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const target = entry.target as HTMLElement; + const id = parseInt(target.getAttribute('data-id') || '0'); + if (id) { + const item = store.items.find(i => i._id === id); + if (item && !item.read) { + updateItem(id, { read: true }); + itemObserver?.unobserve(target); + } + } + } + }); + }, { threshold: 0.5 }); + + contentArea.querySelectorAll('.feed-item').forEach(el => itemObserver!.observe(el)); } export function renderSettings() { @@ -623,7 +648,6 @@ window.app = { }; // Start -// Start export async function init() { const authRes = await apiFetch('/api/auth'); if (!authRes || authRes.status === 401) { |
