Skip to content

v0.5.0

Latest

Choose a tag to compare

@github-actions github-actions released this 10 Oct 11:00
51b6870

Added

  • Sign in with Google or GitHub, API side (D-023; the web screens come next). GET /auth/providers, GET /auth/oauth/{provider}/start?next=, GET /auth/oauth/{provider}/callback, GET /auth/oauth/pending and POST /auth/oauth/complete {display_name, accept_terms}. The callback binds the browser to the flow with a state cookie (HttpOnly, SameSite=Lax, path /api/v1/auth/oauth, 10 min) and requires query state = cookie = an unused row of that provider; the PKCE verifier is stored encrypted with the ENCRYPTION_KEY cipher; the redirect URI comes from API_PUBLIC_URL, never from Host; next goes through one shared allow-list (domain/redirect, also used by the account-connect flow). Every callback outcome is a 302 to the web app (/login?error=<code> on failure). A new user gets no account until they accept the Terms at /signup/complete (D-016); the ticket is redeemed in the same transaction as the user, so a refused form does not burn it. Disabled accounts are refused; an account waiting for deletion can sign in with its provider to cancel the deletion, but never gains a new link by email match, and an automatic link sends the owner an identity_linked notice mail. Start, callback and complete sit behind the auth rate limiter, the worker deletes expired flows hourly, and the audit log records user.registered / user.login with {method} and user.identity_linked. No tokens or addresses reach logs or audit metadata. Setup steps for the GitHub OAuth App (and Google, once a domain exists) are in docs/OWNER-SETUP.md section 2.0.
  • Account deletion for users without a password: a session created within the last 10 minutes is accepted instead of a password (the typed email is still required); an older one gets 403 REAUTH_REQUIRED, so deletion stays available and never fails with a 500. Sessions now record created_at from the injected clock.
  • GET /api/v1/version (also /version): a public, unauthenticated answer to "what runs in production" without SSH, {"version":"0.4.0","commit":"abc1234","built_at":"2026-10-10T12:00:00Z"}, cacheable for 60 seconds. Release images carry the tag version (a main build says sha-<7 hex>), the short commit and the build time, linked in from build arguments that release.yml passes to backend/Dockerfile; a local build falls back to the Go VCS stamp, then dev / unknown. Nothing about the host or the toolchain is exposed. The Uptime workflow prints the deployed version in its log.
  • Pause motion in the app (Settings, Preferences), as the landing has had since D-017: it stops every animation and view transition in this browser, on top of the system's reduced-motion setting, and applies before the first paint.
  • Manage sign-in methods, API side (D-023; the Settings screen comes next). GET /auth/identities, POST /auth/identities/{provider}/link (returns authorize_url; the provider calls the ordinary sign-in callback, which finishes the link and returns to /settings), DELETE /auth/identities/{provider} (refused with 409 when it is the last way to sign in) and POST /auth/password/set for accounts without a password (session younger than 10 minutes, else 403 REAUTH_REQUIRED). /me gains user.has_password and user.login_methods. Linking and unlinking re-authenticate (current_password, or a session under 10 minutes old for users without a password), are audited and are mailed to the owner. A provider that is switched off does not count as the remaining way to sign in. The demo answers the new routes without pretending to link anything. Failed sign-in and link redirects carry provider=<id> (and, for sign-in, the sanitised next).
  • Sign in with Google or GitHub, web side (D-023). /login and /register share one screen: "Continue with Google / GitHub" buttons on top (only the providers GET /auth/providers lists; the block and its divider are hidden when there are none, and a failed list says so and keeps the email form), then the email form. The buttons are links to /api/v1/auth/oauth/{provider}/start?next=, so next survives the provider round trip. New page /signup/complete: the provider's verified email read-only, the name prefilled and editable, an explicit Terms checkbox (D-016) and "Create account"; with no waiting sign-up (a 404) it goes back to /login?error=signup_expired. /login?error=<code> shows plain-language copy for every code the API sends (oauth_cancelled, oauth_state_invalid, oauth_provider_error, email_unverified, account_exists, account_unavailable; identity_in_use goes to /settings); an optional provider=github|google in that URL names the provider. Deleting the account with a session too old for a password-less user (403 REAUTH_REQUIRED) now offers "Sign in again with {provider}" and returns to Settings; the password field there is optional for people who signed up with a provider. The demo and scripts/mock-api.mjs answer every new endpoint (GitHub and Google, a simulated redirect to /signup/complete). 28 new strings in every enabled locale and kk (machine drafts).
  • Manage sign-in methods, web side (D-023). Settings has a "Sign-in methods" section: the password (set or not; "Set a password" for accounts made with Google or GitHub) and each provider the server offers, connected (email and date, Disconnect) or not (Connect). Connect and Disconnect ask for the current password when the account has one, then go to the provider or disconnect; a wrong password shows at the field, the last remaining method is refused with "keep at least one way to sign in", and a session too old for a password-less account (403 REAUTH_REQUIRED) offers "Sign in again with {provider}" for the providers that account is linked to. A provider the admin switched off keeps only Disconnect. /settings?error=<code>&provider=<id> shows plain copy for the codes the link callback sends (anything else reads as a generic sentence and is never echoed) and is cleared from the address; a successful connect carries no flag, so the list is simply read fresh. The delete-account dialog hides the password field for password-less accounts and offers sign-in-again buttons for their linked providers only; the change-password form is hidden for them. The demo has no provider to visit and says so instead of connecting. scripts/mock-api.mjs models password-less accounts (an empty password never signs in). Strings in every enabled locale and kk (machine drafts).

