khannurien
|
d62e4a9883
|
v3: the backend is documented for deno doc, one way in every file, and the docs build and publish themselves — comments were in three shapes: file headers as // lines, as an untagged /** */ (which deno doc drops, only an @module block counts) or missing; 43 exports described by a // deno doc can't see, two JSDoc blocks sitting on the wrong declaration and one duplicated; endpoints commented in 5 of 18 route files, three ways, on router.get() calls no doc tool reads. The rule, applied to every non-test module of api/, migrations included: each opens with a /** … @module */ block; every export has JSDoc, and so does every field of an exported interface; a comment describing a declaration is /** */, // only notes code; functions open with a verb, everything else with a noun phrase; parameters are named in the prose, {@link} only for symbols in scope; whatever throws to its caller says so with @throws (for an APIException, the statuses and when); a router's JSDoc lists its endpoints, method and path, who may call it, what it does. deno doc --lint goes from 528 errors to none in api/ (shared/ keeps its 385; it is next). The lint also wanted explicit types on config.ts's settings and a few return types, JsonLdResult exported, and the routers typed Router. deno task doc builds the HTML reference into docs/ from every module of api/ and shared/ but the tests, deno task doc:lint lints it; the index page is the module doc of a root mod.ts (deno.json's exports), which introduces the backend, shared/ and the frontend to come and links every module, and mod.test.ts fails when one is missing. The docs workflow builds it on every push to main that touches them and publishes it to the gh-pages branch, like publish-assistant's. Two defects found along the way are fixed: a private playlist's cover was served to anyone who had its id or slug, and now needs its owner's media token, as private dump files do (playlistImageUrl adds it for private playlists only, so public cover URLs stay the same for everyone); og.ts and static.ts each filled index.html's placeholders, and now share lib/index-html.ts, so the OG middleware picks up a rebuilt index.html instead of keeping the first it read. The README states the documentation rule. 205 backend and 628 frontend tests pass.
Build and Publish Docker Image / build-and-push (push) Successful in 47s
Build and deploy docs / build-and-push (push) Successful in 26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 21:07:40 +00:00 |
|
khannurien
|
d392be7cb8
|
v3: api/ gets one layout rule and keeps to it, and whatever crosses the wire is defined once, in shared/ — api/model/interfaces.ts and src/model.ts declared the same 40 types by hand and had drifted: the error envelope, email required on one side and optional on the other, Invitee/InviteTreeEntry and LinkPreview/UrlPreviewResponse for one shape each. The validation limits, the role permissions, the chat length cap, the @mention pattern and the Bandcamp and YouTube player responses were copied too, behind "keep in sync" comments. shared/ now holds them once, one file per domain (api, category, chat, comment, dump, follow, mention, notification, permissions, player, playlist, search, user, validation, ws), and imports nothing from either half. The server sends Dates; the client reads Wire<T>, the same type with every Date an ISO string, and turns it back with src/utils/shared/deserialize.ts. APIErrorCode becomes a const object, since the frontend's erasableSyntaxOnly rejects an enum. The shared types showed two real gaps: the avatar upload and GET /api/users/me return the full self user and the frontend read them as public users, and a stored session may predate media tokens, so the client keeps it as its own Session type. The backend rule: each folder holds one kind of module, each file is named after its domain. routes/ parse and answer; services/ hold the logic and every SQL query (lib/auth.ts's token check moves to user-service); remote/ is the only place that fetches from other sites — the guarded fetch and the HTML scraping out of rich-content-service, public-address, the Bandcamp and YouTube resolvers, the link providers, the image proxy (fetch, checks and cache, out of routes/preview.ts) and the Twemoji glyphs (out of lib/site-icons.ts). model/ holds per table a row type, its guard and its mappers; validators/ the request guards; db/connection.ts the handle and transaction(), db/startup.ts the purges and default admin that db.ts ran on import, called at the same moment. Tests mirror the source one file per module, test/<layer>/<module>.test.ts, with shared/'s under shared/test/ (deno task test runs both); the test that covered the bandcamp and soundcloud providers together is two now, and the backend can() has its own, so 202 tests. deno lint src/ is clean too: window.* becomes globalThis.*, and test bodies and act() callbacks that never awaited lose their async. The README states the rule. No behaviour changes; the catalogues only move their source references.
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 20:19:48 +00:00 |
|
khannurien
|
de5abeceb2
|
v3: a performance pass over both halves — indexed queries, streamed files, cached previews, and feeds that stop re-rendering on every tick — a review turned up whole-file reads, unindexed scans and render storms; each fix comes with a test. Database: migration 0016 adds the indexes the hot queries lacked — dumps by created_at, (user_id, created_at) and slug, votes by (user_id, created_at), playlists by slug, users by lower(username) for @mentions, and the child side of every cascading foreign key (comment replies, a user's comments, follows both ways, chat messages, invites, reset tokens); the feeds, dump and playlist pages, upvotes and follower fan-out all plan as index searches now, with no temp-table sort. SQLite runs in WAL with synchronous=NORMAL and a busy timeout, and a transaction() helper wraps the multi-row writes (a dump and its categories, category sets, backlinks, attachment links, playlist reorders, follower notifications) so they commit once instead of once a row. Lists no longer run a COUNT(*) per page: they fetch limit + 1 rows and hasMore comes from the extra one; total stays only on search, the one place it's shown. Files: /api/files read the whole upload (up to 50 MB) for every range request a player makes while seeking; api/lib/serve-file.ts streams the requested bytes from disk, with an ETag and 304s, and serves avatars, attachments, covers and thumbnails too. File and thumbnail URLs carry the file's version (size and last change), so the current one is cached for a year and anything else revalidates — a same-size replacement used to stay stale, and a video's still was cached immutable forever under a URL that never changed. Feeds showed image dumps at full size: /api/thumbnails now makes a 960px preview with ffmpeg (GIFs and unreadable images go out as they are), one run per output however many cards ask at once, and ffmpeg is probed once per process instead of spawned per request. Uploads stream to disk; replacing a file writes beside it and renames over, a hard link keeping the old one for rollback instead of holding both in memory; generated stills and previews go when the file is replaced or the dump deleted, and a custom thumbnail with its dump. Outbound: fetchWithTimeout's deadline now covers the body — a server that answered at once and then dripped could hold a create open indefinitely — and pages are read to 4 MB at most; metadata is cached five minutes so the create form's preview and the post that follows share one fetch (refresh-metadata still goes to the page); proxied images sit in a 32 MB LRU. Broadcasts serialize once rather than per socket, and index.html is kept in memory until the file changes. Frontend: currentTime lived in PlayerContext, so every card re-rendered — and re-parsed its Markdown — four times a second while anything played; the playhead has its own PlayerTimeContext now. The socket context is split three ways (feed, chat via useWSChat, presence via useWSPresence), so typing, presence and chat messages leave the feeds alone. DumpCard, JournalCard and Markdown are memoized; card images use the server preview and load lazily; the feed cache is written when the page is left rather than after every pause in scrolling; the theme context value is memoized.
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 16:35:51 +00:00 |
|
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
|
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
|
0cb5a798c7
|
v3: added journal view to the index, code organization pass
|
2026-03-25 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
|
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 |
|