Skip to content

v2.1.0

Choose a tag to compare

@7PH 7PH released this 27 Apr 01:31
· 128 commits to master since this release

v2.1.0

  • Released: 2026-04-27
  • Summary: Managed accounts, SEO primitives, premium-gate primitives, sanitized markdown messages, bug-report flow, self-service account deletion, redesigned landing page, preview deployments, and Claude UI-check tooling.

Upgrade Notes

No breaking changes. Most new features are off by default behind feature flags or env vars and won't change behavior until a sub-app opts in.

Migration

A single new migration: app/backend/migrations/2026-04-26-v2.1.0-managed-accounts.sql. It:

  • Drops NOT NULL from users.email, first_name, last_name, hashed_password (managed accounts have none of these).
  • Adds auth_method, display_name, managed_account_group_id to users, plus a users_auth_integrity CHECK constraint that requires existing rows be one of password / oauth / access_code / deleted.
  • Backfills auth_method='oauth' for rows with oauth_provider IS NOT NULL; everything else stays password.
  • Creates managed_account_groups and access_codes.

All existing rows pass the CHECK with the default backfill. The migration is irreversible once any user has auth_method='access_code' or 'deleted'.

New environment variables

Add to your .env (the templates are already in .env.template):

# Required for the dev-mint-token endpoint. Leave empty to keep the route
# unregistered in production-like envs.
INTERNAL_API_KEY=`openssl rand -hex 32`

# Managed accounts (off by default).
MANAGED_ACCOUNTS_ENABLED=false
MANAGED_ACCOUNT_GROUP_MAX_PER_USER=20
MANAGED_ACCOUNTS_MAX_PER_USER=50

MANAGED_ACCOUNTS_ENABLED is forwarded to the frontend as NUXT_PUBLIC_MANAGED_ACCOUNTS_ENABLED automatically by docker-compose.

New Features

Managed accounts

Email-less child accounts that sign in by short access code instead of email/password. An owner provisions a group, bulk-imports accounts into it, and shares a public picker URL (/c/:token) where a managed user picks their name and types their code. Premium status is inherited from the group owner via is_premium_effective(session, db_user).

  • New schemas: managed_account_groups, access_codes, plus auth_method / display_name / managed_account_group_id on users, with a CHECK constraint enforcing one of password / oauth / access_code / deleted.
  • Pages: /managed-account-groups/index, /managed-account-groups/[id], public /c/[token] picker.
  • New middleware: managed-accounts-enabled.ts, nonmanaged-only.ts.
  • Pluggable can_manage_groups policy (app/backend/src/helpers/policies.py) so apps can override who is allowed to provision groups.
  • Composables: useAccessCode, useManagedAccountGroup, usePickerLink, useUserDisplay.

