Skip to content

Commit 482e121

Browse files
lane711claude
andauthored
feat(menu-plugin): data-driven admin sidebar navigation (#940)
* chore(deps): consolidate all dependabot dependency updates - hono: ^4.12.18 → ^4.12.26 (security fixes in 4.12.25: CORS, body-limit, serve-static path traversal, AWS Lambda Set-Cookie, Lambda@Edge header) - vitest: ^4.0.5 → ^4.1.9 (root, packages/core) - @vitest/coverage-v8: ^4.0.5 → ^4.1.9 (root, packages/core) - my-sonicjs-app vitest: ^2.1.8 → ^4.1.9 (align with root) - postcss: 8.5.6 → 8.5.15 (transitive, lockfile) - qs: 6.15.0 → 6.15.2 (transitive, lockfile) - shell-quote: 1.8.3 → 1.8.4 (transitive, lockfile) Closes PRs: #918, #862, #851, #849, #843, #840, #839, #837 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: replace page_blocks with e2e_test collection page_blocks was registered in the sample app but has no place in a base install. Replaced with e2e_test collection that covers all field types (string, slug, textarea, number, boolean, date, datetime, user, media, select, radio, lexical, object/flat, array/blocks) so E2E specs have a stable test fixture without polluting the starter experience. Updated 10 E2E specs to reference e2e_test / E2E Test instead of page_blocks / Page Blocks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(create-app): bump compatibility_date to 2024-09-23 to resolve crypto import error * docs(marketing): add v3 beta Discord announcement and blog post Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(marketing): fix npm create command to sonicjs (not @beta tag) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(create-app): include blog collection by default, fix seed timestamp, simplify post-install output (#925) - Remove prompt for example blog collection; always include it - Fix admin seed crash: Date.now() → new Date() for Drizzle timestamp_ms columns - Move Cloudflare resource creation to optional "Deploy to Cloudflare" section; only show migration/seed steps when they actually failed * chore: release v3.0.0-beta.3 * docs(www): add v3.0.0-beta.3 to changelog and homepage * fix(create-app): fix admin seeding and generate BETTER_AUTH_SECRET for local dev - Rewrite generated seed-admin.ts to use raw SQL (bypass Drizzle schema mismatch with auth_user columns missing from migration) - Create auth_account record so Better Auth credential login works - Use PBKDF2 via Web Crypto for password hashing (matches auth system) - Auto-generate .dev.vars with BETTER_AUTH_SECRET on project creation (missing secret caused 500 on every auth request) - Add --admin-email / --admin-password flags for non-interactive use - Replace wrangler resource commands in success output with npm run deploy * chore: release v3.0.0-beta.4 * docs(www): add v3.0.0-beta.4 to changelog and homepage * chore: untrack settings.local.json and add to gitignore Per-developer Claude Code settings should not be shared. settings.local.json contained machine-specific rules and shared auto-approve allowlists. Untrack it so each dev keeps their own local copy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): add versioning plugin to manifest registry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.5 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.5 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): fix v3 changelog placement and stale doc errors (#926) - Move v3.0.0-beta.3 entry from under "Version 2.x" into "Version 3.x" - Add "Version 3.x" section to changelog sidebar nav - Fix JWT default lifetime: 24h → 30d (matches DEFAULT_JWT_EXPIRES_IN_SECONDS) - Fix stale comment labeling v3.0.0-beta.3 entry as v2.19.0 on homepage * fix(create-app): update blog_post collection and disable cache plugin by default (#927) - Switch blog_post content field from quill to lexical - Update blog_post schema: name, slug, author type (user), remove stale fields - Set core-cache defaultActive to false (only lexical should be on by default) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(create-app): fall back to npx wrangler when not on PATH Resource creation runs before npm install, so wrangler isn't in node_modules/.bin/ yet. Users running via npx also won't have a global wrangler binary. Fall back to `npx wrangler` in both cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugins): disable cache plugin by default on new installs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.7 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.7 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(stats): auto-deploy stats.sonicjs.com on main merges (#932) - Add deploy-stats.yml CI workflow (triggers on packages/stats/** or packages/core/src/** changes to main + workflow_dispatch) - Build core, type-check, then wrangler deploy --env production - Add deploy:production script to packages/stats - Add deploy:stats convenience script to root package.json - Update wrangler compatibility_date to 2025-05-05 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(www): resolve build errors blocking deploy (#933) - rehype.mjs: guard parentNode.properties before set — mdxJsxFlowElement nodes (inline code in JSX <li>/<span>) have no properties object, causing "Cannot set properties of undefined (setting 'language')" - blog/[slug]/page.tsx: pass Callout to compileMDX components map — blog posts using <Callout> crashed with "Expected component Callout to be defined" * chore(www): upgrade next + opennextjs-cloudflare for Next 16 compat (#934) * fix(www): resolve build errors blocking deploy - rehype.mjs: guard parentNode.properties before set — mdxJsxFlowElement nodes (inline code in JSX <li>/<span>) have no properties object, causing "Cannot set properties of undefined (setting 'language')" - blog/[slug]/page.tsx: pass Callout to compileMDX components map — blog posts using <Callout> crashed with "Expected component Callout to be defined" * chore(www): upgrade next and opennextjs-cloudflare for Next 16 compat next: 16.2.1 → 16.2.9 @opennextjs/cloudflare: 1.18.0 → 1.19.11 1.18.0 couldn't bundle next-server.js with Next 16 (missing shims for node-environment, node-polyfill-crypto, etc). 1.19.11 adds Next 16 support and requires next >= 16.2.3. * docs(www): update email plugin docs to cover Resend and Cloudflare Email (#936) Previously documented Resend only. Plugin now supports both Resend (via RESEND_API_KEY env var or DB settings) and Cloudflare Email (via send_email Workers binding), with a console fallback for dev. - Add providers table with enable instructions for each option - Document provider resolution order (env var → DB → CF binding → console) - Split configuration into per-provider code groups - Add setup steps for both Option A (Resend) and Option B (Cloudflare Email) - Fix admin settings path: /admin/email/settings (was /admin/plugins/email/settings) - Replace stale context.plugins.get('email') API with getEmailService().send() - Update Magic Link note to not require Resend specifically Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add Why Better Auth rationale to core auth plugin docs (#935) Explains edge compatibility, TypeScript-first design, D1/SQLite support, pluggable auth strategies, and security posture. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): rewrite analytics plugin page to reflect actual implementation (#938) Removes fictional endpoints (/api/analytics/*), nonexistent config options, fake DB tables, and unimplemented features (auto page views, sessions, bounce rates, realtime, reports UI). Documents the real API (/api/events), correct response shapes, admin dashboard data source (system_logs), hook limitations, and document-model storage. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): remove seed data plugin from core plugins page (#937) Plugin was removed; clean up all references from sections nav, feature grid, and plugin section. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): update magic link auth to reflect Better Auth integration (#939) Replace docs that described the old custom magic-link-auth plugin with accurate setup and API docs for the Better Auth native magicLink plugin. - Correct endpoint: POST /auth/sign-in/magic-link (was /auth/magic-link/request) - Correct response: { status: true } (was { message: "..." }) - Remove plugin activation steps — always active when email is configured - Remove magic_links table schema — Better Auth uses auth_verification - Add BETTER_AUTH_SECRET prerequisite and Resend email plugin setup steps - Add Better Auth client (magicLinkClient) usage examples - Document extendBetterAuth for customizing expiry - Update authentication/page.mdx magic link section to match Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(menu-plugin): data-driven admin sidebar navigation Replaces hardcoded sidebar nav with document-model-backed menu items. Plugins and admins can contribute, reorder, and toggle nav entries without code changes; four system items (Content, Collections, Users, Settings) are seeded idempotently on first boot. Key pieces: - `core-menu` plugin: registers `menu_item` document type with q_* generated columns, seeds system items via `upsertSystemItem`, reconciles plugin-declared menu entries via `reconcileMenuFromPlugins` - `menu-repository.ts`: CRUD layer (raw D1 SQL, tenant-scoped, R1/R3 compliant) - `menu.ts` middleware: post-response HTML block replacement swaps `<!-- ADMIN_SIDEBAR_NAV_ITEMS -->` marker with data-driven items; falls back to hardcoded items if DB unavailable - Admin routes at `/admin/menu`: list, create/edit/delete user items, visibility toggle, move-up/move-down reorder, locked-field enforcement - `menu-icons.ts`: consolidated 30-icon map + `resolveIcon` helper - 7 real-SQLite unit tests (menu-repository.sqlite.test.ts) - E2E spec 82-menu-management.spec.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(menu-plugin): make opt-in, remove from core plugin list menuPlugin is no longer auto-registered. Developers enable it by adding it to config.plugins.register in their app entry point. menuMiddleware is now mounted inside the plugin's register() so it only runs when the plugin is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(plugins): regenerate manifest registry to include menu-plugin Ran generate-plugin-registry.mjs to pick up the new menu-plugin manifest.json. core-menu now appears in the /admin/plugins list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): rename id to 'menu', restore to core, redirect plugin page - Rename plugin id core-menu → menu (no 'core' prefix) - Re-add to corePluginsBeforeCatchAll so /admin/menu routes always mount - Regenerate manifest-registry.ts to pick up renamed id - /admin/plugins/menu redirects to /admin/menu (the actual editor) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): remove duplicate Menu sidebar entry adminMenu: null in manifest stops pluginMenuMiddleware from injecting a second 'Menu' link via <!-- DYNAMIC_PLUGIN_MENU -->. Added 'Menu' as a 5th system seed item (sortOrder 50) so it still appears in the data-driven sidebar managed by menuMiddleware. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): add Menu to hardcoded sidebar fallback, hide orphaned plugin DB records - Add 'Menu' item to baseMenuItems in admin-layout-catalyst so the link always shows even before menu_item docs are seeded (hardcoded fallback) - Filter installed plugins whose IDs are absent from PLUGIN_REGISTRY (e.g. stale 'core-menu' DB records after rename to 'menu') so they no longer cause duplicates on the plugins list page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(menu-plugin): revert to hardcoded sidebar when plugin is disabled menuMiddleware checks the plugins table for the 'menu' plugin status before replacing the sidebar. If status is 'inactive' (disabled via admin UI), the hardcoded sidebar fallback is used instead. Treats missing DB record as active so initial seeding still works. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): query documents table for plugin status, not legacy plugins table PluginService stores plugins in documents (type_id='plugin', slug=id), not the legacy Drizzle-schema plugins table. The previous query always returned null → fell back to true → sidebar never reverted when disabled. Now queries json_extract(data, '$.status') from documents correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): remove Menu from hardcoded sidebar when plugin inactive Menu item was in baseMenuItems unconditionally. Now only appears via the data-driven sidebar (menuMiddleware) when plugin is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): audit and update plugin system documentation (#941) - Remove Seed Data plugin from overview (already gone from core page) - Fix Hot-Swappable claim: routes mount at startup, only status toggles at runtime - Fix Workflow plugin status: inactive/coming soon (routes currently disabled) - Add Two-Phase Boot section explaining sync register vs async onBoot split - Add PluginRegisterMustBeSyncError callout (common authoring mistake) - Add onBoot as lifecycle item #1; remove incorrect ctx.hooks.register from activate - Fix hook catalog: before/after event names, typed .on() API vs deprecated .register() - Add declarative hooks pattern (hooks map on definePlugin) - Fix capabilities: string array + property access (ctx.cap.email), not object array + .get() - Add full capabilities table with required strings and ctx.cap accessors - Add Cron Support section (crons, onCronTick, wrangler.toml) - Add Schema-Driven Settings section (configSchema) - Fix Plugin Context interface to match DefinedPluginContext shape - Fix DB/KV examples: use onBoot not activate, correct ctx.env access - Fix auth core plugin: Better Auth session language, remove JWT service API - Remove stale /admin/auth/tokens admin page reference Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(stats): stats.sonicjs.com — document model migration + dashboard plugin (#942) * chore(stats): reset DB to document model + add migrations + migration script - Create packages/stats/migrations/ with 0001_core + 0002_documents (byte-identical to core) - Fix wrangler.toml: add [[env.production.d1_databases]] binding (was missing, broke --env production commands) - Deploy CI: run d1 migrations apply before wrangler deploy - Add db:migrate:production + db:reset:local npm scripts - Remove unused drizzle-kit/drizzle-orm devDeps (stats uses document model via core) - Add scripts/migrate-legacy-data.mjs: ETL tool to seed backup JSON into documents table (used to migrate 7013 events + 4033 installs from v2 content table) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(stats): add stats dashboard plugin with weekly install funnel - Add packages/stats/src/plugins/stats-dashboard/ plugin - Dashboard at /admin/dashboard shows weekly started vs completed vs failed installs - Summary cards: total events, unique installations, overall completion rate - Weekly table with completion % badge (color-coded) + volume bar - Plugin registers menu item "Dashboard" at order 0 (top of nav) - Export renderAdminLayoutCatalyst + AdminLayoutCatalystData from @sonicjs-cms/core index - Wire statsDashboardPlugin into packages/stats/src/index.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(stats): add setup:db script with admin seed for local dev - Copy seed-local.mjs from my-sonicjs-app (same auth_user schema) - db:reset:local: wipe .wrangler D1 state, apply migrations, seed admin - setup:db alias for db:reset:local (matches my-sonicjs-app convention) - Admin: admin@sonicjs.com / sonicjs! Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(stats): auto-generate BETTER_AUTH_SECRET in setup:db - Add scripts/setup-local.sh: generates .dev.vars with BETTER_AUTH_SECRET if missing, then resets D1 + seeds admin - Without BETTER_AUTH_SECRET, login returns 500 (Better Auth can't init) - Fixes login with admin@sonicjs.com / sonicjs! after npm run setup:db Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(stats): wire stats-dashboard plugin + RBAC seed for local dev - Remove disableAll flag so plugin boots, mounts routes, and injects sidebar menu - Self-inject Dashboard menu item via /admin/* middleware (plugin not in compiled manifest) - Seed RBAC document_types + role-admin + user-roles link so requireRbac passes on first login - Bump core to beta.7, stats dep to "*" for workspace resolution - Regen manifest-registry with versioning + stats-dashboard entries - Regen migrations-bundle timestamp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): add settingsTabContent extension point to plugin SDK Plugins can now self-register a custom settings tab via definePlugin's settingsTabContent field: loadData(db) fetches server-side data, render() produces the HTML embedded in the Settings tab of /admin/plugins/:id. Menu plugin uses this to embed the menu editor inline, replacing the redirect to /admin/menu. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): hide plugin menu items when plugin is deactivated reconcileMenuFromPlugins now queries inactive plugin IDs from the documents table and excludes them from the active set, so deactivateStalePluginRows hides their menu items. reconcile also runs immediately on activate/deactivate so the change takes effect without waiting for next boot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(stats): full usage + churn analytics dashboard * feat(stats): full usage + churn analytics dashboard Expand the stats dashboard from a single funnel table into a complete analytics view backed entirely by the document model (events + installs): KPIs: total installs, active (7d/30d), churned + churn rate, completion rate, install failures, runtime errors. Charts (Chart.js, already bundled in catalyst layout): - Installation growth — new installs/week + cumulative (dual-axis line) - Install funnel — started/completed/failed grouped bars, last 16 weeks - Retention — active(≤7d)/active(8-30d)/churned doughnut - Engagement lifespan — first→last activity buckets - OS / Template doughnuts, Node version horizontal bar Error analysis: - Install failures grouped by errorType + step - Runtime errors (error_occurred) grouped by errorType + version Churn/retention derived in SQL via julianday() on installs first_seen/ last_seen. All queries tenant-scoped + is_published=1 (R3). User-facing strings escaped; chart JSON neutralized against </script> breakout (R8). Events collection gains structured error fields (error_code, error_message, step, sonicjs_version) so future installer failures report actionable detail instead of only a free-text errorType. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(stats): show month-day labels instead of ISO week numbers on charts Replace strftime('%Y-%W') with Monday-keyed date expressions in SQL; add fmtWeekDate() helper to convert YYYY-MM-DD to "Mon D" for chart labels and the weekly breakdown table. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(telemetry): project_snapshot event — collections, plugins, field types Adds a new project_snapshot event emitted on each SonicJS instance cold start after bootstrap. Captures: collection names + doc counts, active plugin names, field type histogram, total doc count. - core/types/telemetry: add project_snapshot event type - core/services/telemetry-service: add trackProjectSnapshot() method - core/middleware/bootstrap: fire project_snapshot after successful boot (fire-and-forget, never blocks; stable installation_id via KV) - stats/collections/events: add project_snapshot + error_occurred to enum - stats/dashboard: 3 new charts (top collections, active plugins, field types) + detail table; shown only when snapshot data is present Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: release v3.0.0-beta.8 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): fix CSRF and visible value on visibility toggle forms - form.submit() doesn't fire submit event so CSRF auto-injection in catalyst layout never ran; switch to requestSubmit() which fires it - renderMenuSettingsContent runs in v2 layout (no CSRF script) so add inline CSRF injection script scoped to that embedded content - fix visible checkbox value from '1' to 'true' to match route check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.8 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(stats): expand project_snapshot charts — version dist, doc volume, always-visible section Always show "What People Build" section (amber banner when no data yet). Add 2 new charts: SonicJS version distribution (doughnut) + content volume distribution by bucket (bar). Add reporting-installations KPI counter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(menu-plugin): show plugin enabled/disabled status in menu manager list Add fetchPluginStatuses() to query plugin status from documents table. Plugin-sourced menu items now show an 'enabled' or 'disabled' badge alongside the source badge in both the standalone /admin/menu page and the embedded settings tab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): fix plugin status lookup in menu list Replace IN-clause query (which required exact pluginId→slug match) with a query that fetches all inactive plugin slugs, then checks membership. Avoids binding issues and ID mismatch edge cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(create-sonicjs): flush pending telemetry requests before process exit shutdown() was a no-op so in-flight fetch() calls were abandoned when the CLI process exited. Now pending requests are tracked and awaited (max 3s) in shutdown(), ensuring installation events reach stats.sonicjs.com. Bumps create-sonicjs to 3.0.0-beta.9. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): use same plugin status query as plugins list page Was filtering WHERE status='inactive' which missed plugins whose status field is NULL. Now fetches all plugin docs and reads status directly, matching PluginService.getAllPlugins() logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.9 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update README files to reflect v3 current state (#945) - Fix version references: 2.0.0-alpha.1 → 3.0.0-beta.9 - Fix auth description: JWT → Better Auth (sessions + RBAC) - Replace stale Quick Start config (deprecated autoSync/autoLoad) with registerCollections() + plugins.register pattern - Remove syncCollections export (removed) and collections DB table export (never exists on greenfield) - Fix plugin example: onActivate → activate(context: PluginContext) - Root README: replace raw SQL INSERT INTO collections block with TypeScript CollectionConfig pattern - Root README: fix plugin hooks example to use correct Plugin interface - Root README: remove drizzle/ from project structure (directory does not exist) - Root README: fix GitHub links (sonicjs-ai → sonicjs) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(admin): show branch label in sidebar on localhost (#946) Adds a colored pill badge below the logo in the admin sidebar that displays the short branch name (after last slash) automatically when running on localhost. Off in production — no env var needed by default. - Template: module-level setBranchLabel() + djb2-hashed color palette - Bootstrap: auto-detects localhost via Host header + reads GIT_BRANCH - Dev script: injects --var GIT_BRANCH:<current-branch> into wrangler dev To enable: run `npm run dev` (branch auto-detected); no .dev.vars change needed. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stats): add POST /v1/events telemetry ingestion endpoint CLI posts to /v1/events but route didn't exist (404). Added unauthenticated handler that writes directly to D1 bypassing ACL. Also fixes wrangler.toml: add [[env.production.d1_databases]] (not inherited from top-level) and update core dep from ^2.3.4 to ^3.0.0-beta.9. Also fixes pre-existing ESLint naming-convention error in otp-login-plugin (is_active destructure rename → _isActive). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.10 - fix(stats): add POST /v1/events telemetry endpoint (was 404) - fix(media): export MEDIA_QUERYABLE for test visibility - feat(admin): branch label in sidebar on localhost - docs(www): changelog + homepage updated for beta.10 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(admin): sidebar plugin links now appear on all admin pages pluginMenuMiddleware was using activeMenuItems (from manifest registry) for HTML injection, ignoring items dynamically prepended to pluginMenuItems by plugins like stats-dashboard that self-inject via register(app). Core admin pages don't pass dynamicMenuItems to templates, so they emit the DYNAMIC_PLUGIN_MENU marker — but since activeMenuItems was empty for external plugins, injection was skipped and the marker stayed as a comment. Fix: read c.get('pluginMenuItems') post-next for HTML injection instead of activeMenuItems, so any pre-next middleware additions are included. chore: release v3.0.0-beta.11 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cors): apply CORS middleware globally so /auth/* routes work from Astro/frontends Previously CORS was scoped to /v1/* only — cross-origin POST /auth/login always hit a CORS wall because the auth routes had no CORS headers at all. - Move cors() middleware to global scope in app.ts (covers /auth/*, /v1/*, admin) - Add credentials: true so session cookie flows back to the browser - Remove now-redundant cors() from api.ts - Default CORS_ORIGINS in wrangler.toml templates now includes localhost:4321 (Astro default) - Fix forms plugin crash (no such table: forms) — return graceful empty list / 404 instead of 500 - Add E2E spec 82 for cross-origin auth header verification - Docs: rewrite CORS section in authentication.md, add CORS troubleshooting to getting-started.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.12 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.12 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): clean up duplicate beta.12 entries from announce script; bump manifest version Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(auth): return JWT token from POST /auth/login Login delegated to Better Auth for session cookies but never minted a JWT — API callers got { user } with no token field. Register already returned { user, token }; login now matches. After successful BA sign-in, generate a JWT via AuthManager.generateToken and return it in the response body + set auth_token cookie (same pattern as /auth/register). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(admin): add interactive JSON viewer for json-type fields json fields previously rendered [object Object] in the edit form. Edit mode now shows a collapsible tree (json-formatter-js, CDN, 3.5KB gz) with a toggle to switch to a raw textarea for editing. Invalid JSON is caught inline before the viewer refreshes. View-only (disabled) mode shows the tree without the edit toggle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stats): add error logging to /v1/events, health endpoint, fix type errors - Log insert failures to CF console instead of swallowing silently - Add /health endpoint to verify worker liveness + D1 connectivity - Cast statsDashboardPlugin to any (DefinedPlugin/Plugin dist/src skew) - Use (c as any) for pluginMenuItems context access (plain Hono typing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stats): rename health endpoint to /stats-health (avoid core/v1 conflicts) /health is taken by core's SonicJS status endpoint. /v1/* is taken by core API routes. /stats-health is uncontested and works on the workers.dev URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stats): deploy to correct worker — sonicjs-stats not sonicjs-stats-production stats.sonicjs.com is bound to the default sonicjs-stats worker. All previous CI deploys targeted --env production (sonicjs-stats-production) which is a separate worker not wired to the custom domain. - Remove --env production from deploy + migrations commands in CI - Add comment to wrangler.toml documenting the correct deploy target Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(release): never add beta dist-tag by default; keep beta=latest Add critical rules to release engineer agent: never publish with --tag beta unless explicitly asked, and always sync beta dist-tag to match latest after every publish. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(stats): sonicjs_version from compiled constant; fix json_each on serialized arrays; add country breakdown - bootstrap.ts: replace c.env.SONICJS_VERSION (missing env var → 'unknown') with imported SONICJS_VERSION constant from utils/version (inlined at build time) - dashboard SQL: wrap json_extract with json() for active_plugins, collection_counts, field_type_histogram — trackProjectSnapshot serializes these as JSON strings; json_each needs json() to parse the string into an array/object first - /v1/events: capture CF-IPCountry header (2-letter code, no PII) into event data - dashboard: add Country chart (horizontal bar, unique installs by country) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(auth): support configurable post-login redirect via ?redirect= param GET /auth/login and POST /auth/login/form now accept a ?redirect=<path> query param. After successful login the browser navigates to that path instead of the hardcoded /admin/content fallback. External URLs are silently ignored (open-redirect guard: value must start with /). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: sync package-lock and generated files after build Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(auth): deny-by-default public access, add CollectionConfig.access, rewrite auth docs - Collections no longer grant public read by default — matches Strapi/Payload secure-by-default posture. Opt in via `access: { public: ['read'] }`. - Add `access` property to CollectionConfig (Payload-style per-collection ACL). - Rename "Access portal" → "Admin Panel" in RBAC UI. - Seed Authenticated + Public RBAC roles (Strapi parity). - Rewrite docs/authentication.md for Better Auth + document ACL. - Update docs/api-reference.md: cookie-based auth, remove legacy JWT examples. - Update docs/collections-config.md: document access property. - Rewrite docs/content-management.md for document model. - Add 2 unit tests for deny-by-default + access merge. - Add E2E spec for Admin Panel naming + seeded roles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: renumber E2E spec to 85 (avoid collision with main's 83-* specs) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): fix plugin status badges, edit 404, and sidebar coverage - fetchPluginStatuses: default missing plugins to inactive (not active) - admin-menu-list template: remove /edit suffix from edit link (404 fix) - app.ts: register menuMiddleware before admin routes (Hono ordering fix) - menu-plugin/index.ts: remove duplicate menuMiddleware registration - menu.ts: replace all marker pairs via regex (fixes mobile sidebar) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(menu-plugin): add Plugins as default system menu item Adds /admin/plugins link (sortOrder 50) before Menu (now 60) so the Plugins page is always present in the DB-driven sidebar by default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): add POST /:id/delete route for delete form HTML forms can't send DELETE method; the template POSTs to /:id/delete but only DELETE /:id existed. Adds the matching POST route. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): use collection icon for Plugins menu item Matches the icon used in the hardcoded sidebar fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(menu-plugin): clicking menu row navigates to edit page Adds onclick to <tr> that navigates to /admin/menu/:id, skipping clicks on interactive elements (buttons, links, inputs, forms). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): default to off when not yet activated Fresh install has no plugin document in DB. Previously treated as active (showing DB-driven sidebar on first boot). Now defaults to inactive so the hardcoded sidebar shows until user explicitly activates the plugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): hide plugin menu items when plugin is deactivated DB-driven sidebar now filters plugin-sourced items by active status, matching the hardcoded sidebar's behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): seed plugin items visible=false when plugin inactive - Revert middleware plugin-status filter; visible flag drives sidebar - Replace fetchInactivePluginIds with fetchActivePluginIds; only explicitly active plugins get visible=true on first insert - deactivateStalePluginRows now uses activeIds (not byPluginId keys) so inactive plugins are hidden even if still in the registry - Also sync visible column alongside data->$.visible on deactivate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(auth): API keys plugin + close auth test gaps vs Payload Audited Payload's auth/login/rbac test suite against ours and closed the gaps — both missing tests on existing behavior and one genuine feature hole. API keys (new api-keys-plugin) - better-auth 1.6.x ships no apiKey plugin, so add programmatic per-user keys on the document model (no new table). Plaintext sk_ secret shown once; only its SHA-256 hash stored (q_apikey_hash). Tenant-scoped, expiry + revoke (hard-erase), ownership-guarded. - Resolve x-api-key / Authorization: Bearer sk_ in the app-wide auth chain (core-wired in app.ts after the BA session middleware), so existing requireAuth/requireRole guards work unchanged for headless clients. - Plugin owns the management surface: admin manage UI + JSON API at /admin/plugins/api-keys, settings (maxKeysPerUser, defaultExpiryDays), sidebar menu. Mirrors the security-audit-plugin split. Test-gap closures - brute-force-detector: lockout detector was live in the login path with zero coverage — add 14 unit tests (per-IP/email lock, window expiry, TTL unlock, suspicious multi-email, alert threshold). - email validation: Zod .email() accepted trailing-hyphen domains (user@example-.com); tighten with a refine + edge-case tests. - session lifecycle e2e: logout destroys the session; logging out one session leaves a concurrent session alive (Payload parity). Verification: type-check clean; 62 unit/integration tests; 10 e2e (86-api-key-auth, 87-session-lifecycle) green against a real wrangler server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(menu-plugin): alphabetize parent dropdown in menu form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): fix Save Changes on menu edit form HTMX hx-put was silently failing (redirect + hx-target="body" race). Switch edit form to plain POST /admin/menu/:id/update. Extract shared handler so PUT /:id (API) and POST /:id/update (HTML form) share logic. Also fix visible checkbox: unchecked = absent from FormData = false. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): allow reparenting system/plugin menu items parent is an organizational choice, not content — admins should always be able to move items under a different parent. Remove parent from lockedFields in defaults, reconcile, and the updateItem guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(menu-plugin): skip locked fields in update handler Form always submits all fields; locked ones (url for system/plugin items) were hitting the updateItem guard. Now skip locked fields when building changes — parent stays always-editable as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): export helloWorldPlugin from core + add hello-world tutorial - Export `helloWorldPlugin` and `createHelloWorldPlugin` from `@sonicjs-cms/core` so users can `import { helloWorldPlugin } from '@sonicjs-cms/core'` instead of hitting a broken relative path - Fix internal import in v4-author-guide §9 test example (was pointing to package-internal path, now uses public API) - Add "Using built-in plugins" section to v4-author-guide with correct import pattern and explicit warning against relative paths - Add `docs/plugins/hello-world-from-scratch.md`: step-by-step tutorial building a plugin with configSchema `recipientName` setting that greets "Hello, Joe!" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(www): upgrade @opennextjs/cloudflare to 1.20.0 for Next.js 16 support 1.19.11 failed to bundle next-server.js under Next.js 16.2.9. 1.20.0 explicitly adds >=16.2.6 to its peer dep range and resolves the esbuild bundling errors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.15 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.15 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): apply release:announce auto-updates for v3.0.0-beta.15 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): fix plugin docs to match actual v3 API Fix multiple inaccuracies in the plugin guide and architecture deep-dive discovered by verifying code against the live codebase: - configSchema is a plain ConfigSchema record (type/label/default), not Zod - Hook names corrected to typed catalog: content:after:create/update/delete/publish, auth:registration:completed; legacy bus hooks (auth:login, auth:logout, request:start) accessed via (ctx.raw as any)?.hooks.register() - Remove ctx.logger (doesn't exist), ctx.documentTypeRegistry, ctx.documentService, ctx.getConfig() — none are on DefinedPluginContext - D1 access in onBoot is ctx.env?.DB, not ctx.db - c.get('documentRepository') and c.get('pluginConfig') never set on context; replaced with actual D1 patterns - Fix registerPlugins arg order: (app, plugins, hostContext) not (plugins, app, ctx) - Add deprecated alias table so readers know content:save/content:publish still work but emit warnings Add tests/e2e/88-plugin-system.spec.ts (8 tests) verifying plugin system endpoints, auth enforcement, hook paths, and configSchema form — all green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(admin): persist plugin detail tab selection via URL hash Tab clicks update window.location.hash; page load and hashchange restore the active tab so refreshing lands on the same tab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): hello-cruel-world demo plugin + nav active-only fix - Add hello-cruel-world demo plugin with extensive comments explaining every v3 extension point: routes, menu, hooks, configSchema, lifecycle, onBoot self-registration via PluginServiceClass.ensurePlugin() - Plugin self-registers in DB on boot so it appears in /admin/plugins - Routes at /hello-cruel-world/* (not /api/*) — explains catch-all ordering - Info tab shows description + auto-rendered clickable route list from plugin.settings._routes (merged into DB settings on each boot) - Fix admin-plugin-settings.template.ts settings form to skip _-prefixed keys so _routes metadata doesn't render as an editable input field - Add npm run killall to root package.json (pkill workerd/wrangler) - Fix pluginMenuMiddleware to only show active plugins in sidebar nav: - Singleton (setPluginMenu) now populated with user plugins only, excluding any plugin that also ships a manifest.json (e.g. redirects) - Core/registry plugins continue to use manifest + DB active-status gate - User plugins without a manifest always appear (always mounted) - Add globe-alt icon to plugin-menu-singleton MENU_ICON_MAP - 6 E2E tests in tests/e2e/89-hello-cruel-world-plugin.spec.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): enhance hello-cruel-world plugin UX and settings tab visibility - Add defaultName configSchema setting; base route greets it when no :name in URL - Seed all configSchema defaults in onBoot so Settings tab appears on fresh install - Use shared mutable pluginOptions so saved settings reach route handlers at request time - Reorder plugin settings tabs: Information → Settings → Activity Log; default to Info - Hide Settings tab when plugin has only _-prefixed metadata keys (no user-configurable settings) - Fix tab JS: VALID_TABS conditional on hasUserSettings; null-guard missing tab elements - Fix View Page E2E selector to use getByRole instead of href (avoids sidebar collision) - Admin page shows Current Config panel with greeting + defaultName + live preview link Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plugins): rename hello-cruel-world → example plugin + collection - Plugin id/name/routes: hello-cruel-world → example - Collection type id: hello-cruel-world-moods → example - Manage Moods links to /admin/content?model=example&page=1 - Info tab route paths now clickable for all GET routes (auth or not) - No local E2E runs — CI validates Playwright specs - CLAUDE.md updated: skip local E2E, unit+integration only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugins): preserve _routes/_adminPath when saving configSchema settings configSchema form POST was calling updatePluginSettings(pluginId, parsed) with only the form fields, overwriting non-schema keys like _routes and _adminPath that plugins store for the Info tab. Now merges parsed values into existing settings so those keys survive a Settings tab save. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: regenerate migrations bundle timestamp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(create-app): ship example plugin with new installs Adds the example plugin to the starter template so every new SonicJS project bootstrapped via create-sonicjs-app includes a working demo of the plugin system out of the box. Ships: public API routes, admin page, moods collection, hook subscriptions, configSchema settings form, and self-registration via onBoot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.16 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.16 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): fix announce script duplicate entries for v3.0.0-beta.16 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add example plugin docs + blog post walkthrough - plugins/page.mdx: add Example Plugin under Core Plugins section - plugins/development/page.mdx: callout + reference section for example plugin - blog: building-your-first-sonicjs-plugin tutorial covering all extension points Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add example plugin dedicated page + core plugins entry - New /plugins/example page: full reference covering file structure, two-phase boot, /example/* routing rationale, factory pattern, configSchema settings, moods collection, hooks, data seeding, customization, and removal - plugins/core/page.mdx: Example Plugin section in feature grid, sections array, and body with key patterns table + removal snippet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): wire cross-links between example plugin pages + blog post - plugins/page.mdx: add buttons → /plugins/example + blog post - plugins/development/page.mdx: add buttons → /plugins/example + blog post - blog post: replace plain /plugins/development link with /plugins/example first Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(www): correct blog post URLs from /content/blog/tutorials/... to /blog/... Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugins): ensure document_type row exists before seeding in onBoot Root cause: plugin wirePlugins() has no bootstrapComplete guard, so a static-asset request (favicon, etc.) can fire onBoot before bootstrapMiddleware has run autoRegisterCollectionDocumentTypes. D1 local enforces the FK on documents.type_id — the seed INSERT fails with SQLITE_CONSTRAINT_FOREIGNKEY, gets swallowed by the catch, and the once-guard means it never retries. Fix: INSERT OR IGNORE the example document_type row inside the seed try-block before the COUNT check. Idempotent when the row already exists; races bootstrap safely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(admin): reorder plugin tabs to Settings → Info → Activity Settings tab now defaults as active when a plugin has user-configurable settings. Info and Activity follow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.17 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.17 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugins): prune stale configSchema keys from stored settings (#973) Removing a field from configSchema had no effect on stored settings — the field persisted in the DB blob and kept appearing in the admin UI. Two fixes: 1. POST /admin/plugins/:id/configure: replace the naive spread-merge with schema-aware merge that only carries forward non-schema keys (internal `_`-prefixed keys like `_routes`) from existing settings. Schema-removed fields are pruned on next save. 2. GET /admin/plugins/:id (legacy settings page): filter `enrichedSettings` to schema-defined + `_`-prefixed keys before passing to the template, so stale stored keys never render in the UI. Fixes: #972 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.18 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.18 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): release announcement updates for v3.0.0-beta.18 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugins): example plugin settings not reflected in API output (#975) onBoot populated pluginOptions once at isolate warm-up; admin UI saves updated settings to DB but the in-memory cache was never refreshed, so GET /example always returned the stale boot-time values. Fix: read greeting/defaultName from DB per-request via PluginService.getPlugin in a getPluginSettings() helper, parallel with getRandomMood. The stale options param is removed from createExampleApiRoutes. Also fixes greeting being declared in configSchema but never used in the response (was hardcoded). Now uses ${settings.greeting}. Applies to both my-sonicjs-app and packages/create-app starter template. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(plugins): bump example plugin version to 1.0.1 Reflects the settings-not-saved bug fix shipped in #975. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.19 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.19 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): release announcement updates for v3.0.0-beta.19 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(scripts): add update:plugin script to pull latest plugin files from GitHub Scaffolded plugin files are ejected copies — npm install updates @sonicjs-cms/core but leaves plugin source untouched. This script fetches the latest files from the canonical starter template on GitHub main and overwrites the local copies. Usage: npm run update:plugin # update example plugin npm run update:plugin -- example # explicit npm run update:plugin -- example --dry-run Ships in both my-sonicjs-app and packages/create-app starter template so new projects get the script at scaffold time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.20 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): add v3.0.0-beta.20 to changelog and homepage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): release announcement updates for v3.0.0-beta.20 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(www): document update:plugin script on plugins and example plugin pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugins): preserve _routes when saving settings via legacy tab (#977) POST /:id/settings was overwriting the full settings blob without merging, wiping _routes and _adminPath written by onBoot on every settings save. Now merges with existing settings before writing, matching the /configure handler. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(database-tools): add search and JSON field browser to table viewer (#976) - Search bar filters all columns including JSON data fields via LIKE - Search term persists across pagination, sort, and page-size changes - JSON/long text cells render as clickable links opening a modal viewer - Modal shows syntax-highlighted pretty-printed JSON with copy button - Closes on backdrop click, close button, or Escape key - E2E spec 91 covers search input, URL params, clear, modal open/close Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(media): real total count + media selector search (#888, #890) (#978) - MediaDocumentService.list() now runs a parallel COUNT(*) with the same WHERE filters and returns `total` in MediaListResult - Admin media route uses `total` for totalFiles and fixes hasNextPage (was capped at page size 24 instead of real library total) - Media selector search input: add missing `name="search"` attr so the typed term is actually sent with the HTMX request - Add hx-swap="outerHTML" + hx-select="#media-selector-grid" to prevent the full panel (search box + grid) from nesting inside the grid on each keystroke Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(mcp): add MCP plugin implementation plan for issue #784 (#981) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * test(media): regression tests for #889 upload date display + MCP plugin spec (#982) * docs(mcp): add MCP plugin implementation plan for issue #784 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(media): add regression tests for #889 upload date display + sort Adds 4 unit tests and 1 E2E spec to guard against re-introduction of the 1970-date bug: createFromUpload must produce a valid non-1970 uploadedAt via mediaDocToFile (seconds * 1000), and list() must return newest files first (updated_at DESC). Closes #889 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(api-keys): show new key until user copies, no auto-redirect (#984) Remove 1.5s auto-reload that fired before user could copy the secret. New key row now injected into the DOM immediately. Banner stays visible until user clicks "Copy & dismiss" or the × close button. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(oauth-providers): show Settings tab and render provider fields (#987) - hasUserSettings now returns true when a custom renderer is registered, so Settings tab is visible even with empty/null plugin DB settings - Adds renderOAuthProvidersSettingsContent with GitHub + Google credential fields (clientId, clientSecret, enabled toggle) - Custom saveSettings reconstructs nested providers structure before POST - Registers oauth-providers in pluginSettingsComponents Fixes: Settings tab hidden until user manually saves a dummy entry Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: release v3.0.0-beta.22 🤖 Generated with [Claude Code](https://claude.com/claude-code) * docs(www): add v3.0.0-beta.22 to changelog and homepage 🤖 Generated with [Claude Code](https://claude.com/claude-code) * chore: sync migrations-bundle timestamp after merge Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: upgrade Node.js to 22 for wrangler 4.x compatibility * fix(menu): address code review findings - XSS: reject javascript:/data: URL schemes in create and update handlers - visible flag on create: form.has() instead of !== 'false' (always-true bug) - isMenuPluginActive catch: return false on error, not true - label double-encode: store raw, escape only at render time - move-up/down: filter by same parent before swap (was crossing parent boundaries) - move-up/down: only redirect with 'Reordered' toast when write actually happened - reconcile UPDATE path: also write visible column when re-activating plugin items - toggleVisibility/updateItem: add type_id='menu_item' guard to WHERE clause Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: sync package-lock.json with updated dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9286bb4 commit 482e121

159 files changed

Lines changed: 26289 additions & 16530 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/sonicjs-release-engineer.md

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Release Engineer Agent
22

3+
## Critical Rules (NEVER violate without explicit instruction)
4+
5+
- **ALL releases use `npm run version:beta` (`3.x.x-beta.N``3.x.x-beta.N+1`) until the user explicitly says "we are out of beta" or "release stable".** NEVER use `version:patch`, `version:minor`, or `version:major` for routine releases — those drop the pre-release suffix and publish a stable version.
6+
- **NEVER add `--tag beta` (or alpha/rc) when publishing** — all publishes go to `latest` dist-tag regardless of the version string containing `-beta.N`.
7+
- **The `beta` dist-tag MUST always point to the same version as `latest`.** After any publish, run: `npm dist-tag add @sonicjs-cms/core@<VERSION> beta && npm dist-tag add create-sonicjs@<VERSION> beta`
8+
- Versions may contain `-beta.N` in the version string — that is fine and expected. The dist-tag (`latest`) is what matters for `npm install` behavior.
9+
10+
---
11+
312
You are a specialized agent that manages npm package releases and dependency updates for SonicJS. Your primary responsibilities are:
413

514
1. **Updating npm dependencies** - Keep dependencies current and secure
@@ -110,22 +119,24 @@ npm run build:core
110119

111120
### Step 2: Determine Version Bump Type
112121

113-
Ask the user for the release type if not specified:
114-
- **patch** (2.3.12 → 2.3.13): Bug fixes only
115-
- **minor** (2.3.12 → 2.4.0): New features, backwards compatible
116-
- **major** (2.3.12 → 3.0.0): Breaking changes
122+
**DEFAULT: use `version:beta` for ALL routine releases (bug fixes, features, improvements) until the user explicitly declares "out of beta" or "release stable".**
123+
124+
Only use patch/minor/major when the user explicitly requests a stable release:
125+
- **beta** (3.0.0-beta.17 → 3.0.0-beta.18): DEFAULT — all bug fixes and features during beta period
126+
- **patch** (3.0.0 → 3.0.1): Stable releases only — bug fixes (user must confirm out-of-beta first)
127+
- **minor** (3.0.0 → 3.1.0): Stable releases only — new features (user must confirm out-of-beta first)
128+
- **major** (3.0.0 → 4.0.0): Stable releases only — breaking changes (user must confirm out-of-beta first)
117129

118130
### Step 3: Bump Version
119131

120132
```bash
121-
# For patch release
122-
npm run version:patch
123-
124-
# For minor release
125-
npm run version:minor
133+
# DEFAULT: beta prerelease bump (3.x.x-beta.N → 3.x.x-beta.N+1)
134+
npm run version:beta
126135

127-
# For major release
128-
npm run version:major
136+
# Only for stable releases (user must explicitly request):
137+
# npm run version:patch
138+
# npm run version:minor
139+
# npm run version:major
129140
```
130141

131142
This automatically:
@@ -401,11 +412,11 @@ npm publish --workspace=create-sonicjs --tag rc
401412
## Usage Examples
402413

403414
```
415+
/release-engineer # DEFAULT: publish next beta (X.X.X-beta.N+1) → latest dist-tag
404416
/release-engineer update # Update npm dependencies
405-
/release-engineer patch # Publish patch release
406-
/release-engineer minor # Publish minor release
407-
/release-engineer major # Publish major release
408-
/release-engineer beta # Publish beta pre-release
417+
/release-engineer patch # Publish stable patch (only after user declares out-of-beta)
418+
/release-engineer minor # Publish stable minor (only after user declares out-of-beta)
419+
/release-engineer major # Publish stable major (only after user declares out-of-beta)
409420
/release-engineer status # Check current version and npm status
410421
```
411422

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@
105105
"WebFetch(domain:raw.githubusercontent.com)",
106106
"WebFetch(domain:stackoverflow.com)",
107107
"WebFetch(domain:developer.mozilla.org)",
108-
"mcp__*",
108+
"mcp__memory__*",
109+
"mcp__graphiti__*",
109110
"Bash(vercel:*)",
110111
"Bash(git add:*)",
111112
"Bash(git commit:*)",

.github/workflows/deploy-stats.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Deploy Stats
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- 'packages/stats/**'
9+
- 'packages/core/src/**'
10+
- '.github/workflows/deploy-stats.yml'
11+
workflow_dispatch:
12+
13+
jobs:
14+
deploy:
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 15
17+
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@v4
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
cache: 'npm'
27+
28+
- name: Install dependencies
29+
run: npm ci
30+
31+
- name: Build core package
32+
run: npm run build:core
33+
34+
- name: Type check stats
35+
run: cd packages/stats && npx tsc --noEmit
36+
37+
- name: Apply D1 migrations
38+
# stats.sonicjs.com is bound to the default sonicjs-stats worker (no --env flag)
39+
run: cd packages/stats && npx wrangler d1 migrations apply DB --remote
40+
env:
41+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
42+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
43+
44+
- name: Deploy stats to Cloudflare Workers
45+
# Deploy to default worker (sonicjs-stats) — that is what stats.sonicjs.com is bound to
46+
run: cd packages/stats && npx wrangler deploy
47+
env:
48+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
49+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

.github/workflows/pr-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
- name: Setup Node.js
4242
uses: actions/setup-node@v4
4343
with:
44-
node-version: '20'
44+
node-version: '22'
4545
cache: 'npm'
4646

4747
- name: Install dependencies

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ pids
4444
coverage/
4545
*.lcov
4646

47+
# Claude Code — local settings are per-developer, not shared
48+
.claude/settings.local.json
49+
4750
# IDE
4851
.vscode/settings.json
4952
.idea/

CLAUDE.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Base grants only match `public` + `role`, so the `role` principal is mandatory f
143143
5. **Public exposure**: set `settings.baseGrants.public = ['read']` only if the data is non-PII.
144144
6. **PII**: set `settings.pii = true` and ensure the erase path covers it.
145145
7. **Tests**: add at least one `*.sqlite.test.ts` or `*.integration.test.ts` case. Mock tests prove nothing about SQL (R10).
146-
8. **E2E**: add a Playwright spec (numbered 68+).
146+
8. **E2E**: add a Playwright spec (numbered 68+). Write it; do NOT run it locally — CI validates it.
147147

148148
### Status snapshot (avoid re-investigating)
149149

@@ -166,15 +166,14 @@ Repo indexed by **codegraph**. Bash auto-proxied by **rtk**. Default reply mode
166166

167167
Anti-patterns: `grep -r` across repo when codegraph answers; reading 5+ files to learn a flow; spawning a subagent for a known file; verbose narration.
168168

169-
## E2E Testing is Mandatory
169+
## E2E Testing
170170

171-
Every feature/fix ships with a Playwright spec in `tests/e2e/`. Workflow:
171+
Every feature/fix ships with a Playwright spec in `tests/e2e/`. Write the spec, but **do NOT run E2E tests locally** — they require a running wrangler dev server, consume excessive memory, and are validated by CI on PR. Running them locally is too expensive.
172172

173+
Workflow:
173174
1. Implement
174175
2. Add `tests/e2e/<NN>-<slug>.spec.ts` (NN = next sequential; current floor 68 — R11)
175-
3. `npx playwright test tests/e2e/<NN>-<slug>.spec.ts` (add `--headed` to debug)
176-
4. Fix any failures
177-
5. Commit implementation + tests together
176+
3. Commit implementation + tests together — CI runs E2E
178177

179178
Spec skeleton:
180179

@@ -205,15 +204,10 @@ cd packages/core && npm run type-check
205204
npm test
206205
npm test -- <pattern> # single test by name/path
207206

208-
# E2E (root)
209-
npm run e2e
210-
npx playwright test tests/e2e/68-x.spec.ts # one spec
211-
npx playwright test tests/e2e/68-x.spec.ts --headed # debug
212-
213207
# Local D1 — reset + migrate + seed
214208
cd my-sonicjs-app && npm run setup:db
215209

216-
# Dev server (wrangler :8787)
210+
# Dev server (wrangler :8787) — use only when explicitly needed, not for testing
217211
cd my-sonicjs-app && npm run dev
218212

219213
# Migration bundle regen (after any packages/core/migrations/*.sql edit)

README.md

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,7 @@ sonicjs-ai/
241241
│ # Used for testing the published package
242242
243243
├── www/ # 🌐 Marketing website
244-
├── tests/e2e/ # End-to-end test suites
245-
└── drizzle/ # Database migrations
244+
└── tests/e2e/ # End-to-end test suites
246245
```
247246

248247
### Important Notes
@@ -256,32 +255,52 @@ sonicjs-ai/
256255
## 🔧 Content Management
257256

258257
### Creating Collections
259-
SonicJS uses a dynamic field system. Create collections through the admin interface or define them in the database:
260-
261-
```sql
262-
-- Example: Blog Posts collection with custom fields
263-
INSERT INTO collections (id, name, display_name, description, schema) VALUES (
264-
'blog-posts', 'blog_posts', 'Blog Posts', 'Article content collection',
265-
'{"type":"object","properties":{"title":{"type":"string","required":true}}}'
266-
);
267-
268-
-- Add dynamic fields
269-
INSERT INTO content_fields (collection_id, field_name, field_type, field_label, field_options) VALUES
270-
('blog-posts', 'title', 'text', 'Title', '{"maxLength": 200, "required": true}'),
271-
('blog-posts', 'content', 'richtext', 'Content', '{"toolbar": "full", "height": 400}'),
272-
('blog-posts', 'excerpt', 'text', 'Excerpt', '{"maxLength": 500, "rows": 3}'),
273-
('blog-posts', 'featured_image', 'media', 'Featured Image', '{"accept": "image/*"}'),
274-
('blog-posts', 'publish_date', 'date', 'Publish Date', '{"defaultToday": true}'),
275-
('blog-posts', 'is_featured', 'boolean', 'Featured Post', '{"default": false}');
258+
259+
Collections are TypeScript config objects registered at app startup — no database table required.
260+
261+
```typescript
262+
// src/collections/blog-posts.collection.ts
263+
import type { CollectionConfig } from '@sonicjs-cms/core'
264+
265+
export default {
266+
name: 'blog_post',
267+
displayName: 'Blog Post',
268+
slug: 'blog-posts',
269+
description: 'Article content collection',
270+
271+
schema: {
272+
type: 'object',
273+
properties: {
274+
title: { type: 'string', title: 'Title', required: true, maxLength: 200 },
275+
content: { type: 'lexical', title: 'Content', required: true },
276+
publishedAt: { type: 'datetime', title: 'Published Date' },
277+
},
278+
required: ['title', 'content'],
279+
},
280+
281+
managed: true,
282+
isActive: true,
283+
} satisfies CollectionConfig
284+
```
285+
286+
```typescript
287+
// src/index.ts — register before createSonicJSApp
288+
import { registerCollections, createSonicJSApp } from '@sonicjs-cms/core'
289+
import blogPostsCollection from './collections/blog-posts.collection'
290+
291+
registerCollections([blogPostsCollection])
292+
export default createSonicJSApp({ plugins: { register: [] } })
276293
```
277294

278295
### Field Types
279-
- **text**: Single-line text with validation
280-
- **richtext**: WYSIWYG editor with TinyMCE
296+
- **string**: Single-line text with validation
297+
- **lexical**: Rich text editor
281298
- **number**: Numeric input with min/max constraints
282299
- **boolean**: Checkbox with custom labels
283-
- **date**: Date picker with format options
300+
- **datetime**: Date/time picker
284301
- **select**: Dropdown with single/multi-select
302+
- **slug**: URL slug with auto-generation
303+
- **user**: User reference picker
285304
- **media**: File picker with preview
286305

287306
## 🌐 API Endpoints
@@ -373,17 +392,21 @@ Create plugins for extending SonicJS functionality:
373392

374393
```typescript
375394
// src/plugins/my-plugin/index.ts
376-
import { Plugin } from '@sonicjs-cms/core'
395+
import type { Plugin, PluginContext } from '@sonicjs-cms/core'
377396

378397
export default {
379398
name: 'my-plugin',
380-
hooks: {
381-
'content:beforeCreate': async (content) => {
382-
// Plugin logic here
383-
return content
384-
}
385-
}
386-
} as Plugin
399+
version: '1.0.0',
400+
description: 'My custom plugin',
401+
402+
async activate(context: PluginContext) {
403+
// Runs when the plugin is activated
404+
},
405+
406+
async install(context: PluginContext) {
407+
// Runs once on install — migrations, seed data, etc.
408+
},
409+
} satisfies Plugin
387410
```
388411

389412
## 📄 License
@@ -411,9 +434,9 @@ SonicJS is 100% open source and free forever. If you find it useful, please cons
411434

412435
## 📞 Support
413436

414-
- [GitHub Issues](https://github.com/lane711/sonicjs-ai/issues)
437+
- [GitHub Issues](https://github.com/lane711/sonicjs/issues)
415438
- [Documentation](docs/)
416-
- [Community Discussions](https://github.com/lane711/sonicjs-ai/discussions)
439+
- [Community Discussions](https://github.com/lane711/sonicjs/discussions)
417440

418441
---
419442

0 commit comments

Comments
 (0)