Releases: PiotrRomanczuk/strummy
Release list
v0.162.0: Release: primary domain migration to strummy.online
Summary
- Promotes
maintoproduction— deploys tostrummy.online. - Includes: student sidebar regrouping fix (#578), primary domain migration from
strummy.vercel.app/strummy.apptostrummy.online(#579).
What changes for users
- The app is now reachable at strummy.online (old
strummy.vercel.appandguitar-crm.vercel.appURLs redirect there automatically). - Sidebar for students now groups My Lessons/My Assignments/My Repertoire under Learning, and Song Library/Theory under a new Resources section.
Test plan
- After merge, confirm
strummy.onlineserves the new production deployment - Confirm an invite email's accept link resolves correctly on strummy.online
Full Changelog: v0.161.3...v0.162.0
v0.161.3: Monitoring that can interrupt you, instead of waiting to be checked
The problem
An audit of the monitoring stack found six observability channels and effectively zero automated alerting. Sentry, the system_logs viewer, /api/health, the debug dashboard, PostHog and the home-ops ingest are all pull-based — someone has to go and look before anything is noticed.
That's how a dead client-side Sentry SDK stayed dead for weeks (#573). Worse: lib/env.ts did detect the missing DSN and warned — into system_logs, which nobody reads. A monitoring system whose failure notice is delivered through an unread channel conceals its own failure.
This closes the two holes that made that unsafe.
1. The cron fleet could stop entirely, silently
Every scheduled job in Strummy rides one Vercel cron (Hobby allows a single definition): /api/cron/dispatcher fans out to 15 jobs in-process — lesson reminders, notification delivery, calendar sync, the GDPR cleanup.
A job that throws was already reported (logger.error → captureException). But a cron that never fires produces no error to capture, so that failure mode was pure silence. Your students would have noticed before you did.
The dispatcher now checks in to a Sentry cron monitor (cron-dispatcher, crontab 0 6 * * *, 30-minute margin, UTC): in_progress on entry, ok/error on exit. Sentry alerts on a missed check-in — which also catches "Sentry is misconfigured", because from Sentry's side both look identical: nothing arrived.
Two details worth knowing:
- No check-in is recorded when
verifyCronSecretfails, so a stranger hitting the URL can't suppress the alert by faking a run. automaticVercelMonitors: truecannot do this — it doesn't instrument App Router route handlers, which is every route here.next.config.tsalready said so in a comment; nothing had acted on it.
2. The dispatcher answered 200 even when jobs failed
Individual /api/cron/* routes keep returning 200-with-error-payload (no paging on known-degraded states). But the dispatcher is the fleet's single status signal, and an always-200 answer makes a broken run indistinguishable from a healthy one to any external prober.
It now returns 500 when any job failed. Deliberate no-ops are still excluded via skipped. The existing test that asserted 200 on a failed job now asserts 500 — that assertion change is the behavior change.
3. Nothing outside the app could check whether it was alive
/api/health is admin-only (401 without a session), so no uptime monitor can poll it. New /api/health/live is unauthenticated and returns a single reachability bit:
200 {"status":"ok","database":"up"}— app serves and database answers503— otherwise (unconfiguredcounts as down; in production a missing URL/key is an outage)
It leaks nothing — a test asserts the body has exactly status/database/checkedAt, because the underlying checker's message carries raw error text including LAN hosts and ports. And a plain "is the URL up" check is no substitute: Next happily returns 200 for a shell page with a dead database behind it.
Verified
- lint 0 errors (297 warnings — unchanged baseline; check-in logic moved to
lib/health/cron-monitor.tsbecause keeping it inline pushed the route past the 200-line limit) tsc0 errors- 291 suites / 3805 tests green — +1 suite, +8 tests
Manual test steps: docs/manual-tests/2026-07-30-observability-alerting.html
⚠️ Two manual steps make this actually useful
A monitor with no alert rule is still pull-based. Both need credentials I don't have:
- Sentry → Alerts: rule on the
cron-dispatchermonitor for missed and error check-ins → email/phone. The monitor is created on first check-in, so trigger the dispatcher once first. - Uptime Kuma (on the Pi): HTTP monitor on
https://strummy.vercel.app/api/health/live, 60s interval, accepted status 200-299 only so the 503 trips it. This is the only monitor that survives the whole Vercel deployment being down, because it runs outside it.
Deliberately not included
- Dropping the dormant
audit_log+ 12 partitions. Destructive production DDL — it belongs in its own migration with a rollback path, with explicit approval, not bundled into a monitoring change. - Removing
lib/observability/home-ops-log.mjs— I'd flagged this as dead code and was wrong. It's imported byapp/api/cron/lesson-reminders/route.tsand has test coverage; it's simply unconfigured. Enabling it is just settingINGEST_URL/INGEST_TOKEN.
Docs
10-admin-observability.md gains the check-in contract and the two-health-endpoint rationale. Its UI-surfaces table is also corrected: it listed /dashboard/logs and /dashboard/admin/debug as "Coming soon" placeholders while its own Gaps section said both shipped 2026-07-19. Both are mounted.
Obsidian vault not yet updated — follow-up from the Mac.
Full Changelog: v0.161.2...v0.161.3
v0.161.2: Browser errors now actually reach Sentry
What was broken
Sentry has been reporting nothing from the browser. The previous release turned Sentry back on and the server side genuinely worked — which is exactly why this went unnoticed. Errors from API routes and server rendering flowed into Sentry normally, so the dashboard looked alive, while every error your students actually hit in their browser was silently dropped.
That means all 21 error boundaries, session replay, and every breadcrumb reported nowhere. If a student's lesson page blew up client-side, the fallback UI rendered and you never heard about it.
Why
A file-name convention, not a logic bug — which is what made it invisible.
The app builds with Turbopack (Next 16's default). The Sentry SDK's Turbopack path bundles client config by matching **/instrumentation-client.* and nothing else. The old name sentry.client.config.ts is read only by the SDK's webpack path, which never runs here.
So the file type-checked, linted, imported cleanly, and read as perfectly correct in review — and was never included in the build. Sentry.init never executed in a browser.
The fix
Renaming sentry.client.config.ts → instrumentation-client.ts. That's the entire functional change; the file's contents were already correct, including an onRouterTransitionStart export that had been inert under the ignored name.
Also added a guard test that fails if the old name ever comes back. This failure mode is invisible to builds, type-checking, and lint, so a static check is the only thing that catches it — verified it fails 4 of 5 assertions when the legacy name is restored.
Verified
Against live production before the fix:
window.__SENTRY__andwindow.Sentrybothundefinedon strummy.vercel.app- DSN absent from all 24 scripts on
/and 25 chunks on/sign-in - zero client-config chunks in the deployment's build log, while the server config chunk was present
Local production build after the fix:
- DSN inlined into a client chunk, with replay sample rates,
sendDefaultPii, andtracesSampleRate onRouterTransitionStartbundled into 2 chunks- lint 0 errors ·
tsc0 errors · 290/290 suites, 3797 tests passing
Manual test steps: docs/manual-tests/2026-07-30-sentry-client-instrumentation.html
⚠️ Heads-up before merging
Two things found while verifying, both unrelated to this fix but worth knowing:
maindeploys straight to production. The live deployment carries thestrummy-git-main-…alias and noproductionbranch exists. Merging this ships to real users immediately — there is no staging gate.- Preview deployments are disabled.
vercel.json'signoreCommandcancels every non-production build after a few seconds, sostrummy-preview.vercel.appis stale and cannot be used to check a change first.
CLAUDE.md claimed the opposite on both counts and is corrected here. Auto-merge was deliberately not armed given point 1 — the merge is yours to trigger.
Not covered
Server-side delivery is verified by construction (config compiles in, instrumentation.ts registers it, ingest endpoint confirmed reachable) rather than by watching a real app error arrive. The available Sentry token is project:releases-scoped and cannot read events. An event:read token would close that gap.
Also: the Sentry quota impact of replay plus 10% tracing on live traffic is not measured here.
Obsidian vault not yet updated for this task — follow-up from the Mac.
Full Changelog: v0.161.1...v0.161.2
v0.161.1: Release job: name the repo explicitly — no checkout means no git context
First live run of the release job failed with fatal: not a git repository. The job deliberately runs without a checkout (it needs no code), and gh release create infers the repository from the local git context — which doesn't exist there. The gh api calls in the same script were immune because they name the repo in their request paths. One flag: -R "$REPO".
v0.161.0 was backfilled by hand for #568's merge commit with the same title and notes the job would have produced, so the version sequence stays truthful.
This PR's merge is the live retry: on landing, the job must auto-cut v0.161.1 (fix/ → patch from v0.161.0). If that release appears without manual help, the automation is verified end-to-end.
Full Changelog: v0.161.0...v0.161.1
v0.161.0: Release automation returns: every merge tags and publishes a Release
Last tag: v0.160.0, July 14 — roughly 15 merges have shipped untagged since, while the README described branch-prefix versioning as live. This restores it, redesigned around today's branch protection.
How it works
A small release job in ci.yml (needs: quality-gates, push-to-main only):
- Finds the PR for the pushed commit (
/commits/:sha/pulls). No PR → clean skip — direct pushes like rebuild-trigger commits never release. - Reads the latest release tag and bumps it:
feature/|feat/→ minor, anything else → patch,version:major|minor|patchlabels override. gh release createwith the PR title, the PR body as notes, and acompare/changelog link.
Why tags-only, no bump commit
The old pipeline pushed chore: bump version commits to main. Branch protection now requires passing status checks on anything reaching main — and a commit born inside a workflow run can't have them. Tags sit outside branch protection, so GITHUB_TOKEN with contents: write suffices and nothing fights the protection we just added. Tags/Releases become the version truth; package.json's version stays frozen (this isn't an npm package).
Safety notes
- PR title/body are untrusted text — kept in files, passed via
--notes-file, never interpolated into shell. needs: quality-gatesmeans a failing build can never get tagged.- Dry-run of the version math:
feature/x → v0.161.0,fix/y → v0.160.1,fix/y + version:major → v1.0.0.
Self-validating
Merging this PR should cut v0.160.1 (branch feat/ is… actually minor — v0.161.0) with this description as its release notes. If a Release appears, it works.
Also updates the README's versioning paragraph from 'paused' to the live behaviour — the last CI claim that was aspirational rather than factual.
Full Changelog: v0.160.0...v0.161.0
v0.160.0: feat(auth-email): brand the 4 default GoTrue auth emails to match the invite
feat(auth-email): brand the 4 default GoTrue auth emails to match the invite
What & why
Strummy sent five auth emails, but only the student invite was custom-designed. The other four — signup confirmation, magic link, password reset, email change — shipped as Supabase's unstyled GoTrue defaults (a bare <h2> heading and a raw link on white). That meant a self-serve teacher's first email looked broken next to the brand, and any password-reset or magic-link email did too.
This PR brings all four up to parity with the invite's editorial design.
What changed
- 4 new templates in
supabase/templates/—confirmation.html,magic_link.html,recovery.html,email_change.html— each mirroringinvite.html's system:- Georgia serif italic headline,
#1a1a1aheader band with the Strummy wordmark,#c9a84cgold rule, Courier-mono eyebrows/footer, dark CTA button, and a manual OTP-code fallback. - Per-type copy and subjects (e.g. "Reset your Strummy password", "Confirm your new Strummy email"); the email-change template renders both old → new addresses.
- Georgia serif italic headline,
config.toml— registered the four[auth.email.template.*]blocks alongsideinvite, with branded subject lines. TOML validated.scripts/preview/render-all-emails.ts(new dev tool) — renders every outbound email (GoTrue auth + app transactional) into one self-contained gallery HTML for visual review; its generatedout/is gitignored.
⚠️ Deploy note (not automatic)
Per the existing note in config.toml, prod auth config is applied via the Supabase Management API, not supabase config push (the rest of the [auth.*] block is local-dev oriented and would clobber prod). So merging this does not push the templates to live prod on its own:
- Local / StrummyProd (CLI-managed): take effect on next
supabase start+ thesupabase-post-start.shpatch. - Live prod: needs a Management-API apply step (inline HTML, not a Kong URL — GoTrue silently drops the email if a template fetch 404s). Recommend wiring this into the StrummyProd P4 cutover.
Full Changelog: v0.159.0...v0.160.0
Merged PR: #521
v0.158.5: test(rls): prove student cross-read data isolation against real /rest/v1/
test(rls): prove student cross-read data isolation against real /rest/v1/
What this adds
A security hard gate for the v1 self-host launch: an automated test proving that an authenticated student cannot read another student's private data — verified against the real PostgREST /rest/v1/ layer with real GoTrue JWTs (no mocks, no UI navigation).
tests/e2e/cross-role/rls-data-isolation.spec.ts is hermetic and self-contained:
- Provisions two students + a teacher + one private row per student-scoped table via the service role.
- Signs both students in for real access tokens (
signInWithPassword). - Asserts isolation on every student-scoped table: lessons, assignments, practice_sessions, student_song_progress, profiles.
- Each table gets three assertions: cross-read returns empty, a positive-control own-read (proves the query path works, so "empty" means RLS blocked it — not that data was absent), and a service-role sanity check (proves student B's row actually exists).
- Tears everything down afterward, sweeping by the
rls-iso-prefix (also clears orphans from any prior failed run). - Reads URL + anon + service-role keys from env, so it runs against any stack — proven green locally; ready to re-run against self-host StrummyProd before cutover.
Also fixed
fix(test):app/auth/__tests__/actions.test.tsasserted the old"already registered"signup error literal, but #517 rewrote that message to "This email already has an account or a pending invitation…", leavingmain's Jest suite red. Updated the assertion to the new wording. (The file's large diff is the project's format hook normalizing the whole file; the behavioral change is one line.)
Verification
- New spec: 11/11 green against the dev
StudentManagerstack via real/rest/v1/. - Full Jest suite green with the auth-test fix (2836 tests).
Full Changelog: v0.158.4...v0.158.5
Merged PR: #518
v0.158.4: fix(auth): persist signup names, gate onboarding, improve auth messages
fix(auth): persist signup names, gate onboarding, improve auth messages
What
Four auth-flow issues surfaced by an end-to-end registration test on the live app.
- Signup names were lost. The sign-up action stores
first_name/last_namein auth metadata, buthandle_new_useronly readfull_name(never set by the form) and ignored first/last name — profiles ended up with empty names and the app greeted users by their email. Migration20260622210000rewriteshandle_new_userto persistfirst_name/last_nameand derivefull_name(prefers metadata full_name, else first+last), in both the shadow-link and new-user branches. - Onboarding could be skipped. The role/onboarding gate lived only in
/auth/callback(OAuth + email-code path), so a password sign-in reached the dashboard with no role. Centralized the gate in the dashboard layout — role-less users redirect to/onboardingregardless of entry path. - 'Already registered' dead-ended invited users (told to sign in / reset a password they never set). Message now points invited users to their invitation email.
- 'Email not confirmed' offered no recovery. The sign-in action flags that state and the page shows a 'Resend confirmation email' action.
Migration deployment note
20260622210000 is applied to the StrummyProd stack. Applying it to the live self-hosted prod (StudentManager) needs care: that DB is schema-behind and may be missing user_roles, which handle_new_user references — apply a schema-aware variant or add the table first.
Tests
Pre-commit hook green (full Jest suite + lint).
🤖 Generated with Claude Code
Full Changelog: v0.158.3...v0.158.4
Merged PR: #517
v0.158.3: chore(db): capture authoritative Cloud schema baseline for self-host migration
chore(db): capture authoritative Cloud schema baseline for self-host migration
What
Authoritative snapshot of the live production schema (Cloud zmlluqqqwrfhygvpfqka, PG 17.6), captured 2026-06-22 as the source of truth for the Cloud → self-host migration. Prod had drifted from the repo migration chain (renames, type changes, out-of-band MCP DDL), so a dump of prod — not a replay of migrations/ — is the truth.
supabase/baseline/cloud_schema_2026-06-22.sql—publicschema viapg_dump --schema-only: 62 tables, 50 functions, 20 enum types, 199 RLS policies, 46 triggers, 213 indexes.supabase/baseline/cloud_auth_hooks_2026-06-22.sql— theauth.users → handle_new_usertrigger (cross-schema; a public-only dump misses it, which would silently break signup/invite profile creation).supabase/baseline/README.md— provenance, load order for the new prod stack, and deliberate exclusions (data, platform-managed schemas/triggers).
Why
Doubles as the P1 load artifact for the new dedicated self-hosted prod stack. No app code; no change to any running database.
🤖 Generated with Claude Code
Full Changelog: v0.158.2...v0.158.3
Merged PR: #516
v0.158.2: fix(db): capture 2 orphan prod migrations into version control
fix(db): capture 2 orphan prod migrations into version control
What
Two PL/pgSQL functions were live on production (Cloud zmlluqqqwrfhygvpfqka) but existed nowhere in the repo — applied directly via the Supabase MCP and never committed. This captures them verbatim from prod schema_migrations under their original version strings, so the repo and prod line up.
20260622121619_fix_handle_new_user_invite_email_match—handle_new_user()now matches shadow profiles byinvite_email(placeholder-email shadows), so accepting an invite links the existing profile instead of creating a blank account and orphaning lesson history.20260622121836_fix_track_lesson_changes_delete—track_lesson_changes()wraps the AFTER-DELETE audit insert in an exception block so lesson deletions are never blocked by thelesson_historyFK.
Why
Surfaced by a Cloud↔repo migration-history diff while planning the Cloud→self-host migration. These were the only prod-only, version-controlled-nowhere changes — losing them in any rebuild-from-repo would silently regress the invite flow and lesson deletes. This is additive (idempotent CREATE OR REPLACE FUNCTION), no schema/data change.
Verification
- SQL validated by applying both inside a rolled-back transaction on the dev DB (both
CREATE FUNCTION, cleanROLLBACK). - Pre-commit hook green: 209 suites / 2831 tests + lint.
🤖 Generated with Claude Code
Full Changelog: v0.158.1...v0.158.2
Merged PR: #515