From 0b3c8adbc24e1959a80d97ca62efde6af800fa30 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Wed, 22 Jul 2026 10:20:32 +0200 Subject: [PATCH] Complete experimental UX and safe branch deployments --- .gitignore | 2 + CHANGELOG.md | 7 + README.md | 6 +- TODO.md | 28 + docs/branch-deployments.md | 82 +++ docs/lumi-ui.md | 110 +++- docs/lumi-ux-checkpoint-review.md | 50 ++ docs/lumi-ux-route-manifest.md | 165 ++++++ docs/plugin-ui-guide.md | 104 ++++ docs/recovery-mode.md | 4 + docs/updates.md | 24 +- knowledge/core/lumi-core.md | 40 +- knowledge/plugins/okf.md | 2 +- package-lock.json | 105 +++- package.json | 11 +- playwright.config.js | 40 ++ plugins/lumi_ai/index.js | 12 + plugins/lumi_ai/public/assistant.css | 12 + plugins/lumi_ai/public/improvement-center.css | 5 + plugins/lumi_ai/public/improvement-center.js | 24 +- plugins/lumi_ai/public/settings.css | 13 + plugins/lumi_ai/public/settings.js | 111 ++-- plugins/lumi_ai/views/settings.ejs | 2 +- .../public/settings-modal.css | 5 + plugins/okf/index.js | 26 + plugins/okf/views/admin.ejs | 32 +- plugins/throne_wishlist/public/admin.css | 7 +- scripts/verify-all.js | 1 + scripts/verify-content-library.js | 20 + scripts/verify-destructive-actions.js | 4 +- scripts/verify-overlays.js | 17 +- scripts/verify-release-metadata.js | 19 +- scripts/verify-update-system.js | 63 +- scripts/verify-ux-foundation.js | 89 +++ scripts/verify-webui.js | 12 +- src/main.js | 49 +- src/services/content-library.js | 17 +- src/services/db.js | 4 +- src/services/overlay-routes.js | 83 ++- src/services/repo-update.js | 198 ++++++- src/services/update-manager.js | 45 +- src/services/update-repository.js | 49 ++ src/services/updater.js | 53 +- src/web/public/app.js | 248 ++++---- src/web/public/command-management.js | 161 ++++++ src/web/public/command-policies.js | 189 ++++++ src/web/public/content-library.css | 8 + src/web/public/content-library.js | 13 +- src/web/public/lumi-components.css | 253 ++++++-- src/web/public/lumi-dialog.js | 118 ++++ src/web/public/lumi-interactions.js | 330 +++++++++-- src/web/public/lumi-layout.css | 36 +- src/web/public/lumi-page.js | 34 ++ src/web/public/lumi-request.js | 112 ++++ src/web/public/lumi-state-button.js | 6 +- src/web/public/lumi-tokens.css | 69 ++- src/web/public/navigation-builder.css | 264 +++++++++ src/web/public/navigation-builder.js | 379 ++++++++++++ src/web/public/overlay-chat-dock.css | 2 + src/web/public/overlay-management.css | 157 ++++- src/web/public/overlay-management.js | 240 ++++++-- src/web/public/overlay-preview-window.css | 220 +++++++ src/web/public/overlay-preview-window.js | 281 +++++++++ src/web/public/overlay-renderer.js | 1 + src/web/public/overlay-runtime.css | 9 +- src/web/public/styles.css | 8 +- src/web/public/theme-editor.css | 14 + src/web/public/theme-editor.js | 61 +- src/web/public/update-management.js | 469 +++++++++++++++ src/web/server.js | 457 +++++++++++---- src/web/views/admin-command-policies.ejs | 162 +----- src/web/views/admin-commands.ejs | 126 +--- src/web/views/admin-navigation.ejs | 544 ++++-------------- src/web/views/admin-overlay-detail.ejs | 46 +- src/web/views/admin-overlay-preview.ejs | 41 ++ src/web/views/admin-overlays.ejs | 4 +- src/web/views/admin-resources.ejs | 12 +- src/web/views/admin-settings.ejs | 9 +- src/web/views/admin-theme.ejs | 11 +- src/web/views/admin-updates.ejs | 206 +++---- src/web/views/commands.ejs | 14 +- src/web/views/localhost-login.ejs | 8 +- src/web/views/overlay-render.ejs | 4 +- src/web/views/partials/layout-bottom.ejs | 10 +- src/web/views/partials/layout-top.ejs | 13 +- tests/ui/fixture-server.js | 35 ++ tests/ui/lumi-ux.spec.js | 506 ++++++++++++++++ update-manifest.json | 8 +- 88 files changed, 6224 insertions(+), 1426 deletions(-) create mode 100644 docs/branch-deployments.md create mode 100644 docs/lumi-ux-checkpoint-review.md create mode 100644 docs/lumi-ux-route-manifest.md create mode 100644 docs/plugin-ui-guide.md create mode 100644 playwright.config.js create mode 100644 scripts/verify-ux-foundation.js create mode 100644 src/web/public/command-management.js create mode 100644 src/web/public/command-policies.js create mode 100644 src/web/public/lumi-dialog.js create mode 100644 src/web/public/lumi-page.js create mode 100644 src/web/public/lumi-request.js create mode 100644 src/web/public/navigation-builder.css create mode 100644 src/web/public/navigation-builder.js create mode 100644 src/web/public/overlay-preview-window.css create mode 100644 src/web/public/overlay-preview-window.js create mode 100644 src/web/public/update-management.js create mode 100644 src/web/views/admin-overlay-preview.ejs create mode 100644 tests/ui/fixture-server.js create mode 100644 tests/ui/lumi-ux.spec.js diff --git a/.gitignore b/.gitignore index 8c12c05..68a7e6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules/ +test-results/ +playwright-report/ /data/ /updates/ plugins/*/data/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 7166f0a..e009b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Lumi changelog +## 0.2.25 + +- Added verified, snapshot-backed production deployment switching between `main` and exact `experimental-*` branches, including bundled plugin code, preserved plugin data, automatic failed-apply restoration, and graceful wrapper restarts. +- Added a guarded one-time bootstrap that completes bundled-plugin synchronization after an older stable updater installs this experimental core; metadata checks alone cannot trigger deployment. +- Completed the experimental DesignMotionHQ UX foundation and representative settings, navigation, theme, command, updates, resources, and OBS overlay workflows with a five-viewport browser/accessibility matrix. +- Fixed overlay editor scrolling and viewport containment, interactive pop-out preview editing, and persistent resource storage-limit saves with immediate page reconciliation. + ## 0.2.24 - Fixed resource locking and deletion by sending the shared timed-confirmation token directly with AJAX actions, and made deletion reliable on Windows and network-backed storage. diff --git a/README.md b/README.md index 7a1e4a3..3099904 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,11 @@ so dependency repair does not rely on spawning a command script directly. Recovery mode can be started with `LUMI_SAFE_MODE=1 npm run run`, `node run.js --safe-mode`, or `data/recovery/safe-mode.flag`. See [`docs/updates.md`](docs/updates.md) and -[`docs/recovery-mode.md`](docs/recovery-mode.md). +[`docs/recovery-mode.md`](docs/recovery-mode.md). Production can also be moved +between `main` and an exact `experimental-*` branch from **Admin > Updates** +with a verified pre-deployment snapshot, graceful restart, one-click return to +`main`, and emergency restore. See +[`docs/branch-deployments.md`](docs/branch-deployments.md). ## Twitch bot diff --git a/TODO.md b/TODO.md index 50b34a3..d9df07f 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,34 @@ This file tracks larger Lumi work that cannot safely be completed in one pass. Keep pending work under the relevant category and move completed items to the Done section with a short note. +## Remaining DesignMotionHQ UX work — experimental-ux checkpoint (2026-07-22) + +The shared UX foundation and representative settings, navigation, theme, command, +updates, resources, and overlay workflows are implemented. Continue applying the +same hierarchy, progressive disclosure, motion, state feedback, accessibility, +and context-preservation principles globally in this order: + +- Migrate and browser-accept the remaining core route groups: Feedback; + Logs/Diagnostics; Users/Privileges/Pages/Plugins; and their empty, loading, + partial, error, offline, permission-limited, disabled, and read-only states. +- Migrate remaining plugin-owned routes to the shared request, lifecycle, dialog, + field-state, responsive-list, and no-forced-reload contracts. +- Add a plugin-enabled authenticated browser fixture so Lumi AI polling, + Improvement Center live updates, and plugin settings are exercised in the + Playwright viewport matrix rather than only source/focused verification. +- Browser-test the complete built-in/custom-theme contrast matrix, including + default badges, disabled controls, preview surfaces, alerts, and plugin pages. +- Complete manual keyboard and screen-reader acceptance for every destructive + and entity-edit dialog, including focus restoration and timed confirmation. +- Continue replacing broad legacy selectors and route-specific controls with + explicit shared components; finish wording, density, helper-text, and advanced + disclosure cleanup on lower-priority plugin and diagnostics surfaces. +- Finish homepage hero/embed usability: non-technical configuration copy, + unavailable/fallback explanations, Twitch parent-domain guidance, YouTube live + failure states, and external iframe refusal handling. +- Re-run the checkpoint review after each route group and remove completed items + from `docs/lumi-ux-checkpoint-review.md` and the route manifest P1 list. + ## Command policy and stream controls Implemented on 2026-07-19: centralized command groups and per-command overrides diff --git a/docs/branch-deployments.md b/docs/branch-deployments.md new file mode 100644 index 0000000..424b2a2 --- /dev/null +++ b/docs/branch-deployments.md @@ -0,0 +1,82 @@ +# Safe production branch deployments + +Lumi can deploy the stable `main` branch or an exact published +`experimental-*` branch from **Admin > Updates > Running code branch**. This is +a managed deployment; Lumi does not change the live installation's Git +worktree. It fetches the selected branch into `data/update-cache/repo`, verifies +the core and bundled-plugin entry files, snapshots the running core and plugin +code plus the database, replaces managed application files, records the deployed +branch and commit, and then restarts. + +Local data remains in place during a normal branch switch, including settings, +databases, plugin data, uploads, logs, secrets, AI models/runtimes, community +knowledge, corrections, and `node_modules`. Bundled plugin code follows the +selected branch; local-only plugins absent from that branch stay installed. The +update-aware wrapper repairs changed runtime dependencies before it starts the +new branch. + +## Prerequisites + +1. Start production through `npm start`, `npm run run`, or a service that runs + `node run.js`. The wrapper recognizes Lumi's restart exit code and starts the + deployed code after dependency verification. +2. Ensure the configured repository credential can fetch branches without an + interactive password prompt. +3. Push the experimental branch before opening the deployment control. Only + `main` and remote branches named `experimental-*` are accepted. +4. Keep snapshot retention high enough to retain at least the latest core + backup. The default keeps five per target for 30 days. + +## Test an experimental branch + +To enter from stable `0.2.24`, open **Admin > Settings**, select +**Experimental** as the update branch, save, and use **Update from repository**. +The older updater installs experimental core `0.2.25`; on its first startup the +new guarded bootstrap confirms that an actual core update completed, snapshots +the installation, synchronizes bundled plugin code, and restarts a second time. +Merely checking metadata cannot trigger this bootstrap. Stable `0.2.24` predates +the exact-branch picker, so it selects the newest published `experimental-*` +branch. The same guarded entry path is available again after a return to stable. +While experimental code is running, use the exact branch controls below to move +between `main` and any published experimental branch. + +1. Open **Admin > Updates** and find **Running code branch**. +2. Confirm the page reports the expected running branch and commit. +3. Choose the exact experimental branch and select **Deploy selected branch**. +4. Review the confirmation. Keep the page open while Lumi creates the backup, + deploys, restarts gracefully, and reconnects. +5. Verify the branch and commit after reconnection, then test the affected + production workflows. + +The same branch can be selected again for a verified repair deployment. + +## Return to stable main + +Use **Return safely to main**. Lumi first snapshots the running experimental +code, then deploys the current remote `main` while preserving live data. This is +the preferred normal return path because it does not intentionally rewind the +database or discard data written during the experiment. + +Experimental database migrations must remain backward-compatible with `main` +when this path is expected to work. Mark experimental update metadata with +`rollback_safe: false` and document the migration when a change cannot safely be +read by the stable code. + +## Emergency restore + +Use **Emergency restore previous backup** only when the deployed branch cannot +run correctly enough to return through the normal branch control. It restores +both the previous core code and the database captured before deployment, so +database changes written after that snapshot may be lost. + +If the normal WebUI is unavailable, start recovery mode with +`LUMI_SAFE_MODE=1 npm run run`, `node run.js --safe-mode`, or the +`data/recovery/safe-mode.flag` marker. Recovery markers and deployment state are +also visible through the production diagnostics `update_state` check. + +## Failure behavior + +Fetching and target verification happen before live files are touched. Once +replacement starts, failures trigger an automatic restore from the new +snapshot. If automatic restore also fails, Lumi retains the recovery marker and +snapshot for safe mode instead of silently continuing with a partial install. diff --git a/docs/lumi-ui.md b/docs/lumi-ui.md index e482039..860476c 100644 --- a/docs/lumi-ui.md +++ b/docs/lumi-ui.md @@ -19,10 +19,24 @@ from visual tokens and reusable components. - `src/web/public/lumi-interactions.js`: progressive interaction layer for server-sent events, no-auto-refresh notices, dirty settings save bars, expandable settings containers, refresh prompts, and soft navigation. +- `src/web/public/lumi-request.js`: shared enhanced request parser and the + `{ ok, message, fieldErrors, data, patch, redirect, reloadRequired }` + response contract. +- `src/web/public/lumi-page.js`: opt-in page mount/unmount lifecycle. It aborts + page-owned listeners and requests before running the registered teardown. +- `src/web/public/lumi-dialog.js`: shared focus trap, background isolation, + Escape policy, scroll lock, and opener restoration for modal surfaces. +- `src/web/public/command-management.js` and `command-policies.js`: lifecycle-owned + custom-command editing, targeted collection refresh, policy search, inherited + summaries, and scoped save reconciliation. +- `src/web/public/update-management.js`: lifecycle-owned repository checks, + progress subscriptions, recovery actions, snapshot-policy validation, and + restart reconnection without document reloads. - `src/services/web-events.js`: small role-aware Server-Sent Events bus exposed at `GET /api/events` for authenticated users. -- `src/web/public/styles.css`: legacy and feature-specific styles that still use - the shared tokens. New general-purpose styling belongs in the Lumi UI files. +- `src/web/public/styles.css`: layered legacy/base styles kept during migration. + It must not become a second component library. New general-purpose styling + belongs in the Lumi UI files. - `src/web/views/partials/page-header.ejs`: standard page title and description. - `src/web/views/partials/theme-vars.ejs`: safe active-theme variables for shell and standalone pages. @@ -34,6 +48,32 @@ Use `lumi-stack`, `lumi-cluster`, `lumi-split`, `lumi-grid`, `page-header`, and `status-indicator` before adding one-off layout rules. Preserve existing IDs, field names, data attributes, and JavaScript hooks when restyling a page. +## Product-wide UX principles + +Lumi follows the interaction principles catalogued by DesignMotionHQ across the +entire product, not only on individual redesigns. Treat these as review criteria +for core pages, plugins, overlays, dialogs, and future features: + +- Use proximity and whitespace to make related controls read as a group before + adding borders or extra containers. Keep within-group spacing tighter than the + space between groups. +- Match surface hierarchy to the task. Routine, non-blocking work belongs in the + page, an anchored popover, or an edge drawer that preserves context. Reserve a + modal for a decision or task that genuinely blocks the rest of the interface. +- Keep disclosure predictable. Use native buttons and details/summary controls, + expose their state to assistive technology, and avoid layout jumps that move + the control a person just used. +- Give every interactive element a visible focus state. DOM order must match the + visual reading order, and closing a temporary surface returns focus to its + opener. +- Motion explains spatial or state changes and never delays routine work. Honor + reduced-motion preferences and avoid decorative animation that competes with + status, content, or controls. + +New reusable behavior belongs in the shared Lumi UI layer. A feature stylesheet +may compose those patterns for its own layout, but should not create a competing +interaction model. + ## Interaction Rules Pages should not self-refresh for state or progress changes. Core connection @@ -41,6 +81,10 @@ recovery now displays a notice instead of calling `window.location.reload()`. Server-originated events use `GET /api/events` with explicit event names such as `server:status`, `server:warning`, `ai:model_status`, and `data:new_available`. Admin-only events must be published with `{ role: "admin" }`. +Browser consumers subscribe through `window.LumiEvents.subscribe(...)` so a page +uses one shared event connection. The connection is closed before full navigation +and on `pagehide`, preventing long-lived streams from starving the next document +request. Public pages do not connect to the authenticated event endpoint. List/data updates should announce that new data exists and show a refresh prompt. The shared refresh prompt uses a 3-second cooldown before another refresh can be @@ -52,6 +96,18 @@ tracks original values, marks changed fields with theme-aware unsaved styling, shows a top Save changes bar, warns before accidental navigation, and clears markers only after successful saves. +Settings saves use `LumiRequest`, update each successful form's canonical +snapshot, keep failed forms dirty, map returned field errors to their controls, +and return focus to the last edited field. The save bar stays stable while +scrolling and provides both Save and Revert. A successful ordinary save never +reloads the page. Handlers may set `reloadRequired: true` only when a runtime +restart or equivalent context replacement genuinely requires it. + +Entity forms that can safely patch their current region use +`data-lumi-enhanced-form`. They retain an ordinary method/action fallback, +receive local pending and result text, preserve input after failure, and emit +`lumi:form-saved` with the normalized response for feature-owned reconciliation. + Buttons should use `partials/state-button.ejs` for submit, loading, success, or error states. Single-state and multi-state buttons share the same Lumi button tokens. Hidden states stay measurable with `data-state-hidden`, so the button @@ -71,14 +127,38 @@ known placeholders such as `{gifter_username}`, `{item_name}`, `{creator_username}`, and `{amount_display}` render with plausible sample values without changing the saved template. -Soft navigation progressively enhances same-origin links by replacing -`main.content`, updating history, and fading content in place. If a fetch fails, -JavaScript is unavailable, or unsaved settings are present, navigation falls back -to normal browser behavior. +Soft navigation is deliberately opt-in with `data-lumi-soft-nav`. A route may +opt in only after its page code uses `LumiPage.register(...)` and its feature +assets are already available. The lifecycle unmounts the previous page before +replacing `main.content`, then updates title, history, active navigation, scroll, +and heading focus. Arbitrary inline scripts are never re-executed. All other +links retain normal browser navigation. Sidebar navigation sections behave as an accordion. Opening one `.nav-section` closes the other expanded sections while preserving the active page highlight -and `aria-expanded` state. +and `aria-expanded` state. On mobile the sidebar is a modal drawer: its closed +content is inert and hidden from the accessibility tree, focus is contained +while open, Escape and the scrim close it, and focus returns to the opener. + +## Cascade, widths, and state surfaces + +The stylesheet contract is declared as `@layer reset, tokens, base, layout, +components, utilities, features, overrides`. Tokens, layout, and shared +components own their respective generic selectors; plugin and editor CSS belongs +to `features`. Temporary specificity exceptions belong to `overrides` and need +a removal note. + +Rendered pages default to `standard` width. Pass `pageWidth` as `prose`, `form`, +`standard`, `wide`, or `workspace` to the shared layout. Workspace width is for +editors such as overlays, not ordinary settings pages. The document remains the +primary vertical scroll container; bounded logs, tables, dialogs, canvas +surfaces, and explicitly labelled workspace editor panes are the exceptions. + +Use `empty-state`, `loading-state`, `partial-state`, `error-state`, +`offline-state`, and `permission-state` with specific recovery copy. Status is +never color-only. Persistent service degradation uses a banner, field/component +errors stay inline, low-severity completion may use a dismissible notice, and a +dialog is reserved for a genuine decision blocker. ## Themes @@ -120,6 +200,11 @@ Run `npm run verify:webui` to compile every EJS view and exercise built-in theme validation plus custom duplicate, apply, edit validation, typography validation, stateful theme actions, localhost login rendering, rename, and delete. +Run `npm run verify:ux` for the request/lifecycle/dialog/drawer/cascade and +overlay regression contracts. Run `npm run test:ui` for the temporary-database +Playwright matrix at 320, 390, 768, 1280, and 1920 px with axe, keyboard drawer, +no-reload settings, and overlay scroll/save checks. + ## Localhost Login Development builds opened from `localhost`, `127.0.0.1`, or `::1` show a @@ -198,11 +283,12 @@ with collapsed summaries, warning/danger badges, safe target versions, changelog ranges, migration notes, recovery state, snapshot/revert availability, and live progress from `GET /admin/updates/events`. -Core update success shows a five-second in-page notice before refresh/restart. -Plugin update success logs progress for the affected plugin row without forcing -a whole-page refresh. ZIP update controls remain available but are hidden under -advanced/manual reveal sections because ZIPs may bypass repository metadata and -compatibility checks. +Core and plugin update success keeps the current document available while Lumi +restarts, reports reconnect attempts, and refreshes the update workspace in +place after the service is reachable. Repository checks and snapshot cleanup +settings reconcile locally, with field-linked validation errors. ZIP update +controls remain available but are hidden under advanced/manual reveal sections +because ZIPs may bypass repository metadata and compatibility checks. See [Update system](updates.md) and [Recovery mode](recovery-mode.md) for the semver policy, compatibility bridge behavior, snapshots, revert limits, recovery diff --git a/docs/lumi-ux-checkpoint-review.md b/docs/lumi-ux-checkpoint-review.md new file mode 100644 index 0000000..185cb24 --- /dev/null +++ b/docs/lumi-ux-checkpoint-review.md @@ -0,0 +1,50 @@ +# Lumi UX overhaul checkpoint review + +This review records the current staged implementation checkpoint on `experimental-ux`. It covers the shared foundations and the highest-risk representative flows; it is not the final product-wide acceptance review. + +## Implemented surfaces + +- Shared tokens, cascade layers, content widths, field states, request envelopes, page lifecycle, dialogs, save/revert behavior, and explicit soft navigation. +- Responsive shell navigation, including focus containment, inert background content, Escape/scrim dismissal, and opener focus restoration. +- Admin settings targeted saves with inline and partial-error handling. +- Overlay management document scrolling, lifecycle cleanup, targeted saves, and OBS import reconciliation without a document reload. +- OKF draft reset and edit flows without a forced reload. +- Navigation builder keyboard movement, scoped save/reset responses, explicit labels, no-JavaScript JSON fallback, and lifecycle-owned behavior. +- Theme Studio targeted custom-theme saves, canonical preview reconciliation, lifecycle cleanup, and contrast corrections for disabled and preview states. +- Lumi AI settings lifecycle ownership for polling/timers and structured enhanced responses while retaining normal POST fallbacks. +- One shared authenticated live-event connection across core Updates and the Lumi AI Improvement Center, with explicit release before full navigation and repeated-navigation browser coverage. +- Custom commands and command policies with lifecycle-owned editors, structured validation, targeted saves/list refresh, keyboard-sortable discovery, and labelled mobile command cards. +- Updates and recovery with lifecycle-owned progress, shared event subscriptions, timed recovery/restore confirmations, targeted repository checks and cleanup-policy saves, and restart reconnection without a document reload. +- Plugin authoring guidance and a route/state manifest. +- Playwright coverage at 320, 390, 768, 1280, and 1920 pixels, including serious/critical axe checks on the public shell and representative authenticated pages. + +## Severity review + +### P0 + +No known P0 issue remains in the changed representative flows. + +### P1 + +- Feedback, logs/diagnostics, users/privileges/pages/plugins, and remaining plugin routes still need page-by-page migration and acceptance against the route manifest. +- The plugin-enabled browser fixture is not yet available, so Lumi AI lifecycle behavior is source/regression verified but not included in the authenticated Playwright route matrix. +- The complete built-in/custom-theme contrast matrix has not yet been browser-tested. +- All destructive and entity-edit dialogs have not yet received manual keyboard and screen-reader validation. + +### P2 + +- Several legacy views still rely on broad base selectors. Cascade layers now constrain their precedence, but the markup should continue moving to explicit shared components. +- Representative empty, loading, error, partial-success, offline, permission-limited, disabled, and read-only states exist in the design contract; not every route renders every applicable state yet. + +### P3 + +- Visual polish and density tuning remain for lower-priority plugin and diagnostics surfaces after their component migration. + +## Exit evidence for this checkpoint + +- `npm run verify:ux` +- `npm run test:ui` +- `node scripts/verify-all.js` +- `npm audit` + +The next checkpoint should migrate the remaining route groups in the order recorded in `docs/lumi-ux-route-manifest.md`, then repeat this review with changed routes removed from the remaining P1 list. diff --git a/docs/lumi-ux-route-manifest.md b/docs/lumi-ux-route-manifest.md new file mode 100644 index 0000000..d79cc40 --- /dev/null +++ b/docs/lumi-ux-route-manifest.md @@ -0,0 +1,165 @@ +# Lumi UX route and state manifest + +This manifest is the implementation contract for the DesignMotionHQ UX overhaul. It is maintained alongside `docs/lumi-ui.md` and must be updated whenever a rendered route, role boundary, asynchronous state, or page-owned script changes. + +## Shared state contract + +Every data-dependent surface distinguishes these states instead of collapsing them into a blank page or generic error: + +| State | Required presentation | Recovery or next action | +|---|---|---| +| Loading | Preserve existing content where possible; otherwise use a delayed local spinner or content-shaped skeleton. | Cancel when supported; do not flash a loader for work completing in under 300 ms. | +| Empty | Explain why the collection is empty. | Offer the role-appropriate create, connect, configure, or learn-more action. | +| Partial | Keep successful content visible and name the unavailable provider, row, or widget. | Retry the failed region or review its settings. | +| Error | Anchor a specific message to the failed form/component and state what remains safe. | Retry, correct linked fields, copy diagnostics, or return to a safe view. | +| Success | Patch the smallest affected region and announce completion without moving focus. | Continue in place or follow an explicit next-step link. | +| Offline/degraded | Distinguish local Lumi availability from Twitch, Discord, YouTube, OBS, AI runtime, socket, and other provider state. | Reconnect, configure, retry, or keep using unaffected local features. | + +Dirty, saving, queued, disabled, read-only, permission-denied, rate-limited, stale, reconnecting, conflict, and destructive-pending states extend this table where relevant. + +## Public and member surfaces + +Routes: `/`, `/commands`, `/leaderboards`, `/stats`, `/stats/:username`, `/profile`, `/feedback`, `/pages/:slug`, `/mods`, `/moderator`, `/auth/*`, `/setup`, and restricted/error/missing views. + +Surface: Public discovery and member account tasks +Users/roles: anonymous visitors, members, moderators, administrators +Primary job: discover community capabilities or complete the one account/community task named by the route +Entry context: sidebar, shared link, authentication return, or a community call to action +Desired outcome: useful information or a completed account action without admin density +Primary action: search/discover, sign in/link, submit feedback, or save the current profile task +Secondary actions: filters, comparison, history, related community destinations +Tertiary/destructive actions: unlink account, revoke or delete through separated contextual confirmation +Information needed immediately: current identity/status, the main content, eligibility and prerequisites +Information deferred: raw IDs, diagnostics, uncommon account details +Worst-case path: authentication/provider failure or destructive account action; preserve input and provide a safe return +Six-state behavior: shared contract; empty and filtered-empty copy are distinct +Feedback/loading strategy: local pending control, inline result, lightweight skeleton for collections +Mobile/tablet/desktop strategy: single-column reading flow on phone; labelled list rows replace small action tables; standard/prose width +Keyboard/focus strategy: heading focus after navigation, visible search, semantic lists/tables, labelled account actions +Success exit/next step: remain in context with updated status, or return to the intended pre-auth route + +## Core administration surfaces + +Routes: `/admin`, `/admin/settings`, `/admin/navigation`, `/admin/theming`, `/admin/diagnostics`, `/admin/logs`, `/admin/feedback`, `/admin/commands`, `/admin/command-policies`, `/admin/privileges`, `/admin/users`, `/admin/pages`, `/admin/plugins`, `/admin/updates`, `/admin/resources`, and platform setup wizards. + +Surface: Core administration workspaces +Users/roles: administrators; moderators on explicitly delegated command/user routes +Primary job: assess health, configure one domain, or manage one collection safely +Entry context: persistent administration navigation or an actionable dashboard warning +Desired outcome: a confirmed targeted update with place, filters, and draft context preserved +Primary action: domain-specific save/create/review/update action +Secondary actions: visible search, filters, preview, test, duplicate, export +Tertiary/destructive actions: advanced diagnostics, reset, uninstall, delete, rollback and manual update paths +Information needed immediately: current state, consequences, readiness, main decision fields, validation +Information deferred: raw metadata, rare provider controls, verbose diagnostics and recovery tooling +Worst-case path: partial save, invalid configuration, provider loss, destructive mutation, update/restart interruption +Six-state behavior: shared contract plus scoped dirty/saving/conflict/restart-required states +Feedback/loading strategy: enhanced response envelope, field-linked errors, local progress, persistent degraded banner +Mobile/tablet/desktop strategy: form/standard widths for settings; wide list-detail layouts; workspace widths only for editors +Keyboard/focus strategy: persistent labels, section links, table row names, non-drag reorder controls, dialog focus restoration +Success exit/next step: patch the affected section and keep focus; restart/reconnect only when explicitly required + +### Core route matrix + +| Surface | Representative states | Width | Form taxonomy | Primary regression risk | +|---|---|---|---|---| +| Dashboard | healthy, partial provider failure, stale, loading | standard | immediate action | duplicated configuration and non-actionable status | +| Settings/homepage | clean, dirty, invalid, partial save, restart-required | form/wide | settings | reload, lost preview, unrelated batched forms | +| Navigation builder | empty, selected, dirty, invalid upload | workspace | settings/upload | lost selection and drag-only ordering | +| Theme Studio | built-in read-only, custom draft, invalid contrast, pop-out | wide | settings/entity | draft leaking globally or preview reload | +| Commands/policies | empty, filtered-empty, selected, invalid, permission-limited | wide | entity/filter/destructive | dense schema presentation and lost filters | +| Feedback | empty queue, filtered, selected, export pending, merged | wide | entity/immediate/destructive | losing current item after mutation | +| Diagnostics/logs | idle, running, partial, degraded, paused/following | wide | immediate/settings | whole-page scrolling for bounded log data | +| Users/privileges/pages/plugins | empty, filtered, row pending, denied | wide | entity/filter/destructive/upload | unnamed row actions and forced reload | +| Updates/recovery | current, available, applying, reconnecting, failed, recovery | form/wide | immediate/upload/destructive | hiding recommended path or implicit reload | +| Resources | empty library, queue pending, partial upload, quota full, delivery error | workspace | upload/entity/filter | failed file erasing successful queue items | +| Setup wizards | prerequisites, verifying, provider error, complete | form | entity/immediate | accidental submit and lost prior steps | + +## Overlay management workspace + +Routes: `/admin/overlays`, `/admin/overlays/:id`, `/api/admin/overlays/*`, and privileged OBS/preview endpoints. + +Surface: Overlay list, scene/source editor, events, OBS integration and diagnostics +Users/roles: administrators and users granted explicit overlay capabilities +Primary job: compose and verify what OBS renders without losing editor context +Entry context: overlay list or an OBS/integration warning +Desired outcome: a saved canonical overlay matching the live draft preview +Primary action: edit the selected scene/source and save its scoped changes +Secondary actions: switch scene, test, align, reorder, preview, copy protected URL +Tertiary/destructive actions: advanced diagnostics, token replacement, reset and delete +Information needed immediately: selected object, preview, dirty/save state, validation and OBS connection state +Information deferred: event bindings, delivery credentials, raw diagnostics and destructive operations +Worst-case path: invalid source, blocked URL, partial render, disconnected OBS/socket, revoked link, destructive deletion +Six-state behavior: loading/empty-scene/partial-source/error/success/offline plus dirty and reconnecting +Feedback/loading strategy: draft-first rendering, targeted requests, optimistic transform with rollback, live status text +Mobile/tablet/desktop strategy: document is the only main vertical scroller; preview before inspector on narrow layouts; sticky preview only when it fits +Keyboard/focus strategy: named stage, arrow movement, labelled numeric fields and ordering buttons; no forced viewport movement +Success exit/next step: selection, open inspector, focus, viewport, canvas zoom and preview remain unchanged + +## Plugin surfaces + +Routes: `/plugins/auto-vc`, `/plugins/birthday`, `/plugins/economy-framework`, `/plugins/economy-games`, `/plugins/expression-interaction`, `/plugins/lumi_ai`, `/plugins/lumi_ai/improvement_center`, `/plugins/moderation`, `/plugins/okf`, `/plugins/okf/admin`, `/plugins/quotes`, `/plugins/throne_wishlist`, and `/plugins/welcome_messages` plus their detail/action routes. + +Surface: Plugin-owned member and administration workflows +Users/roles: members, moderators, administrators, and plugin-specific grantees +Primary job: use or configure the capability named by the plugin +Entry context: role-filtered plugin navigation, profile integration, or linked improvement task +Desired outcome: complete the plugin task with the same Lumi interaction contract as core +Primary action: plugin-specific save/create/review action +Secondary actions: search, filter, preview, test, duplicate, archive +Tertiary/destructive actions: raw diagnostics, maintenance, reset, revoke and delete +Information needed immediately: capability availability, current configuration/status, dependencies and main task +Information deferred: advanced provider/model fields, history, raw logs and maintenance +Worst-case path: missing dependency, runtime/provider offline, permission denial, partial save or destructive maintenance +Six-state behavior: shared contract with explicit unconfigured and permission states +Feedback/loading strategy: shared request/form/dialog/status services; long AI work uses progress or meaningful stages +Mobile/tablet/desktop strategy: declared form/standard/wide width; responsive list/table strategy; section navigation for dense plugins +Keyboard/focus strategy: labelled controls, contextual row actions, preserved master-detail selection and dialog return focus +Success exit/next step: patch the current domain/row and preserve filter, entry and disclosure state + +### Plugin migration order and intent + +| Plugin group | Primary workflow | Information architecture | Representative degraded state | +|---|---|---|---| +| Lumi AI | configure runtime/models and review improvement work | runtime, budgets, routing, context, tools, safety, improvement, diagnostics | runtime/model/search dependency unavailable | +| OKF | search knowledge or review/edit an entry | visible search plus master-detail review | entry permission changed or correction preview failed | +| Economy + games | configure currency flows and manage balances/events | currency, earning, spending, banking, limits, maintenance | platform earning source unavailable | +| Moderation | review status/actions/evidence/notes | list-detail with separated evidence and action | platform enforcement partially failed | +| Remaining plugins | configure one bounded capability | summary, main settings, entities, advanced/maintenance | integration unconfigured or provider offline | + +## Standalone browser surfaces + +Routes: `/overlay/:token`, fixed-scene overlay routes, `/overlay-chat/:ticket`, `/overlay-web/:ticket`, and standalone moderation status. + +Surface: OBS/browser-source rendering and standalone restriction status +Users/roles: viewers of a protected browser source, OBS runtime, or sanctioned user +Primary job: render reliably without the application shell, or understand a restriction and next step +Entry context: OBS Browser Source URL or moderation redirect +Desired outcome: stable transparent media rendering or a clear restriction explanation +Primary action: normally none for OBS; follow the stated recovery/appeal action for restriction state +Secondary actions: reconnect/status only when normal browser interaction exists +Tertiary/destructive actions: none +Information needed immediately: render/status content and concise recovery information +Information deferred: technical diagnostics and protected credentials +Worst-case path: expired ticket, blocked media, CSP failure, socket loss, unavailable provider +Six-state behavior: loading, empty scene, partial module, render error, connected success, reconnecting/offline +Feedback/loading strategy: preserve last safe frame while reconnecting; never require focus or user gesture assumptions unique to tabs +Mobile/tablet/desktop strategy: transparent canvas geometry is independent of shell breakpoints; status pages use prose width +Keyboard/focus strategy: decorative render tree is hidden where appropriate; status page remains normally navigable +Success exit/next step: continue rendering; no automatic document navigation + +## Baseline selectors and contracts to preserve + +- Main replacement root: `main.content`; page scripts must register with `LumiPage` before opting into soft navigation. +- Settings enhancement: `form[data-lumi-settings-form]`; ordinary POST remains the no-JavaScript fallback. +- State actions: `[data-lumi-state-button]` and existing route-specific action attributes. +- Overlay editor: `[data-overlay-editor-state]`, `[data-overlay-editor]`, `[data-editor-scene]`, `[data-module-editor]`, and protected overlay capability checks. +- Dialog backdrops: `.modal-backdrop[aria-hidden]`; opening code must use `LumiDialog` or emit compatible state changes. +- Navigation: `[data-sidebar-toggle]`, `#lumi-sidebar`, `[data-sidebar-dismiss]`, `.nav-link`, and `.nav-section`. +- Enhanced response: `{ ok, message, fieldErrors, data, patch, redirect, reloadRequired }`; reload is permitted only when `reloadRequired === true`. + +## Phase review record + +Known baseline P0/P1 findings are the reload-based save flow, viewport-coupled overlay settings height, forced overlay scrolling, page scripts without teardown, incomplete drawer focus handling, fragmented dialogs, inconsistent labels, and duplicated generic CSS ownership. A phase cannot be closed while a changed surface retains a known P0/P1 finding. + +Current migrated core surfaces include settings, overlay management, navigation builder, custom-theme editing, custom commands, command policies, and updates/recovery. Navigation changes now support pointer and labelled keyboard movement without a document reload; custom-theme saves preserve the draft preview and return canonical values. Command mutations return field-linked errors and refresh only the managed collection, while policy batches remain in place after save. Update checks, recovery actions, and cleanup settings now preserve the document, destructive recovery actions attach timed confirmation tokens, and restart flows reconnect before refreshing the workspace in place. Lumi AI settings polling is lifecycle-owned, but plugin-enabled browser-fixture coverage remains a required later gate. diff --git a/docs/plugin-ui-guide.md b/docs/plugin-ui-guide.md new file mode 100644 index 0000000..f1bf16b --- /dev/null +++ b/docs/plugin-ui-guide.md @@ -0,0 +1,104 @@ +# Lumi plugin UI guide + +Plugin pages are Lumi product surfaces. They use the shared EJS layout, tokens, +components, request contract, dialog behavior, accessibility rules, and browser +verification; plugins must not ship a parallel generic component library. + +## Page composition + +Render through the shared layout and pass a deliberate `pageWidth`: `form` for +bounded configuration, `standard` for ordinary member/admin pages, `wide` for +list-detail or data-heavy pages, and `workspace` only for a real editor. Use the +shared page/section headers, stacks, clusters, fields, buttons, notices, badges, +table wrappers, disclosures, and state surfaces. + +Organize fields around the user's decision rather than storage order. Keep one +primary action per region, quieter secondary actions nearby, tertiary work in a +named disclosure/menu, and destructive actions spatially separated with the +existing contextual safeguard. + +## Forms and enhanced responses + +Keep a functional method/action fallback on every form. Classify it as settings, +entity, immediate action, destructive action, filter/search, or upload/import. + +- Add `data-lumi-settings-form` only to genuinely related persistent settings. +- Add `data-lumi-enhanced-form` to an entity/settings form when its handler can + return a targeted result without reconstructing the route. +- Do not put action, destructive, filter, or unrelated forms into the shared + settings save bar. + +Enhanced handlers return: + +```json +{ + "ok": true, + "message": "Saved.", + "fieldErrors": {}, + "data": {}, + "patch": {}, + "redirect": null, + "reloadRequired": false +} +``` + +Return HTTP 400/422 with `ok: false` and field-name messages for validation. +The browser keeps failed input and links errors through `aria-describedby`. +Use `reloadRequired` only for an explicit restart/runtime replacement, never to +communicate routine success. + +## Lifecycle and dialogs + +Any page opting into soft navigation registers a lifecycle: + +```js +window.LumiPage.register("plugin-example", { + mount(root, { signal }) { + window.addEventListener("resize", updatePreview, { signal }); + const timer = window.setInterval(refreshStatus, 5000); + return () => window.clearInterval(timer); + } +}); +``` + +Use the supplied abort signal for global listeners and fetches; dispose timers, +observers, sockets, and inserted UI in the returned teardown. Do not re-execute +inline IIFEs after content replacement. + +Open modal surfaces through `LumiDialog` or a compatible +`.modal-backdrop[aria-hidden]` state. Give the dialog a programmatic title and +optional description. Do not implement a separate focus trap, background +isolation, scroll lock, or opener restoration. + +## Accessibility and responsive behavior + +Every control has a stable ID and associated label. Link helper/error text with +`aria-describedby`; use `aria-invalid` only while invalid. Icon actions include +the object in their accessible name. Tables have semantic headings/captions and +use the shared wrapper; choose horizontal scrolling for dense comparison data +or labelled list rows for small action tables. + +The complete workflow must reflow at 320 px and remain usable at 200% zoom, +keyboard-only, touch/coarse pointer, reduced motion, High Contrast, and custom +theme values. Dragging and hover are optional accelerators, never the only path. + +Implement loading, empty, partial, error, success, and offline/degraded states +where data or providers are involved. Empty and no-results are different. +Errors name what failed, what is safe, and a real recovery action. + +## Styling rules + +Plugin CSS belongs in the `features` cascade layer and composes semantic tokens. +Do not redefine `.button`, `.card`, `.field`, `.table`, `.modal`, or other shared +primitives. Hard-coded colors are reserved for meaningful external brand/media +values and still need contrast. Do not use remote fonts, shrink operational text +below `0.875rem`, calculate the main layout height from the viewport in +JavaScript, or create a nested main vertical scroller. + +## Verification + +Add EJS compile coverage and a representative Playwright/axe route state. +Exercise empty, populated, error/unconfigured, permission, and degraded states; +test keyboard focus, narrow reflow, theme values, no-reload mutation, and repeat +mount/unmount behavior. Run `npm run verify:ux`, the plugin's focused verifier, +and `npm run test:ui` before handoff. diff --git a/docs/recovery-mode.md b/docs/recovery-mode.md index 9bde4fd..87d9f61 100644 --- a/docs/recovery-mode.md +++ b/docs/recovery-mode.md @@ -52,6 +52,10 @@ Admins can: - clear a stale marker after verifying startup, - retry normal startup. +The normal Updates page submits these actions through the shared timed +confirmation and request contracts. When an action restarts Lumi, the current +document remains open and reconnects before refreshing recovery status in place. + Manual major-version rollback remains blocked unless the snapshot is explicitly marked rollback safe. The automatic rollback attempted for the same failed update is allowed because it restores the immediately preceding state. diff --git a/docs/updates.md b/docs/updates.md index 67a75ee..b83c998 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -62,6 +62,22 @@ and optional core/plugin boundaries. Stable checks read `main` by default. The newest `experimental-*` branch is used only when explicitly selected on the updates page. +## Production Branch Switching + +**Admin > Updates > Running code branch** lists the exact remote `main` and +`experimental-*` branches with their commits. Deploying one uses the same +managed repository, snapshot, protected-data, verification, recovery-marker, +and restart path as core updates even when the branch has the same semantic +version as the running code. Bundled plugin code is switched with core code; +plugin data and local-only plugins remain preserved. + +Returning to `main` through this control preserves the live database and is the +normal rollback path for an experiment. Emergency snapshot restore remains +available, but it restores the database captured before deployment and can +therefore discard later database writes. See +[Safe production branch deployments](branch-deployments.md) for the operating +procedure and migration-safety requirements. + ## Snapshots And Revert Before a core, plugin, repo, or ZIP update, Lumi creates a snapshot under @@ -162,9 +178,11 @@ Admin update actions publish Server-Sent Events through `update:download`, `update:apply`, `update:verify`, `update:complete`, `update:failed`, and `update:revert`. -Core update success returns a five-second in-page notice before refresh/restart. -Plugin update success updates progress for the affected plugin action without a -whole-page refresh, then restarts Lumi so the selected plugin code is loaded. +Core and plugin update success returns a five-second in-page notice before the +service restarts. The current document remains available, reports reconnect +attempts, and refreshes the Updates workspace in place once Lumi responds again. +Repository checks and cleanup-policy saves also reconcile without document +navigation. Submit buttons inherit the endpoint and method from their parent form unless they explicitly declare `formaction` or `formmethod`; this prevents browsers' current-page fallback values from redirecting an update away from its apply diff --git a/knowledge/core/lumi-core.md b/knowledge/core/lumi-core.md index 7ddedda..f7fdf7f 100644 --- a/knowledge/core/lumi-core.md +++ b/knowledge/core/lumi-core.md @@ -14,7 +14,7 @@ editable: false Lumi is the core web UI and bot runtime. ## Runtime Package: lumi-bot -Version: 0.2.24 +Version: 0.2.25 ## Routes - POST /api/diagnostics/v1/run - GET /api/events @@ -183,6 +183,7 @@ Version: 0.2.24 - POST /admin/overlays - POST /admin/overlays/reorder - GET /admin/overlays/:id +- GET /admin/overlays/:id/preview-window - POST /admin/overlays/:id - POST /admin/overlays/:id/duplicate - POST /admin/overlays/:id/delete @@ -740,7 +741,7 @@ Version: 0.2.24 - Purpose: Processes the admin navigation reset action and stores or applies submitted form data. - Inputs: No request parameters detected by static analysis. - Response format: HTTP redirect after handling the request -- Access: admin access expected +- Access: admin access expected; logged-in session required or used - Side effects: writes or mutates server-side state - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Most non-API POST routes are browser form submissions and usually redirect after completion. @@ -1135,16 +1136,16 @@ Version: 0.2.24 - Purpose: Creates, updates, previews, toggles, or deletes custom commands. - Inputs: body: `code`, `conditional_fuzzy`, `description`, `language`, `mode`, `response`, `trigger` -- Response format: HTTP redirect after handling the request +- Response format: Form/action response; exact format was not detected statically. - Access: admin access expected; logged-in session required or used -- Side effects: writes or mutates server-side state; writes database state when the called service mutates data +- Side effects: writes database state when the called service mutates data - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Input length or numeric bounds are enforced by helper functions in the handler. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion. ### POST /admin/commands/:id/toggle - Purpose: Creates, updates, previews, toggles, or deletes custom commands. - Inputs: path params: `id` -- Response format: HTTP redirect after handling the request +- Response format: Form/action response; exact format was not detected statically. - Access: admin access expected - Side effects: writes database state when the called service mutates data - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Most non-API POST routes are browser form submissions and usually redirect after completion. @@ -1153,27 +1154,27 @@ Version: 0.2.24 - Purpose: Creates, updates, previews, toggles, or deletes custom commands. - Inputs: path params: `id` -- Response format: HTTP redirect after handling the request +- Response format: Form/action response; exact format was not detected statically. - Access: admin access expected -- Side effects: writes or mutates server-side state; writes database state when the called service mutates data +- Side effects: writes database state when the called service mutates data - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Most non-API POST routes are browser form submissions and usually redirect after completion. ### POST /admin/commands/:id/update - Purpose: Creates, updates, previews, toggles, or deletes custom commands. - Inputs: path params: `id`; body: `code`, `conditional_fuzzy`, `description`, `language`, `mode`, `response`, `trigger` -- Response format: HTTP redirect after handling the request +- Response format: Form/action response; exact format was not detected statically. - Access: admin access expected; logged-in session required or used -- Side effects: writes or mutates server-side state; writes database state when the called service mutates data +- Side effects: writes database state when the called service mutates data - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Input length or numeric bounds are enforced by helper functions in the handler. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion. ### POST /admin/commands/:id/preview - Purpose: Creates, updates, previews, toggles, or deletes custom commands. - Inputs: path params: `id` -- Response format: HTTP redirect after handling the request +- Response format: Form/action response; exact format was not detected statically. - Access: admin access expected -- Side effects: writes or mutates server-side state; writes database state when the called service mutates data +- Side effects: writes database state when the called service mutates data - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Most non-API POST routes are browser form submissions and usually redirect after completion. ### GET /admin/pages @@ -1414,9 +1415,9 @@ Version: 0.2.24 - Purpose: Checks, applies, reverts, or reports update state for core or plugin updates. - Inputs: file upload: multipart form file data -- Response format: HTTP redirect after handling the request +- Response format: Form/action response; exact format was not detected statically. - Access: admin access expected -- Side effects: writes or mutates server-side state; writes files; may restart or stop runtime processes +- Side effects: writes or mutates server-side state; writes files - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion. ### POST /admin/updates/plugins/:id/check @@ -1716,6 +1717,15 @@ Version: 0.2.24 - Side effects: Usually read-only. - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. +### GET /admin/overlays/:id/preview-window + +- Purpose: Renders the admin overlays id preview window WebUI page. +- Inputs: path params: `id` +- Response format: HTML page rendered from an EJS view +- Access: admin access expected +- Side effects: Usually read-only. +- Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. + ### POST /admin/overlays/:id - Purpose: Processes the admin overlays id action and stores or applies submitted form data. @@ -1934,9 +1944,9 @@ Version: 0.2.24 ### POST /admin/overlays/:id/obs -- Purpose: Processes the admin overlays id obs action and stores or applies submitted form data. +- Purpose: Provides admin overlays id obs data as JSON. - Inputs: path params: `id`; body: `endpoint`, `password`, `provider`, `sync_direction` -- Response format: HTTP redirect after handling the request +- Response format: JSON response; HTTP redirect after handling the request - Access: admin access expected - Side effects: writes or mutates server-side state - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion. diff --git a/knowledge/plugins/okf.md b/knowledge/plugins/okf.md index f68b85a..4117983 100644 --- a/knowledge/plugins/okf.md +++ b/knowledge/plugins/okf.md @@ -99,7 +99,7 @@ Default state: enabled - Purpose: Processes the okf plugin administration action for entries slug. - Inputs: path params: `slug`; body: full submitted body is passed to a helper; exact fields are defined by the matching form/service -- Response format: HTTP redirect after handling the request +- Response format: JSON response; HTTP redirect after handling the request - Access: OKF editor or manager permission required; admin access expected; logged-in session required or used - Side effects: writes or mutates server-side state - Limits/notes: Generated from static route source analysis; confirm exact behavior in the handler before changing integrations. Errors are caught and usually returned as a flash message, JSON error, or error page. Most non-API POST routes are browser form submissions and usually redirect after completion. diff --git a/package-lock.json b/package-lock.json index 82e9dfd..fe9265f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "lumi-bot", - "version": "0.2.24", + "version": "0.2.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.2.24", + "version": "0.2.25", "dependencies": { - "adm-zip": "^0.5.12", + "adm-zip": "^0.6.0", "better-sqlite3": "^11.5.0", "better-sqlite3-session-store": "^0.1.0", "discord.js": "^13.17.1", @@ -19,6 +19,10 @@ "tmi.js": "^1.8.5", "ws": "^8.21.1" }, + "devDependencies": { + "@axe-core/playwright": "^4.12.1", + "@playwright/test": "^1.61.1" + }, "engines": { "node": ">=18" }, @@ -26,6 +30,19 @@ "obs-websocket-js": "^5.0.8" } }, + "node_modules/@axe-core/playwright": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.12.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, "node_modules/@discordjs/builders": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0.tgz", @@ -69,6 +86,22 @@ "node": ">= 10" } }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -134,12 +167,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", + "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/append-field": { @@ -166,6 +199,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/axe-core": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -804,6 +847,21 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1300,6 +1358,39 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", diff --git a/package.json b/package.json index c956c92..8847d07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.2.24", + "version": "0.2.25", "private": true, "type": "commonjs", "scripts": { @@ -16,6 +16,9 @@ "verify:web-auth": "node scripts/verify-web-auth.js", "verify:destructive-actions": "node scripts/verify-destructive-actions.js", "verify:overlays": "node scripts/verify-overlays.js", + "verify:ux": "node scripts/verify-ux-foundation.js", + "test:ui": "playwright test", + "test:ui:update": "playwright test --update-snapshots", "verify:webui": "node scripts/verify-webui.js && node scripts/verify-destructive-actions.js", "benchmark:okf": "node scripts/benchmark-okf-search.js", "verify:content": "node scripts/verify-content-library.js" @@ -24,7 +27,7 @@ "node": ">=18" }, "dependencies": { - "adm-zip": "^0.5.12", + "adm-zip": "^0.6.0", "better-sqlite3": "^11.5.0", "better-sqlite3-session-store": "^0.1.0", "discord.js": "^13.17.1", @@ -37,5 +40,9 @@ }, "optionalDependencies": { "obs-websocket-js": "^5.0.8" + }, + "devDependencies": { + "@axe-core/playwright": "^4.12.1", + "@playwright/test": "^1.61.1" } } diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..49ad1a3 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,40 @@ +const { defineConfig } = require("@playwright/test"); + +const port = Number(process.env.LUMI_UI_TEST_PORT || 3317); + +module.exports = defineConfig({ + testDir: "./tests/ui", + outputDir: "./test-results/ui", + timeout: 45_000, + expect: { timeout: 8_000 }, + fullyParallel: false, + workers: 1, + retries: process.env.CI ? 1 : 0, + reporter: [["list"], ["html", { outputFolder: "test-results/ui-report", open: "never" }]], + use: { + baseURL: `http://127.0.0.1:${port}`, + trace: "retain-on-failure", + screenshot: "only-on-failure", + video: "retain-on-failure", + reducedMotion: "reduce" + }, + webServer: { + command: "node tests/ui/fixture-server.js", + url: `http://127.0.0.1:${port}/health`, + timeout: 60_000, + reuseExistingServer: false, + env: { + ...process.env, + PORT: String(port), + LUMI_UI_TEST_PORT: String(port), + NODE_ENV: "test" + } + }, + projects: [ + { name: "phone-320", use: { browserName: "chromium", viewport: { width: 320, height: 720 }, isMobile: true, hasTouch: true } }, + { name: "phone-390", use: { browserName: "chromium", viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true } }, + { name: "tablet-768", use: { browserName: "chromium", viewport: { width: 768, height: 1024 }, hasTouch: true } }, + { name: "desktop-1280", use: { browserName: "chromium", viewport: { width: 1280, height: 900 } } }, + { name: "desktop-1920", use: { browserName: "chromium", viewport: { width: 1920, height: 1080 } } } + ] +}); diff --git a/plugins/lumi_ai/index.js b/plugins/lumi_ai/index.js index cac867d..d1bfd97 100644 --- a/plugins/lumi_ai/index.js +++ b/plugins/lumi_ai/index.js @@ -1760,6 +1760,18 @@ function saveAiFeedbackOkfCorrection(review, correction, values, actor) { }, actor); } function flash(req, res, type, message) { + if (req.get("X-Lumi-Enhanced") === "1" || req.get("X-Requested-With") === "XMLHttpRequest") { + const ok = type !== "error"; + return res.status(ok ? 200 : 422).json({ + ok, + message, + fieldErrors: {}, + data: {}, + patch: {}, + redirect: null, + reloadRequired: false + }); + } req.session.flash = { type, message }; return res.redirect(`/plugins/${PLUGIN_ID}`); } diff --git a/plugins/lumi_ai/public/assistant.css b/plugins/lumi_ai/public/assistant.css index 033cff4..1d86eb5 100644 --- a/plugins/lumi_ai/public/assistant.css +++ b/plugins/lumi_ai/public/assistant.css @@ -1,3 +1,4 @@ +@layer features { .lumi-ai-shell { width: 100%; min-width: 0; } .lumi-ai-overlay-root { position: fixed; inset: 0; z-index: 60; pointer-events: none; isolation: isolate; } .lumi-ai-pill { width: 100%; min-height: 42px; display: flex; align-items: center; gap: 9px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-2); color: var(--ink); cursor: pointer; } @@ -76,3 +77,14 @@ body.sidebar-collapsed .lumi-ai-pill { justify-content: center; padding: 8px; } body.sidebar-collapsed .lumi-ai-pill-label, body.sidebar-collapsed .lumi-ai-state { display: none; } + +.lumi-ai-header span, +.lumi-ai-clear, +.lumi-ai-code-header, +.lumi-ai-code-copy, +.lumi-ai-timeout-details, +.lumi-ai-feedback, +.lumi-ai-cooldown, +.lumi-ai-disclaimer, +.lumi-ai-disclaimer-detail { font-size: var(--lumi-font-size-sm); } +} diff --git a/plugins/lumi_ai/public/improvement-center.css b/plugins/lumi_ai/public/improvement-center.css index a1de87e..701419c 100644 --- a/plugins/lumi_ai/public/improvement-center.css +++ b/plugins/lumi_ai/public/improvement-center.css @@ -1,3 +1,4 @@ +@layer features { .improvement-titlebar { align-items: center; } .improvement-live-status { position: sticky; top: 10px; z-index: 15; margin: 0 0 12px; padding: 10px 13px; border: 1px solid var(--border); border-radius: 8px; background: var(--card); box-shadow: 0 8px 24px rgba(0,0,0,.18); } .improvement-live-status[hidden] { display: none; } @@ -64,3 +65,7 @@ .improvement-dialog { width: calc(100vw - 16px); max-height: calc(100vh - 16px); padding: 14px; } .improvement-decision-row > *, .improvement-decision-row form, .improvement-decision-row .button { width: 100%; } } + +.improvement-card > header, +.improvement-pair span { font-size: var(--lumi-font-size-sm); } +} diff --git a/plugins/lumi_ai/public/improvement-center.js b/plugins/lumi_ai/public/improvement-center.js index a2cb6e5..b8b09d5 100644 --- a/plugins/lumi_ai/public/improvement-center.js +++ b/plugins/lumi_ai/public/improvement-center.js @@ -168,18 +168,36 @@ function connectLiveUpdates() { if (!window.EventSource || stream) return; - stream = new EventSource("/api/events"); - stream.addEventListener("ai:improvement_changed", (event) => { + const handleChange = (event) => { try { const data = JSON.parse(event.data || "{}"); scheduleRefresh(data.message || "The feedback queue was updated by another reviewer."); } catch { scheduleRefresh("The feedback queue was updated by another reviewer."); } - }); + }; + if (window.LumiEvents?.subscribe) { + const unsubscribe = window.LumiEvents.subscribe("ai:improvement_changed", handleChange); + const handleStatus = (event) => { + if (event.detail?.status === "disconnected") { + showStatus("Live updates are reconnecting. Your saved actions are still safe.", "warning"); + } + }; + window.addEventListener("lumi:event-status", handleStatus); + stream = { + close() { + unsubscribe(); + window.removeEventListener("lumi:event-status", handleStatus); + } + }; + return; + } + stream = new EventSource("/api/events"); + stream.addEventListener("ai:improvement_changed", handleChange); stream.onerror = () => { showStatus("Live updates are reconnecting. Your saved actions are still safe.", "warning"); }; + window.addEventListener("pagehide", () => stream?.close(), { once: true }); } connectLiveUpdates(); diff --git a/plugins/lumi_ai/public/settings.css b/plugins/lumi_ai/public/settings.css index 12cf200..af50d98 100644 --- a/plugins/lumi_ai/public/settings.css +++ b/plugins/lumi_ai/public/settings.css @@ -1,3 +1,4 @@ +@layer features { .ai-titlebar, .ai-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; } .ai-titlebar { justify-content: flex-start; flex-wrap: wrap; margin-bottom: 0; padding: clamp(16px, 2vw, 24px); border: 1px solid var(--border); border-radius: var(--lumi-radius-md); background: var(--card); box-shadow: var(--lumi-shadow-sm); } .ai-titlebar > div:first-child { min-width: min(100%, 280px); margin-right: auto; } @@ -166,3 +167,15 @@ .ai-inline-form { flex-wrap: wrap; } .ai-diagnostic { grid-template-columns: 1fr; } } + +.ai-stat-grid span, +.ai-model-main span, +.ai-tag, +.ai-gpu-scale, +.ai-tools-source, +.ai-tool-diagnostic-row, +.ai-tool-version, +.ai-tool-scope, +.ai-tool-details strong, +.ai-tool-details span { font-size: var(--lumi-font-size-sm); } +} diff --git a/plugins/lumi_ai/public/settings.js b/plugins/lumi_ai/public/settings.js index 03e0dca..3233b9b 100644 --- a/plugins/lumi_ai/public/settings.js +++ b/plugins/lumi_ai/public/settings.js @@ -1,13 +1,32 @@ (() => { - const actions = document.querySelector("[data-ai-runtime-actions]"); - const state = document.querySelector("[data-runtime-state]"); - const downloadStatus = document.querySelector("[data-download-status]"); - const testForm = document.querySelector("[data-ai-test-form]"); - const testOutput = document.querySelector("[data-ai-test-output]"); - const testToolsNotice = document.querySelector("[data-ai-test-tools-notice]"); - const gpuControl = document.querySelector("[data-gpu-control]"); - const accessForm = document.querySelector("[data-ai-access-form]"); - const runtimePrimary = document.querySelector("[data-runtime-primary]"); + const mount = (root, { signal } = {}) => { + const listenerOptions = signal ? { signal } : undefined; + const timeouts = new Set(); + const intervals = new Set(); + const scheduleTimeout = (callback, delay) => { + const timer = window.setTimeout(() => { + timeouts.delete(timer); + if (!signal?.aborted) callback(); + }, delay); + timeouts.add(timer); + return timer; + }; + const scheduleInterval = (callback, delay) => { + const timer = window.setInterval(() => { + if (!signal?.aborted) callback(); + }, delay); + intervals.add(timer); + return timer; + }; + const actions = root.querySelector("[data-ai-runtime-actions]"); + const state = root.querySelector("[data-runtime-state]"); + const downloadStatus = root.querySelector("[data-download-status]"); + const testForm = root.querySelector("[data-ai-test-form]"); + const testOutput = root.querySelector("[data-ai-test-output]"); + const testToolsNotice = root.querySelector("[data-ai-test-tools-notice]"); + const gpuControl = root.querySelector("[data-gpu-control]"); + const accessForm = root.querySelector("[data-ai-access-form]"); + const runtimePrimary = root.querySelector("[data-runtime-primary]"); if (actions) { const syncPrimary = (nextState) => { if (!runtimePrimary || !window.LumiStateButton) return; @@ -42,9 +61,9 @@ } finally { button.disabled = false; } - }); + }, listenerOptions); } - document.querySelectorAll("[data-ai-download-form]").forEach((form) => { + root.querySelectorAll("[data-ai-download-form]").forEach((form) => { form.addEventListener("submit", async (event) => { event.preventDefault(); const button = form.querySelector("[data-ai-download-button]"); @@ -65,7 +84,7 @@ downloadStatus.textContent = error.message; } } - }); + }, listenerOptions); }); const pollDownloads = async () => { if (!downloadStatus) return; @@ -97,7 +116,7 @@ downloadStatus.hidden = false; downloadStatus.textContent = jobs.map(downloadStateLabel).join(" | "); jobs.forEach((job) => { - const button = document.querySelector(`[data-ai-download-button][data-download-id="${CSS.escape(job.id)}"]`); + const button = root.querySelector(`[data-ai-download-button][data-download-id="${CSS.escape(job.id)}"]`); if (!button || !window.LumiStateButton) return; if (job.state === "complete") window.LumiStateButton.setState(button, "success"); else if (job.state === "error") window.LumiStateButton.setState(button, "error"); @@ -110,10 +129,9 @@ : (job.state === "preparing_install" ? "Stopping runtimes" : job.state === "installing" ? "Installing" : "Working"); } }); - if (active.length) window.setTimeout(pollDownloads, 1000); + if (active.length) scheduleTimeout(pollDownloads, 1000); } catch {} }; - pollDownloads(); if (testForm && testOutput) { const updateTestToolsNotice = () => { if (!testToolsNotice) return; @@ -122,7 +140,7 @@ ? "Tools are enabled. This test uses normal discovery, prompt exposure, permission checks, and execution." : "Tools are disabled for this test; this result will not exercise tool discovery or execution."; }; - testForm.elements.allow_tools?.addEventListener("change", updateTestToolsNotice); + testForm.elements.allow_tools?.addEventListener("change", updateTestToolsNotice, listenerOptions); updateTestToolsNotice(); testForm.addEventListener("submit", async (event) => { event.preventDefault(); @@ -149,11 +167,11 @@ } catch (error) { testOutput.textContent = error.message; } - }); + }, listenerOptions); } if (gpuControl) { - const model = document.querySelector("[data-gpu-model]"); - const context = document.querySelector("[data-gpu-context]"); + const model = root.querySelector("[data-gpu-model]"); + const context = root.querySelector("[data-gpu-context]"); const workload = gpuControl.querySelector("[data-gpu-workload]"); const slider = gpuControl.querySelector("[data-gpu-slider]"); const value = gpuControl.querySelector("[data-gpu-value]"); @@ -218,7 +236,7 @@ }; const scheduleCapacity = () => { window.clearTimeout(capacityTimer); - capacityTimer = window.setTimeout(refreshCapacity, 250); + capacityTimer = scheduleTimeout(refreshCapacity, 250); }; workload.addEventListener("input", () => { const selected = clampNewIntent(); @@ -228,11 +246,11 @@ memory.textContent = formatBytes( (Number(memory.dataset.fullOffloadMb) || 0) * actual / 100 ); - }); - workload.addEventListener("change", refreshCapacity); - model.addEventListener("change", refreshCapacity); - context.addEventListener("input", scheduleCapacity); - context.addEventListener("change", refreshCapacity); + }, listenerOptions); + workload.addEventListener("change", refreshCapacity, listenerOptions); + model.addEventListener("change", refreshCapacity, listenerOptions); + context.addEventListener("input", scheduleCapacity, listenerOptions); + context.addEventListener("change", refreshCapacity, listenerOptions); refreshCapacity(); } if (accessForm) { @@ -246,10 +264,10 @@ input.required = visible; if (!visible) input.value = ""; }; - action.addEventListener("change", updateTimeoutVisibility); + action.addEventListener("change", updateTimeoutVisibility, listenerOptions); updateTimeoutVisibility(); } - document.querySelectorAll("[data-work-retention]").forEach((section) => { + root.querySelectorAll("[data-work-retention]").forEach((section) => { const mode = section.querySelector("[data-work-retention-mode]"); const count = section.querySelector("[data-work-retention-count]"); const age = section.querySelector("[data-work-retention-age]"); @@ -258,12 +276,13 @@ if (count) count.hidden = ageMode; if (age) age.hidden = !ageMode; }; - mode?.addEventListener("change", sync); + mode?.addEventListener("change", sync, listenerOptions); sync(); }); - document.querySelectorAll("[data-ai-work-row]").forEach((row) => { + root.querySelectorAll("[data-ai-work-row]").forEach((row) => { const button = row.querySelector(".ai-work-expand-button"); - const detail = document.getElementById(row.dataset.aiWorkDetail || ""); + const detailId = row.dataset.aiWorkDetail || ""; + const detail = detailId ? root.querySelector(`#${CSS.escape(detailId)}`) : null; if (!button || !detail) return; const toggle = () => { const open = detail.hidden; @@ -274,9 +293,9 @@ row.addEventListener("click", (event) => { if (event.target.closest("a, input, select, textarea") && !event.target.closest(".ai-work-expand-button")) return; toggle(); - }); + }, listenerOptions); }); - const assistantDiagnostics = document.querySelector("[data-assistant-diagnostics]"); + const assistantDiagnostics = root.querySelector("[data-assistant-diagnostics]"); if (assistantDiagnostics) { const status = assistantDiagnostics.querySelector("[data-assistant-status]"); const reason = assistantDiagnostics.querySelector("[data-assistant-reason]"); @@ -307,11 +326,11 @@ reason.textContent = error.message; }); refreshDiagnostics(); - window.setInterval(refreshDiagnostics, 5000); + scheduleInterval(refreshDiagnostics, 5000); } - const logContent = document.querySelector("[data-log-content]"); - const logFilter = document.querySelector("[data-log-filter]"); - const logCopy = document.querySelector("[data-log-copy]"); + const logContent = root.querySelector("[data-log-content]"); + const logFilter = root.querySelector("[data-log-filter]"); + const logCopy = root.querySelector("[data-log-copy]"); if (logContent && logFilter) { const originalLines = logContent.textContent.split(/\r?\n/); logFilter.addEventListener("input", () => { @@ -319,13 +338,27 @@ logContent.textContent = term ? originalLines.filter((line) => line.toLowerCase().includes(term)).join("\n") : originalLines.join("\n"); - }); + }, listenerOptions); logCopy?.addEventListener("click", async () => { await navigator.clipboard.writeText(logContent.textContent); const original = logCopy.textContent; logCopy.textContent = "Copied"; - window.setTimeout(() => { logCopy.textContent = original; }, 1200); - }); + scheduleTimeout(() => { logCopy.textContent = original; }, 1200); + }, listenerOptions); } pollDownloads(); + return () => { + timeouts.forEach((timer) => window.clearTimeout(timer)); + intervals.forEach((timer) => window.clearInterval(timer)); + }; + }; + + if (window.LumiPage) { + window.LumiPage.register("lumi-ai-settings", { + selector: '[data-lumi-page~="lumi-ai-settings"]', + mount + }); + } else { + mount(document); + } })(); diff --git a/plugins/lumi_ai/views/settings.ejs b/plugins/lumi_ai/views/settings.ejs index 415b646..ec2b7b0 100644 --- a/plugins/lumi_ai/views/settings.ejs +++ b/plugins/lumi_ai/views/settings.ejs @@ -1,4 +1,4 @@ -<%- include("../../../src/web/views/partials/layout-top", { title }) %> +<%- include("../../../src/web/views/partials/layout-top", { title, pageWidth: "wide", pageId: "lumi-ai-settings" }) %> <% const renderPresetOptions = (options, current) => { diff --git a/plugins/lumi_ai_web_search/public/settings-modal.css b/plugins/lumi_ai_web_search/public/settings-modal.css index 31e07e3..3c4a251 100644 --- a/plugins/lumi_ai_web_search/public/settings-modal.css +++ b/plugins/lumi_ai_web_search/public/settings-modal.css @@ -1,3 +1,4 @@ +@layer features { .lumi-web-search-status { display: grid; gap: 12px; margin-bottom: 16px; } .lumi-web-search-status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; } .lumi-web-search-status-grid > div { display: grid; gap: 3px; padding: 9px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-2); } @@ -9,3 +10,7 @@ .lumi-web-search-test label { display: grid; gap: 4px; font-weight: 800; } .lumi-web-search-test-output { max-height: 280px; overflow: auto; white-space: pre-wrap; } @media (max-width: 760px) { .lumi-web-search-test { grid-template-columns: 1fr; } } + +.lumi-web-search-status-grid span, +.lumi-web-search-recent { font-size: var(--lumi-font-size-sm); } +} diff --git a/plugins/okf/index.js b/plugins/okf/index.js index afa6d42..70ad86e 100644 --- a/plugins/okf/index.js +++ b/plugins/okf/index.js @@ -201,9 +201,31 @@ module.exports = { try { validateOkfPlaceholderFields(req.body, req.session.user, placeholders); const entry = updateEntry(db, req.params.slug, req.body, req.session.user); + if (isEnhancedRequest(req)) { + return res.json({ + ok: true, + message: "OKF entry updated.", + fieldErrors: {}, + data: { entry }, + patch: {}, + redirect: null, + reloadRequired: false + }); + } req.session.flash = { type: "success", message: "OKF entry updated." }; res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(entry.slug)}`); } catch (error) { + if (isEnhancedRequest(req)) { + return res.status(422).json({ + ok: false, + message: error.message, + fieldErrors: {}, + data: {}, + patch: {}, + redirect: null, + reloadRequired: false + }); + } req.session.flash = { type: "error", message: error.message }; res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(req.params.slug)}`); } @@ -329,6 +351,10 @@ module.exports = { } }; +function isEnhancedRequest(req) { + return req.get("X-Lumi-Enhanced") === "1" || req.get("X-Requested-With") === "XMLHttpRequest"; +} + function formatAiContext(entries) { if (!entries.length) return []; const prepared = preferSpecificKnowledgeChunks(entries).slice(0, 3); diff --git a/plugins/okf/views/admin.ejs b/plugins/okf/views/admin.ejs index c81ae06..a3b6729 100644 --- a/plugins/okf/views/admin.ejs +++ b/plugins/okf/views/admin.ejs @@ -1,4 +1,4 @@ -<%- include("../../../src/web/views/partials/layout-top", { title }) %> +<%- include("../../../src/web/views/partials/layout-top", { title, pageWidth: "wide", pageId: "okf-admin" }) %>
<%- include("../../../src/web/views/partials/page-header", { eyebrow: okfAccess.canManagePermissions ? "Administration" : "Community contribution", @@ -331,7 +331,7 @@ <% } %> <% if (selected) { %> -
@@ -216,229 +147,4 @@ - <%- include("partials/layout-bottom") %> diff --git a/src/web/views/admin-overlay-detail.ejs b/src/web/views/admin-overlay-detail.ejs index 1d4730f..17b7414 100644 --- a/src/web/views/admin-overlay-detail.ejs +++ b/src/web/views/admin-overlay-detail.ejs @@ -18,7 +18,7 @@ renderType: moduleTypes.find((type) => type.id === module.type)?.renderType || module.type }))); %> -<%- include("partials/layout-top", { title }) %> +<%- include("partials/layout-top", { title, pageWidth: "workspace", pageId: "overlay-management overlay-detail" }) %> @@ -31,7 +31,7 @@ }) %>
Overlay name and canvas size -
+
@@ -46,8 +46,12 @@
+ +
-
+

Use this overlay in OBS

<% if (!canViewSecrets) { %> @@ -67,7 +71,7 @@
<% } %> -
+
@@ -80,7 +84,7 @@
> <%= scene.name %> · <%= scene.enabled ? "On" : "Off" %><%= scene.id === overlay.active_scene_id ? " · Live" : "" %>
-
+
@@ -100,7 +104,7 @@ <% scene.modules.forEach((module) => { const config = module.config || {}; %>
<%= module.name %><%= moduleTypes.find((item) => item.id === module.type)?.label || module.type %> · <%= module.enabled ? "Visible" : "Hidden" %> - +
@@ -245,7 +249,7 @@ <% eventHooks.forEach((hook) => { %>
<%= hook.event?.label || hook.event_type %><%= hook.action?.label || hook.action_type %> · <%= hook.module_name %> · <%= hook.enabled ? "On" : "Off" %> - +
@@ -276,7 +280,7 @@
Connect Lumi to OBS Optional

This can switch scenes in OBS and keep OBS scenes matched with Lumi scenes. Most users can leave it off.

Connection<%= obsStatus.state %>
Current OBS scene<%= obsStatus.current_scene || "Unknown" %>
Page permission<%= obsStatus.control_level_label || "Not reported" %>
Current transition<%= obsStatus.current_transition || "Unknown" %>
OBS canvas<%= obsStatus.canvas_width && obsStatus.canvas_height ? `${obsStatus.canvas_width} × ${obsStatus.canvas_height}` : "Unknown" %>
Outputs<%= obsStatus.outputs?.streaming ? "Streaming" : obsStatus.outputs?.recording ? "Recording" : "Idle or unavailable" %>
Profile<%= obsStatus.profiles_supported === false ? "Not exposed by Browser Bridge" : (obsStatus.current_profile || "Unknown") %>
Scene collection<%= obsStatus.scene_collections_supported === false ? "Not exposed by Browser Bridge" : (obsStatus.current_scene_collection || "Unknown") %>
Browser Source<%= obsStatus.source_active === true ? "Active" : obsStatus.source_visible === true ? "Visible" : "Unknown" %>
Problem<%= obsStatus.error || "None" %>
- +
@@ -290,20 +294,32 @@ <% } %>
-
- + diff --git a/src/web/views/admin-overlay-preview.ejs b/src/web/views/admin-overlay-preview.ejs new file mode 100644 index 0000000..784fa1d --- /dev/null +++ b/src/web/views/admin-overlay-preview.ejs @@ -0,0 +1,41 @@ + + + + + + + <%= title %> + + + + +
+
+
+ + + +
+
+ + + +
+

No source selected

+

Waiting for editor

+
+
+
+
+
+ + +
+
+
+
+ + + + + diff --git a/src/web/views/admin-overlays.ejs b/src/web/views/admin-overlays.ejs index 5aaf07a..5c952ed 100644 --- a/src/web/views/admin-overlays.ejs +++ b/src/web/views/admin-overlays.ejs @@ -1,4 +1,4 @@ -<%- include("partials/layout-top", { title }) %> +<%- include("partials/layout-top", { title, pageWidth: "wide", pageId: "overlay-management" }) %>
<%- include("partials/page-header", { @@ -50,7 +50,7 @@
- Storage limits and supported formats + Storage limits and supported formats<%= settings.limit_gib > 0 ? `${settings.limit_gib} GiB library limit` : "Unlimited library" %> · <%= settings.max_file_mib %> MiB per file · <%= settings.max_files %> files per upload
- + Use 0 for no Lumi-specific quota. Physical disk capacity still applies.
- + Lumi stops accepting uploads before the disk falls below this reserve.
- +
- +
- +

diff --git a/src/web/views/admin-settings.ejs b/src/web/views/admin-settings.ejs index 0f4ff09..6bd74e9 100644 --- a/src/web/views/admin-settings.ejs +++ b/src/web/views/admin-settings.ejs @@ -42,12 +42,15 @@
- <% const selectedUpdateBranch = settings.git_branch === 'experimental' ? 'experimental' : 'main'; %> + <% const selectedUpdateBranch = settings.git_branch || 'main'; %> -

Stable uses main. Experimental uses the newest experimental-* branch.

+

Stable uses main. Experimental follows the newest experimental-* branch. Deploy and change an exact production branch from Updates.

@@ -219,7 +222,7 @@
- +
diff --git a/src/web/views/admin-theme.ejs b/src/web/views/admin-theme.ejs index 8723c9f..040ce1e 100644 --- a/src/web/views/admin-theme.ejs +++ b/src/web/views/admin-theme.ejs @@ -1,4 +1,4 @@ -<%- include("partials/layout-top", { title }) %> +<%- include("partials/layout-top", { title, pageWidth: "wide", pageId: "theme-studio" }) %> <% const fieldLabels = { @@ -46,7 +46,7 @@ Select a protected Lumi preset or duplicate one into a custom theme.

- + Active: <%= activeTheme.name %> @@ -166,6 +166,8 @@ action="/admin/theming/custom/<%= editingTheme.id.replace('custom:', '') %>/save" class="theme-edit-form" data-theme-form + data-lumi-enhanced-form + data-lumi-form-id="theme-editor" > <% ["light", "dark"].forEach((mode) => { %>
@@ -279,6 +281,7 @@
+

Draft changes stay in this preview until you save.

Badge - Role admin + Role admin
Dirty state marker
@@ -381,5 +384,5 @@ <% } %> - + <%- include("partials/layout-bottom") %> diff --git a/src/web/views/admin-updates.ejs b/src/web/views/admin-updates.ejs index c899658..500abaa 100644 --- a/src/web/views/admin-updates.ejs +++ b/src/web/views/admin-updates.ejs @@ -1,4 +1,4 @@ -<%- include("partials/layout-top", { title }) %> +<%- include("partials/layout-top", { title, pageWidth: "wide", pageId: "update-management" }) %> <% const status = updateStatus || {}; const core = status.core || null; @@ -20,6 +20,13 @@ return `${base} Warning: this update is to or from an unversioned install or update, so Lumi cannot fully verify version order, changelog range, or restore safety. ${warnings}`; }; const retention = snapshotRetention || { max_age_days: 30, max_per_target: 5 }; + const deployment = typeof deploymentState !== "undefined" && deploymentState ? deploymentState : { branch: "main", commit: null, version: core?.current_version || "unknown", previous_branch: null }; + const deployableBranches = typeof deploymentBranches !== "undefined" && Array.isArray(deploymentBranches) ? deploymentBranches : []; + const deploymentIssue = typeof deploymentError !== "undefined" ? deploymentError : null; + const deploymentSnapshot = (snapshots || []) + .filter((snapshot) => snapshot.type === "bot" && snapshot.status === "available") + .sort((left, right) => Number(right.createdAt || 0) - Number(left.createdAt || 0))[0] || null; + const shortCommit = (value) => value ? String(value).slice(0, 10) : "Not recorded"; const formatBytes = (value) => { const bytes = Number(value) || 0; if (bytes < 1024) return `${bytes} B`; @@ -31,6 +38,8 @@ }; %> +
+
<%- include("partials/page-header", { eyebrow: "Maintenance", @@ -40,7 +49,7 @@ <% if (updateStatusError) { %>
Update metadata could not be loaded: <%= updateStatusError %>
<% } %> -
+ + +
+

Selecting the running branch performs a verified repair deployment. Branches are limited to main and published experimental-* branches.

+ <% } %> + +
+ <% if (deployment.branch !== "main" && deployableBranches.some((branch) => branch.name === "main")) { %> +
+ + +
+ <% } %> + <% if (deploymentSnapshot) { %> +
+ + + +
+ <% } %> +
+
+ +
<% if (recovery.has_incomplete_marker || marker) { %>

Safe Mode / Recovery

@@ -81,17 +149,18 @@ <% if (marker.error) { %>

<%= marker.error %>

<% } %> <% } %>
-
+
-
+
<% } %> +
-
+
> @@ -187,7 +256,7 @@
No installed plugins found.
<% } %> <% plugins.forEach((plugin) => { %> -
> +
> <%= plugin.name %> @@ -280,7 +349,7 @@ <% } %> <% if (plugin.installed !== false) { %> -
+
<% } %> @@ -330,7 +399,7 @@

Core ZIP

Use a full core ZIP or a patch ZIP containing files relative to the Lumi install root.

-
+
@@ -342,7 +411,7 @@

Plugin ZIP

Use when plugin repository metadata is unavailable. Plugin ZIPs must include a valid plugin.json.

- +
@@ -356,7 +425,7 @@

Live Progress

-
+
Update and recovery progress appears here while actions run.
@@ -366,7 +435,7 @@

Snapshots contain compressed rollback code and a compressed database backup. Preserved local data—such as AI models, runtimes, uploads, logs, plugin data, and community knowledge—is kept in place during updates and is not copied into new snapshots.

Automatic snapshot cleanup - +