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
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
73b80af08e
v3: the chat stops dropping things and holds up to long messages — a message sent while the socket was down vanished with the draft already cleared, and a rejected one fared no better since the client ignored any error that wasn't about a vote or a like. Sends now carry a nonce and sit dimmed in the list until their echo retires them; the server's refusal, an offline socket or ten seconds of silence turns one into a failed row with retry, edit and discard, and a failed row that turns up in the history after a reconnect is retired as having landed after all. Failed edits and deletes show in a bar above the composer. The live stream only goes to signed-in sockets, matching the history behind auth; sends, edits and deletes share a per-user token bucket (typing and read markers get their own, and are just dropped past it); and moderation re-reads the role from the database on every check rather than trusting the one a week-old socket captured. Unread now lives on the server as users.chat_read_at (migration 0014, existing users start caught up), so the count survives a reload and every tab and device clears together; opening onto unread history lands on a "new" divider rather than the bottom, and marks it read only once you've scrolled down to it. After a reconnect the chat re-reads everything from its oldest loaded message and takes that as the truth, edits and deletions included, or reloads the latest page if too much happened. History pages on (created_at, id) so a shared millisecond can't hide a message at a boundary. In the modal: a run of messages from one author shares a header with the time in the gutter on hover, mentions and replies to you get an accent edge, a pill counts what arrived while you were scrolled up, the draft survives closing and reloading, relative times tick, reply previews are flattened from markdown, owners can delete their own messages, ↑ in an empty composer edits your last one, and there's a proper loading and retry state instead of a flash of "No messages yet". Long messages: the composer and edit box grow only so far and then scroll, instead of squeezing the list away; the counter shows only near the limit and sits above the box, since .comment-reply-textarea's padding shorthand had quietly cancelled the room kept for it inside; TextEditor sizes itself before paint, so opening an edit animates to the right height instead of dipping and snapping; and the list follows the bottom through any change in its content's height while pinned there, so ending an edit returns you to the end. A mention notification opens the chat over the page you're on rather than the home page.
...
Build and Publish Docker Image / build-and-push (push) Successful in 43s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com >
2026-09-22 20:36:49 +00:00
khannurien
d70761ecae
v3: youtube playlists get a tracklist too — the embed keeps playing the list, so the rows come from two halves: /api/youtube/playlist scrapes titles and lengths off the playlist page (current lockup rows and the older renderer, cached 6h), while the IFrame API attached to the iframe reports the position and takes row clicks and ⏮/⏭; the header names the playing video with the dump's title below it, like a bandcamp track; either half can fail — no scrape leaves the bare embed, no API leaves the rows as a table of contents — and the pause an API-enabled embed gives itself when the tab goes to the background is undone within a short window, so a later pause still stands
...
Build and Publish Docker Image / build-and-push (push) Successful in 42s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-09-19 21:51:10 +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
2606ef4ccc
v3: native bandcamp playback and a queue in the global player
...
Build and Publish Docker Image / build-and-push (push) Successful in 43s
The player now holds a queue instead of a single item, so albums play
through and single tracks share the same code path. Adds a "stream" item
kind for expiring, remotely-hosted sources: Bandcamp signs its mp3 URLs
for ~24h, so an item carries the page it came from and re-resolves itself
on error or on a stale restored session, falling back to the iframe embed
if that fails too.
Gated behind GERBEUR_BANDCAMP_PLAYER (default "embed"), injected into a
meta tag the same way as site-name/site-emoji.
Also: player header gains artwork and a subtitle, volume persists across
queue advances and reloads, and cross-origin streams get a plain seek bar
rather than a waveform that can never decode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
Claude-Session: https://claude.ai/code/session_01SbtjNsT5wvuABnfqhegZEJ
2026-08-22 12:14:28 +00:00
khannurien
a9b94c0bfb
v3: added an "in collections" section to dump pages, listing the collections a dump belongs to
...
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com >
2026-08-08 11:41:36 +00:00
7773a8a501
v3: fixed roles propagation bug, fixed inconsistent file drop zone, fixed chat attachments wrongfully pruned, some visual tweaks
Build and Publish Docker Image / build-and-push (push) Successful in 41s
2026-06-30 13:41:36 +02:00
d2d086831e
v3: chatbox improvements (typing indicators, answer to previous messages, backlog days separators, scroll to last message position, visual tweaks)
Build and Publish Docker Image / build-and-push (push) Successful in 41s
2026-06-30 12:27:03 +02:00
khannurien
6c4f410d9b
v3: added a chatbox
Build and Publish Docker Image / build-and-push (push) Has been cancelled
2026-06-29 20:25:10 +00:00
khannurien
aab5b5bfd5
v3: various improvements and fixes to real-time updates
Build and Publish Docker Image / build-and-push (push) Successful in 40s
2026-06-29 07:52:56 +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
551e29c564
v3: added playlist moderation permission
Build and Publish Docker Image / build-and-push (push) Successful in 43s
2026-06-28 06:37:23 +00:00
khannurien
810faaf21a
v3: added a roles/permissions system, added user management tab on user profile
Build and Publish Docker Image / build-and-push (push) Successful in 42s
2026-06-28 06:21:23 +00:00
khannurien
b567e390d9
v3: fixed journal view not scrolling, fixed some user profile layout bugs, increase thumbnail size on the index
Build and Publish Docker Image / build-and-push (push) Successful in 43s
2026-06-28 05:27:49 +00:00
khannurien
c29f45fbc8
v3: added index default tab selection setting, fixed notification pages not loading, reduced invite token length, global player state survives page reloads, many visual fixes
Build and Publish Docker Image / build-and-push (push) Successful in 44s
2026-06-27 19:44:35 +00:00
khannurien
76de798faf
v3: fixed authorization bug with stale tokens, fixed private dump access bug, various small fixes across the app
Build and Publish Docker Image / build-and-push (push) Failing after 20s
2026-06-27 10:40:07 +00:00
khannurien
73e0114bf7
v3: code quality and visual consistency pass (refactored all forms across the app), fixed filename-related upload bug
Build and Publish Docker Image / build-and-push (push) Successful in 47s
2026-06-26 22:20:07 +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
7afb5d3f07
v3: added comment likes, added votes/likes list view, added db migrations mechanism, updated project dependencies
Build and Publish Docker Image / build-and-push (push) Successful in 42s
2026-06-21 14:17:56 +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
34933a3d4f
v3: fixed rich content extraction heuristics
Build and Publish Docker Image / build-and-push (push) Successful in 3m15s
2026-04-11 13:13:43 +00:00
khannurien
1321e374bf
v3: performance pass, bundle size pass, i18n pass, docker pass
2026-04-08 13:19:39 +00:00
khannurien
20b9bfe7b4
v3: added password change/reset feature
2026-04-06 16:30:00 +00:00
khannurien
3b6980a8fc
v3: added multiple stylesheets, improved user profiles
2026-04-06 15:36:04 +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
ca70bdc14b
v3: added opengraph support to the app, wrote README instructions incl. a Docker image
2026-03-26 19:55:48 +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
d94a319d96
v3: added user profile description
2026-03-22 21:07:17 +00:00
khannurien
c5051e3485
v3: added emoji picker, various bug and layout fixes
2026-03-22 20:24:29 +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
be426eb150
v1 feature: added playlists
2026-03-16 16:52:53 +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