khannurien
c8bf0d583c
v3: the tests learn to expect trouble, and seven bugs they now catch are fixed — a review of the two test-suite commits found them written mostly for the happy path; each fix below comes with the test that fails without it. Infinite scroll: a later page that failed turned loadingMore back off, which made a new IntersectionObserver, which reports a sentinel still in view at once — so an offline feed asked again as fast as the request could fail, on Search, Index, the followed feeds, user dumps and user playlists alike. useInfiniteScroll now waits 1s, 2s, 4s… up to 30s while the sentinel stays put, counted from the last load so re-renders don't restart it, and asks at once again when the sentinel leaves the view or is pushed down by new items; the fake observer in src/test can report an element in view on observe(), as a browser's does. Playlist covers: the row was updated and playlist_updated sent before the file was written, so a client fetching image?v=<new> in between cached the old cover (or a 404) under the new URL for an hour, and a failed write left image_mime set with no file behind it. The file now goes in first, written beside and renamed over, and only then does the row change. Playlist order: the server wanted every member named, but GET hides private dumps the requester can't see, so a playlist holding someone else's dump that went private — or one a moderator reorders around the owner's private dumps — could never be reordered. The requester now orders what they can see and hidden members keep their slots; the visibility rule lives in api/lib/playlist-members.ts, shared by GET, reorder and the socket. playlist_dumps_updated also sent every member's id, private ones included, to a public playlist's whole audience; each viewer now gets the ids they can see, plus a dumpCount of all members so playlist cards keep the server's count. File dumps: replacing the file and changing visibility took two requests, so a failed second one left the new file live under the old visibility; PUT /api/dumps/:id/file takes isPrivate and applies both in one UPDATE, and it now broadcasts like any edit (it sent nothing, so other viewers never saw a new title). Notifications: one that arrived while the list was loading was dropped, then marked read by the read-all after the load, so it never showed; it's kept and merged in, once. Ranges: bytes=0-0,5-6 got a 416; several ranges, another unit or an unparseable header are ignored now and the whole file sent with a 200, as RFC 9110 allows — the old test asserted the 416.
...
Build and Publish Docker Image / build-and-push (push) Successful in 49s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-09-27 15:13:57 +00:00
khannurien
25601edaf8
v3: the backend gets a test suite, and the bugs it turned up are fixed — deno task test runs 186 tests over api/ in about 18s, against a throwaway database and uploads directory (GERBEUR_DB_PATH and GERBEUR_UPLOADS_DIR now override the defaults; api/test/setup.ts sets them before any app module loads), with requests sent through the whole middleware stack in-process via app.handle, outbound fetches and DNS stubbed, and the socket tests on a real Oak listener since an upgraded request has no response for handle() to return. It covers auth (token purposes, revocation after a password change, live roles, login rate limiting, invites claimed once even under parallel registrations, password resets), dumps end to end (create, privacy, by-url matching on the canonical key, backlinks, categories, edits, file replacement, thumbnails, refresh, delete), file serving headers and ranges, attachments, avatars, comments and likes, playlists, follows and feeds, notifications and mentions, categories, search, chat cursors and read markers, link previews and the SSRF guard on every redirect hop, the Bandcamp and YouTube resolvers, migrations (a fresh schema is left untouched; the data rewrites in 0004/0005 and 0010–0013 do what they say), CORS, OG tags, static caching, and the WebSocket protocol. Fixed along the way: an unknown category id on a new dump returned 400 but kept the dump row, and for a file dump deleted the file while keeping the row; an edit or file replacement with one was half-applied before the 400 — categories are now checked before anything is written. A suffix range (bytes=-3) was read as an end offset and got a 416 instead of the last three bytes. Reordering a playlist accepted a list naming a member twice. verifyPassword threw on a stored hash without a salt instead of answering false. And a request with no Accept-Encoding, which RFC 9110 lets a server treat as accepting anything, was sent the brotli sibling; curl and bots now get the plain file.
...
Build and Publish Docker Image / build-and-push (push) Successful in 46s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-09-27 12:31:05 +00:00
khannurien
bfce3186f8
v3: the frontend stops trusting what it last saw — a review of src/ turned up counts that fell to zero, stale copies winning over fresh ones, sessions that tabs trampled on each other, and a player queue that outlived its listener, and this closes them. Voting or liking something with no live count yet started from 0, so a dump with five votes read 1 on the click and, with the socket down, 0 for good once the undo fired; the buttons now pass the count they show, which the optimistic update and its undo start from. After a reconnect the chat re-read its history as the truth, but the merge let the live copy of a message win, so an edit made while the socket was down never showed; the resync now replaces live copies with what it fetched. Every dump_updated added the dump to the live list if it wasn't there, so retitling a months-old dump put it at the top of New, Hot and Journal for everyone, while one that had arrived live kept its old title; an edit now refreshes a live entry in place and adds nothing, and the server announces a dump going public as a dump_created, which is the one case that is an arrival. The new-dump modal let a slow preview for the previous link land on the new one, title suggestion included; changing the link now aborts both lookups. Markdown found mentions by rewriting the source text with a rule that, unlike the server's, didn't skip an @ after a word character, so an email address linked its domain, a URL like mastodon.social/@bob was split, and @ in code came out as literal link syntax; mentions are now found by a remark step over text nodes only, by the server's rule. Changing the password in one tab stored fresh tokens that any other open tab wiped the moment its old token drew a 401; the session now lives behind utils/authStorage.ts, tabs follow each other through storage events (so a logout also signs out every tab), a 401 only forgets the stored session if it still holds that token, and in-place session updates go through a new updateSession() rather than re-reading localStorage. The persisted player queue carries media-token URLs yet survived logout for the next person to sign in; it is now stamped with its owner, not restored for anyone else, cleared on logout or account switch (a guest's queue survives signing in), and older unstamped queues holding a file are dropped. Playlist checkboxes in the new-dump and add-to-playlist modals flipped whether or not the server agreed and now wait for it. CommentThread and the Dump page's loads use authFetch like the rest, so a 401 there signs out consistently; isTokenExpired has one home; and the useScrollLock imports carry their extension so deno check passes.
...
Build and Publish Docker Image / build-and-push (push) Successful in 42s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-09-24 20:24:38 +00:00
khannurien
2ad8ae73d5
v3: the API stops trusting what it's handed — a review of api/ turned up a server anyone could crash, uploads that ran script on the app's origin, and a server that would fetch its own network on request, and this closes them along with the smaller holes around them. A WebSocket message that wasn't an object (a bare null will do) threw inside the listener, which Deno treats as uncaught and exits on; messages are now shape-checked and the handler can't throw out. Uploaded files kept the MIME type the uploader's browser claimed, so an .html or .svg dump opened from /api/files ran with the session token in reach; only images, audio, video, PDF and plain text are served inline now, the rest download, and every file carries nosniff and a sandboxing CSP (PDFs aside, which Chrome won't render sandboxed). The image proxy passed image/svg+xml straight through the same way and now takes raster formats only. Outbound fetches for previews, the proxy and Bandcamp checked the hostname against a partial list — no 169.254.169.254, no 0.0.0.0, an IPv6 check that never matched a bracketed hostname, nothing for names that resolve inward, and redirects followed blindly; every hop now resolves and is refused if any address is private or reserved, redirects are walked by hand, the curl fallback is pinned to the checked IP, and /api/preview needs a login. PATCH /users/me accepted a new password without the current one and now takes only the description and email; a wrong password at login answered 500 because the handler caught its own 401, and now says 401. Changing or resetting a password now ends every older session through users.tokens_valid_after (migration 0015) and an iat on every token, with change-password handing back fresh tokens the modal keeps, and all token checks go through one authenticate() that also refuses deleted users. Invites and reset links are claimed atomically before the async hash rather than marked after, so one invite no longer opens several accounts, and an email address belongs to one account. File and thumbnail URLs carry a media-only token rather than the session token, so a URL in a log exposes files and not the account; older sessions fetch one on load. Private things stop leaking into broadcasts: private playlist updates go to their owner, votes and likes on someone else's private dump 404 and their counts go to the owner alone, and a private dump can't be slipped into a playlist. Smaller: selfProvider only claims this site's own host, so github.com/users/bob stops rendering our bob; the chat read marker can't be parked in the future on an empty chat; dump mutations use the resolved id so a slug works; multipart dumps get the title and comment limits; logins and reset requests are rate-limited through a shared token bucket; the password compare is constant-time; ffmpeg reads only local files in known video containers.
...
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-09-24 20:07:33 +00:00
khannurien
fb8364e24d
v3: reworked posting — three panels, drafts, duplicate detection, upload progress
...
Build and Publish Docker Image / build-and-push (push) Successful in 42s
Posting a dump was a single form where the important choices were the easiest
to miss. It is now three panels: link or file, why & where, playlists.
Composition:
- No more URL/File toggle. An empty panel offers both ways in at once and the
kind follows what you actually did; a file dropped anywhere in the modal is
accepted, not just on the zone.
- Categories and visibility get their own panel instead of a disclosure that
read as optional, and the primary button stays "Next" until they've been
seen. Visibility carries a real label now.
- The draft (link, title, why, categories, visibility) is mirrored to
localStorage on every change and restored on reopen, so Escape or a stray
backdrop click costs nothing. Only an attached file can't be restored, so
that is the one case that asks before closing.
- URL dumps can carry a poster-supplied title instead of being stuck with
whatever the page scraped, editable right under the preview.
- Multipart uploads go through XHR so there is a real progress bar and a
percentage on the button, rather than 50 MB of silence.
Duplicates:
- New dumps.url_canonical column (+ index, backfilled by 0013) holding a lossy
key that ignores scheme, www., trailing slashes, tracking parameters and
YouTube share shapes. GET /api/dumps/by-url reads it, and the create form
warns "already dumped by X" while it fetches the preview. Never blocking.
Fixes:
- /api/preview now reports whether the page was actually reached: a failed
fetch still yields a hostname-only stub, so a dead link and a page without
metadata used to render identically.
- The Web Share Target never worked. The manifest posts to "/", but the index
redirect dropped the query string, so every Android share landed on the feed
with nothing pre-filled.
- File dumps no longer take the extension into their title.
- The link field no longer autofocuses on touch, where it raised a keyboard
over the modal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_01TiAPtJZeCYYk8rKehtLUQU
2026-09-08 14:40:37 +00:00
khannurien
1cb904d2cf
v3: generated placeholder thumbnails for pages with no preview image, and a fix for hotlink-protected ones
...
Build and Publish Docker Image / build-and-push (push) Successful in 47s
Thumbnails were failing in two different ways that both ended as an empty box.
Cloudflare hotlink protection answers a cross-site Referer with 403, so images
we had extracted correctly (dles.aukspot.com's og:image among them) never
rendered — every onError handler set display:none and swallowed it. The new
Thumbnail component loads with referrerPolicy="no-referrer", retries once
through /api/proxy-image for hosts that reject an empty referrer too, and only
then falls back to a placeholder.
Separately, the extraction cascade ended at the page's icon and then a guessed
/favicon.ico, so thumbnailUrl was almost never empty — just a 16x16 icon
cover-cropped into a 128x72 box. It now stops at real artwork, with faviconUrl
and accentColor (theme-color / msapplication-TileColor / mask-icon) as their own
fields. An absent thumbnailUrl finally means "no artwork", which is what makes
the placeholder possible: the site's own color mixed into the theme surface,
with its favicon centered on it, or its initial. Contrast holds for any
third-party color by construction rather than by luminance math, so nyt only
has to set --thumb-tint-strength to 0% to stay monochrome and geocities only
has to raise it. Missing accents fall back to a stable hostname-derived hue, so
rows saved before this get a tint with no backfill.
Migration 0011 reclassifies favicon-shaped thumbnailUrls on existing dumps.
The journal mosaic keeps its pull-quote and text fallbacks — the placeholder
appears there only to repair a broken image.
Also fixed: refresh silently overwrote good metadata with a failure stub, the
refresh button swallowed every error, refresh never broadcast the update,
extractBestIcon ranked SVG icons below 16x16 PNGs, and shared links with no
artwork carried no og:image at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-11 19:43:55 +00:00
khannurien
fae25f3e6c
v3: added site-wide categories, added admin category management, various visual fixes
Build and Publish Docker Image / build-and-push (push) Successful in 2m52s
2026-06-28 16:23:44 +00:00
khannurien
a1b71ad0c8
v3: consistent tabs url across app, maxy small fixes
Build and Publish Docker Image / build-and-push (push) Successful in 42s
2026-06-22 17:25:40 +00:00
khannurien
4687a6b515
v3: add backlinks between dumps (via either dump url or dump target url mentioned through dump description and dump comments)
Build and Publish Docker Image / build-and-push (push) Successful in 43s
2026-06-21 20:30:03 +00:00
khannurien
cf988ae608
v3: allow users to edit dump thumbnail, fixed some linter errors in the frontend
Build and Publish Docker Image / build-and-push (push) Successful in 41s
2026-06-21 15:41:40 +00:00
khannurien
888aae45cf
v3: allow users to edit dump title
Build and Publish Docker Image / build-and-push (push) Successful in 2m54s
2026-06-21 12:57:46 +00:00
khannurien
378b3ffa46
v3: added onboarding email on account creation
2026-03-30 14:55:30 +00:00
khannurien
cbb3505139
v3: search engine, responsive header with compact user menu
2026-03-29 11:56:31 +00:00
khannurien
f0f6472db6
v3: added attachments to resources, allow users to paste images into TextEditor, strengthened WS reliability
2026-03-27 08:53:32 +00:00
khannurien
0cb5a798c7
v3: added journal view to the index, code organization pass
2026-03-25 21:07:17 +00:00
khannurien
c293f3e706
v3: code quality pass
2026-03-24 18:47:05 +00:00
khannurien
b96879a556
v3: fixes to database schema and user registration
2026-03-23 15:55:45 +00:00
khannurien
fbbbb43258
v3: code quality pass, various bug fixes
2026-03-23 07:47:49 +00:00
khannurien
c5051e3485
v3: added emoji picker, various bug and layout fixes
2026-03-22 20:24:29 +00:00
khannurien
a104113e05
v3: linter and formatter pass
2026-03-22 16:08:12 +00:00
khannurien
34e908d1bc
v3: added content slugs, fixed real-time updates in client, added @mentions across the app, added new file selector and drop zone
2026-03-22 16:06:26 +00:00
khannurien
608c6bc6a8
v3: follows, notifications, invite-only registration, unread markers
2026-03-21 18:42:47 +00:00
khannurien
7c098e7c4c
v2: global player, infinite scroll, image picker, threaded comments
2026-03-21 13:55:22 +00:00
khannurien
867e64cb5b
v1 review pass: fixed some minor bugs
2026-03-16 11:08:39 +00:00
khannurien
e88fed4e98
vibe coded v1
2026-03-16 07:34:49 +00:00
khannurien
6207a7549f
initial commit, boilerplate stuff
2026-03-15 17:15:46 +00:00