SEO primitives

  • @nuxtjs/sitemap registered with explicit URLs (auto-discovery off so private routes can't leak).
  • Six schema.org JSON-LD helpers (Course, FAQPage, Product, LocalBusiness, BreadcrumbList, Article) with pure builders and vitest coverage (tests/schema.test.ts).
  • <Breadcrumb> component wired through useOverridable so apps can swap visuals without losing SEO.
  • useCanonical() composable and a dynamic /robots.txt route.
  • Sub-app extension points: config/sitemap-ext.ts, config/robots-ext.ts (templates).
  • PUBLIC_URL is now bound through to the frontend container; the server/ directory is mounted in dev so nitro routes load.

Markup-driven premium gating

Frontend primitives for premium teasers — server-side entitlement still belongs in require_premium (see CLAUDE.md):

  • <PremiumGate> with #teaser and #gated slots. Non-premium users get hidden / aria-hidden / inert plus schema.org JSON-LD (Article + isAccessibleForFree=false) for Google flexible sampling. The CTA opens the existing ModalAuth for logged-out users instead of bouncing through /login.
  • <PremiumBadge> decorative lock chip.
  • usePremiumStatus() for cheap entitlement reads with auto-refresh on tab focus.

Demo wired into the landing-page bento.

Sanitized markdown for conversation messages

User and admin message pages now have a Write/Preview compose toggle backed by markdown-it + a private DOMPurify instance. The renderer refuses to run on the server so DOMPurify can't fail-open without a DOM, and the link-safety hook is scoped to its own purifier so target="_blank" doesn't leak to other DOMPurify callers.

Bug-report entry point

Drop-in <BugReportButton> opens a modal that posts a support conversation with subtype=bug_report, reusing the existing message system end-to-end (admin inbox, threading, status).

  • CoreConversationSubtype enum + UserInitiableSubtype alias in constants_ext.py:template, so sub-apps widen the accepted set with type UserInitiableSubtype = CoreConversationSubtype | Literal[...].
  • Per-subtype rate limits (5/hr + 20/day) and a 10-minute account-age gate on POST /conversations.
  • Server-side URL stripping on the bug-report body for PII hygiene.
  • Shared <ConversationComposerModal> extracted from /messages.

Self-service account deletion

/account gets a Privacy tab; users can request deletion and confirm via emailed link. The /delete-account page renders the confirmation. Backend wires up helpers/account_deletion.py, dedicated email templates, and matching auth-token plumbing.

Landing page redesign

Asymmetric hero (headline + CTAs left, live terminal panel right) and a bento features grid. The terminal's last line is bound to the existing API health probe and flips between probing / healthy / unreachable. New i18n keys live in app-specific en.json/fr.json so the starterpack-managed core locale files stay untouched.

Lazy YouTube facade

<YouTubeFacade> renders a thumbnail + play button and only loads the real iframe on click — saves ~500KB of JS per unplayed video. Uses youtube-nocookie.com so no YouTube cookies are set until the user presses play.

Preview deployments

docker-compose.preview.yml + .github/workflows/preview.yml spin up an isolated stack per pull request and post the preview URL as a PR comment. The comment is now deduped via a hidden marker so reruns update the same comment instead of spamming.

Internal mint-token endpoint (dev tooling)

POST /api/v1/internal/mint-token returns an access token for any user. Gated by INTERNAL_API_KEY — when the key is empty the route is unregistered, so production envs that don't set it are unaffected.

Used by npm run dev-mint-token, which prints a JS snippet that injects the token into a Playwright browser session. With no email argument, it falls back to the first non-deleted admin. Lets Claude run UI checks on auth-gated pages without going through the login form.

Also adds .mcp.json (Playwright MCP server) and ignores .playwright-mcp/.

Bug Fixes

  • Managed-account schemas weren't picked up by convert-models, so ManagedAccount{Group}* and PublicPicker* types were missing on a fresh install (local typecheck happened to pass on cached types; CI failed). Now in the CoreModels union.
  • Preview-URL PR comments are deduped via a hidden marker — reruns update the existing comment instead of posting a new one.

New Core Files

Backend

  • app/backend/migrations/2026-04-26-v2.1.0-managed-accounts.sql
  • app/backend/src/constants_ext.py:template
  • app/backend/src/controllers/internal.py
  • app/backend/src/controllers/managed_account_groups.py
  • app/backend/src/crud/access_codes.py
  • app/backend/src/crud/managed_account_groups.py
  • app/backend/src/helpers/access_codes.py
  • app/backend/src/helpers/account.py
  • app/backend/src/helpers/account_deletion.py
  • app/backend/src/helpers/bug_report.py
  • app/backend/src/helpers/policies.py
  • app/backend/src/helpers/user_display.py
  • app/backend/src/models/access_code.py
  • app/backend/src/models/managed_account_group.py
  • app/backend/src/schemas/managed_account.py
  • app/backend/src/templates/email/account_deletion.html
  • app/backend/src/templates/email/account_deletion.txt
  • app/backend/tests/test_auth_code.py
  • app/backend/tests/test_bug_report.py
  • app/backend/tests/test_managed_account_groups.py
  • app/backend/tests/test_managed_account_permissions.py
  • app/backend/tests/test_user_display.py

Frontend

  • app/frontend/components/account/PrivacyTab.vue
  • app/frontend/components/common/Breadcrumb.vue
  • app/frontend/components/common/BreadcrumbDefault.vue
  • app/frontend/components/common/BugReportButton.vue
  • app/frontend/components/common/PremiumBadge.vue
  • app/frontend/components/common/PremiumGate.vue
  • app/frontend/components/common/YouTubeFacade.vue
  • app/frontend/components/managed-accounts/BulkImportModal.vue
  • app/frontend/components/messages/ConversationComposerModal.vue
  • app/frontend/config/robots-ext.ts:template
  • app/frontend/config/sitemap-ext.ts:template
  • app/frontend/middleware/managed-accounts-enabled.ts
  • app/frontend/middleware/nonmanaged-only.ts
  • app/frontend/pages/c/[token].vue
  • app/frontend/pages/delete-account.vue
  • app/frontend/pages/managed-account-groups/index.vue
  • app/frontend/pages/managed-account-groups/[id].vue
  • app/frontend/server/routes/robots.txt.ts
  • app/frontend/e2e/seo.spec.ts
  • app/frontend/tests/markdown.test.ts
  • app/frontend/tests/schema.test.ts
  • app/frontend/tests/useBreadcrumb.test.ts
  • app/frontend/tests/useCanonical.test.ts

Infra & tooling

  • docker-compose.preview.yml
  • .github/workflows/preview.yml (covered by .github/workflows/* glob)
  • scripts/_core/dev-mint-token.sh (covered by scripts/_core/* glob)