Changed

  • A vX.Y.Z tag build no longer pushes sha-<7 hex>: that tag is pushed only by the main (or manual) build of the commit, so its digest never changes and deploys pinned to it stay reproducible. A tag build pushes X.Y.Z, X.Y and X; a tag that is not vMAJOR.MINOR.PATCH[-pre] now fails the run instead of pushing only the sha tag.
  • Uptime monitoring is sturdier, and the owner guide sets up an external monitor. GitHub ran the */15 schedule only every 3 to 7 hours, so a 70-minute outage on 2026-10-09 went unnoticed. The workflow now fires at :07, :22, :37 and :52 (off-peak minutes) and can also be started by repository_dispatch (uptime-check) or by hand with its own urls. Each URL gets 4 attempts with 5/10/15 s backoff before it counts as down (was 3). Incident issues carry the labels incident and uptime, name the last HTTP code and link the run, and the run summary lists every target. Recovery still comments and closes the issue. OWNER-SETUP section 6 sets up UptimeRobot Free step by step: four monitors (app /api/v1/health, API /api/v1/ready, MCP /health, web /login) with keywords, 5-minute checks, email and Discord alerts, and a public status page. Telegram alerts need UptimeRobot's paid Solo plan or HetrixTools Free; the guide says so. No account is created by the repository.
  • CI checks first-load JS (issue #180). After the production build, npm run budget (frontend/scripts/bundle-budget.mjs) sums the gzip (level 6) of each route's scripts from .next/app-build-manifest.json: root main files, the layout, template, error, loading and not-found entries of every level above the page, and the page itself (the same scripts the prerendered HTML lists, polyfills aside) (Next's table omits them), polyfills excluded. frontend/bundle-budget.json holds the limits for /login, /register, /verify-email, /dashboard, /compose, /posts and /settings (measured size rounded up plus 2 kB). The job also fails when skipToContent (shell.json) is back in the root layout chunk or Radix Dialog is in an auth route's chunks. Raising a budget means editing the JSON in the same PR with a reason; lower it when a route shrinks. No new dependency or action.
  • Each route loads only the message namespaces it uses (step 3 of #145; text and behaviour unchanged). The root layout carries common and errors; (auth) adds auth and legal; (app) adds nav, shell, language and legal; each page adds its own (src/i18n/scopes/*.tsx, mapping in docs/copy/translation-process.md). The demo banner brings shell in the demo build only. First-load JS (gzip of the page and its layout chunks, from .next/app-build-manifest.json; Next's table leaves the layout chunk out) went from 169.7 to 162.0 kB on /login, 162.0 to 154.3 on /verify-email, 175.7 to 167.7 on /dashboard, 185.4 to 177.9 on /compose, 179.6 to 171.2 on /accounts, 182.4 to 176.1 on /settings and 173.0 to 164.3 on /posts; the shared baseline stays 104 kB. LegacyMessagesScope is gone. In development, asking for a namespace no scope above the component loaded logs MISSING_SCOPE: <key> once and renders the key; tests/i18n-route-scopes.test.tsx fails when a page's components use a namespace its scopes do not load, and the site smoke test fails when a demo screen shows a raw key in English or Russian.
  • Message loading runs through scopes (step 2 of #145; text and behaviour unchanged). MessagesScope (src/i18n/scope.tsx) merges the English JSON bundles it is given over its parent, reads other locales with use() from one lazy chunk per locale (a missing key reads English; a chunk that fails to load is logged and the subtree stays English), and registers its bundles so a language switch preloads them, sets the locale in a transition and shows the shell (data-i18n-pending) only after that commit. The root layout wraps the app in one LegacyMessagesScope with every bundle until routes declare their own (step 3). enT outside React became enErrorsT over errors.json only; describeErrorCode takes a translator for errors.* keys. Cost: about +1 kB gzipped of runtime on every route.
  • Message catalogs are split per namespace: frontend/messages/{locale}.json became frontend/messages/{locale}/{namespace}.json (step 1 of #145; text and behaviour unchanged). src/i18n/catalog.ts lists the bundles and types the English catalog; npm run i18n:check also fails on a namespace file English lacks and on a bundle list that differs from messages/en/.
  • Privacy Policy and Terms version 2026-10-11: the Privacy Policy covers signing in with Google or GitHub (what is stored, the scopes asked for, the 10-minute sign-in cookie, export and deletion, confirming deletion without a password). New accounts accept this version; existing ones are not blocked (D-016).
  • Glass surfaces, step 7 of D-024: the landing page gets the app's glass. The floating nav is chrome glass (20 px blur) and the hero product frame is a glass card with a 16 px blur over the flow canvas; the four fact tiles, the closing card and the story frame are glass cards without blur, all on the teal mesh at full strength (the app shows it at 0.45). Docs pages and long text stay solid. The glass, mesh and blur values are the shared tokens; fallbacks for no backdrop-filter, prefers-reduced-transparency and forced colours; the view-transition name moved from the header to the bar so the blur still sees the page. The hero glow is lower so the lead stays AA on the mesh. site/scripts/glass.test.mjs checks the contrast of text on the mesh and on each glass layer, the blur budget and the fallbacks; the smoke test accepts the nav as chrome glass (alpha 0.66 or more over a 20 px blur). The hero video is not re-recorded.
  • Hero transitions, step 6 of D-024: signing in, registering and finishing a Google or GitHub sign-up fill the screen with an accent circle from the pressed button and reveal the dashboard behind it; Publish now (composer and post page) sends a diagonal accent wipe across the screen and the success message lands after it. Each is one overlay that never takes the pointer, at most 520 ms, and is skipped under reduced motion and Pause motion. Scheduling from the composer into the calendar (the shared-element move) is a separate step.
  • Motion, step 5 of D-024: moving between pages is a 150 ms fade instead of a 320 ms rise; lists no longer stagger in (accounts, approvals, media, posts, the onboarding steps), except the dashboard's sections on its first load in a tab; a view transition whose navigation never commits releases the page after 300 ms instead of 1 s.
  • The Dockerfiles pin their base images by digest (golang:1.26-alpine, node:22-alpine, distroless/static-debian12:nonroot), so a registry or mirror cannot hand a release a different or stale base for the tag; Dependabot's weekly docker update bumps the digests.
  • CI and release builds pull Docker Hub images through Google's cache mirror.gcr.io (falling back to Docker Hub): Docker Hub's anonymous pull limit on shared runner IPs answered 429 for hours and failed every E2E and actionlint run.
  • Glass surfaces, step 4 of D-024: the dashboard's four numbers are glass tiles with an icon in a lagoon chip (the failed tile turns red only when something failed), the onboarding checklist is a glass panel, and post lists are solid background panels on every page instead of showing the mesh through (BRAND: dense lists are never glass). Card gains surface="glass".
  • Glass surfaces, step 3 of D-024: the sidebar and the mobile header are glass (the open mobile menu, which scrolls with the page, is not blurred), the active and hovered nav rows use the lagoon secondary colour (the accent bar stays, and forced colours get an outline), and counts are small neutral squares with tabular figures. Status tags replace the rounded pastel pills: a 5 px tag with a hairline border, the label in the text colour and a 12 px glyph that alone carries the colour, one shape per meaning (draft dashed ring, waiting half ring, scheduled clock, in progress arc, published filled check, partly published ringed check, failed triangle, needs attention ring with a bang, cancelled slashed ring; an approval that is allowed but not yet used shows the ringed check, one that was carried out the filled check). Quiet statuses (draft, pending, cancelled) keep a muted label. Tags without a status (risk, trust, actor) keep the tone on the label; unsupported capabilities are dashed and struck through.
  • Glass surfaces, step 2 of D-024: the app page is the canvas colour with a soft fixed teal mesh behind it (a faint grain on top), at a strength that keeps text AA and control outlines at 3:1 directly on it (tested). New utilities .glass-chrome, .glass-card, .glass-float and .glass-panel with fallbacks for browsers without backdrop-filter (solid surface), prefers-reduced-transparency (solid surfaces, no mesh) and forced colours. Dialogs are a solid panel with the glass edge and shadow over a plain dim scrim (--scrim, no blur; dark mode dims rather than hazes), toasts are floating glass, tables stay solid. The browser theme colour and the web manifest follow the canvas.
  • Typeface: Onest replaces Inter in the app, the demo and the site (D-024). It covers every Kazakh letter, has tabular figures and is lighter: an English page now loads 49 KB of font (Onest latin plus the Cyrillic subset the language menu needs) instead of 65 KB. Arabic, Japanese and Simplified Chinese get Noto Sans Arabic, Noto Sans JP and Noto Sans SC (variable) after Onest, loaded only on those locales: in the app as a lazy CSS chunk when the locale switches, on the landing as assets/fonts/<locale>.css linked only from that page and cut to the unicode-range slices it uses (measured first visit: ar 211 KB, ja 563 KB, zh-CN 983 KB of font). Everything is self-hosted from @fontsource-variable packages; no font request leaves the site. The OFL licences ship with the fonts (/licenses/fonts.txt in the app, assets/fonts/<package>-OFL.txt on the site). The script faces load with font-display: optional, so a late font never shifts the page (CLS 0 measured at 400 ms latency on ar, ja, zh-CN). The wordmark keeps its Inter outlines (site/scripts/brand-fonts/).
  • Design tokens for the glass style (D-024, no visual change yet): canvas, the lagoon secondary colour, info, chart-1 to chart-4, glass surfaces, blur and highlight, the background mesh, --duration-hero and --ease-fill, wired into Tailwind (bg-secondary, text-info, bg-canvas, bg-chart-1, duration-hero, ease-fill…; glass itself is only the .glass-* classes, so blur, tint, edge and fallbacks travel together). tests/tokens.test.ts checks the new text pairs and every text colour on each glass surface composited over the mesh at its strongest (AA).
  • CI is one workflow: security.yml is merged into ci.yml. A pull request that touches everything runs 9 jobs instead of 19; the scanners (govulncheck, npm audit, gitleaks, Trivy, CodeQL) keep their checks and SARIF categories. The new aggregator job CI ok is the single check a branch ruleset needs to require; jobs skipped because their paths did not change count as passed. The weekly scan now runs from ci.yml.
  • GitHub Pages moved into ci.yml: a site job builds the demo and the site, checks links and runs the browser smoke test on every pull request that touches frontend/, site/, docs/ or the READMEs, so breakage no longer reaches main; a pages job deploys only after a push to main. pages.yml and its "is Pages enabled?" gate are gone.
  • Registration asks for email and password only; the name is no longer collected there (the API already accepts an empty one). Errors appear under their field when it loses focus and for every fields.* the server refuses; the submit button is never silently disabled (an incomplete form lists what is missing and focuses the first field); the password has a show/hide toggle; the email input is autocapitalize="none" with spell-check off. A taken email offers "Sign in" and "Forgot password?".
  • The "provider connected" notice mail now says where to undo it (Settings, Sign-in methods) and no longer assumes an automatic link.
  • Upgrading from 0.4.0: no new database migrations (the schema stays at 00008), so the deploy is an image swap; deploy.sh and autoupdate run migrate as usual and it finds nothing to do.
  • Google and GitHub sign-in are off unless configured. GOOGLE_CLIENT_ID/SECRET and GITHUB_CLIENT_ID/SECRET (already in the .env examples since 0.4.0) now switch them on: a provider appears on /login only when both halves are set, and setting only one half stops the API at start. Register the callback https://api.<domain>/api/v1/auth/oauth/{google|github}/callback with the provider first (OWNER-SETUP section 2).
  • Legal version 2026-10-11 is recorded only for accounts created from now on. Existing users are not asked to accept again and are not blocked at sign-in (D-016).
  • GET /api/v1/version is public and reports the running release; an image built outside release.yml reports dev / unknown.
  • Release tags must be vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-pre; any other v* tag now fails the Release workflow and pushes nothing. A tag build no longer pushes sha-<7 hex> (the main build of the same commit does).

Fixed

  • The production build no longer ships the demo banner: it was imported statically by the root layout, which bundled it (with its Reset dialog, Radix Dialog and the shell messages) into every route. First-load JS (gzip, all chunks of the route): /login 159.2 → 148.3 kB, /verify-email 151.9 → 137.8 kB; app routes about the same (/dashboard +0.6 kB, the dialog now loads with the pages that use it). Part of #180.
  • Unknown URLs show a translated "Page not found" page with a button to the dashboard, instead of Next's English default (#181).
  • Publish now in the composer no longer creates the post twice when the first try fails after the post was saved (#127): you are taken to the saved post with the reason and publish it from there. The Publish now wipe plays only after a publish that went through.
  • After a navigation that removes the focused control (signing in, a dialog that leaves the page, a link in the mobile menu, moving between sign-in and sign-up) keyboard focus fell back to the top of the document; it now lands on the page's main content. Focus that is still somewhere (a sidebar link) is left alone.
  • The data export now includes sign_in_methods.json (the Google or GitHub accounts you sign in with: provider, account id there, email, dates), and profile.json says whether a password is set. Before, the archive left them out although it promised everything.
  • The "Create one" and "Sign in" links between /login and /register dropped ?next=; they keep it now, and only same-origin paths are accepted.

Removed

  • The "Email delivery is not configured on this server" notice above every signed-in screen: it spoke to the server's admin, so on a server without mail every user saw it. The forgot-password page still says honestly that no mail will arrive, and the verification banner is unchanged.