Skip to content

feat(dashboard): install the dashboard to a home screen with the Otari icon - #450

Merged
njbrake merged 3 commits into
mainfrom
pwa_favicon
Jul 30, 2026
Merged

feat(dashboard): install the dashboard to a home screen with the Otari icon#450
njbrake merged 3 commits into
mainfrom
pwa_favicon

Conversation

@njbrake

@njbrake njbrake commented Jul 30, 2026

Copy link
Copy Markdown
Member

Description

Saving the self-hosted dashboard to a phone home screen produced a generic shortcut. The app shipped only an SVG favicon, so Android had no manifest to read a name or icon from, and iOS looked for an apple-touch-icon it never found.

This adds a web app manifest and PNG app icons under web/public/pwa/, all generated from the existing mark in web/public/favicon.svg (white glyph on the --otari-brand teal):

File Purpose
icon-192.png, icon-512.png Android manifest icons (both sizes are needed for an install offer)
icon-maskable-512.png Extra padding, so a launcher's icon shape crops the background instead of the glyph
apple-touch-icon.png (180px) What iOS Safari uses; it ignores the manifest entirely
manifest.webmanifest Name "Otari Dashboard" / short name "Otari", display: standalone, start_url: /, brand theme color, #f6f9fa splash background

Wiring

  • web/index.html: manifest link, apple-touch-icon, theme-color, and the apple-mobile-web-app-* tags. Status bar style is default rather than black-translucent, since the app shell has no safe-area insets and would otherwise slide underneath the status bar.
  • src/gateway/main.py: mounts /pwa from the built bundle next to the existing /assets mount, so it rides along with the standalone dashboard. Hybrid mode serves the tutorial at /, which is not an app worth installing, so nothing is mounted there. /pwa/ gets the favicon's day of public caching rather than /assets' immutable year, because the filenames are not content-hashed.
  • Rebuilt and committed the dashboard bundle. AGENTS.md and the bundled user guide (docs/dashboard.md) gain notes on the feature and on how the icons are regenerated. A stale session-storage description the review turned up was split out to docs(dashboard): correct the stale session-storage description of sign-in #451.

Deliberately not included

No service worker. Chrome dropped the fetch-handler requirement for menu-driven installs in 108 (mobile) and 112 (desktop), so the install works without one, and the dashboard is live API data: caching a shell offline would mostly create staleness bugs alongside the existing build-staleness poll.

One caveat worth knowing when trying this out: installability requires a secure context. Over https:// or http://localhost you get "Install app"; a self-hosted gateway reached at a plain-HTTP LAN address gets a plain bookmark shortcut instead. iOS "Add to Home Screen" uses the apple-touch-icon either way.

Verification

  • Booted the gateway and loaded it in real Chromium: Page.getAppManifest reports zero parse errors, and all three manifest icons decode at their declared sizes.
  • Measured the rendered PNGs: the maskable icon's ink fits within 79.9% of the canvas, inside the 80% safe circle a launcher mask can crop to, while the plain icons stay at the larger 70% width that suits an unmasked tile.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

None filed; this came from trying to install the self-hosted dashboard on a phone.

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py). No API contract change; the new routes are a static mount excluded from the schema, and make openapi-check is clean.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used:

Claude Code (Opus 5)

Any additional AI details you'd like to share:

Implementation, tests, icon generation, and this description were produced by Claude Code working from @njbrake's request, with the reasoning and decisions his. The icons were generated from the existing favicon.svg mark with rsvg-convert rather than hand-drawn, and the manifest was checked against a real Chromium via the DevTools Protocol.

  • I am an AI Agent filling out this form (check box if true)

Summary

  • Added installable PWA support for the Otari dashboard on Android and iOS, including branded icons, app metadata, and home-screen configuration.
  • Added backend serving and caching for PWA assets, with coverage for missing or stale dashboard bundles.
  • Documented installation steps, secure-context requirements, session behavior, and icon regeneration.
  • Rebuilt the dashboard bundle to include the new PWA metadata and documentation page.
  • No service worker or API contract changes.

…i icon

Saving the self-hosted dashboard to a phone home screen produced a generic
shortcut, because the app shipped only an SVG favicon: Android had no manifest
to read a name or icon from, and iOS looks for an apple-touch-icon it never
found.

Add a web app manifest and PNG app icons under web/public/pwa/, all generated
from the existing mark in web/public/favicon.svg (white glyph on the
--otari-brand teal). The maskable variant carries extra padding so a launcher's
icon shape crops the background rather than the glyph, and the 180px
apple-touch-icon covers iOS, which ignores the manifest.

The gateway mounts the directory at /pwa next to the existing /assets mount, so
it rides along with the standalone dashboard; hybrid mode serves the tutorial at
/, which is not an app worth installing. The names are not content-hashed, so
they get the favicon's day of caching instead of /assets' immutable year.

No service worker: Chrome dropped the fetch-handler requirement for menu-driven
installs, and the dashboard is live API data, so caching a shell offline would
mostly create staleness bugs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 30, 2026 00:52 — with GitHub Actions Inactive
@github-actions github-actions Bot added the missing-template PR is missing required template sections label Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cd143ceb-2114-494f-a5c4-6824a8e07e75

📥 Commits

Reviewing files that changed from the base of the PR and between 2c6b56e and e24863f.

📒 Files selected for processing (20)
  • docs/dashboard.md
  • src/gateway/static/dashboard/assets/ActivityPage-lcRQhBAw.js
  • src/gateway/static/dashboard/assets/AliasesPage-CPjhdIJQ.js
  • src/gateway/static/dashboard/assets/BudgetsPage-BuN1cg5q.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-DXR8OubG.js
  • src/gateway/static/dashboard/assets/DocsPage-Bw1czoYN.js
  • src/gateway/static/dashboard/assets/KeysPage-DHZEligd.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-Bcam0tnJ.js
  • src/gateway/static/dashboard/assets/ModelsPage-avB5ycpT.js
  • src/gateway/static/dashboard/assets/OverviewPage-CNyNgkDA.js
  • src/gateway/static/dashboard/assets/ProvidersPage-DueCtyY7.js
  • src/gateway/static/dashboard/assets/SettingsPage-BVUWlkda.js
  • src/gateway/static/dashboard/assets/TablePagination-CfsjgA8s.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-5txNsDJk.js
  • src/gateway/static/dashboard/assets/UsagePage-B19CFPzF.js
  • src/gateway/static/dashboard/assets/UsersPage-8qOL-FQs.js
  • src/gateway/static/dashboard/assets/index-JlWCttWt.js
  • src/gateway/static/dashboard/index.html
  • tests/unit/test_gateway_root_page.py
  • web/src/styles/globals.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/dashboard.md

Walkthrough

Changes

Dashboard PWA assets

Layer / File(s) Summary
PWA metadata and manifests
web/index.html, web/public/pwa/manifest.webmanifest, src/gateway/static/dashboard/index.html, src/gateway/static/dashboard/pwa/manifest.webmanifest
Adds installation metadata, manifests, icons, standalone launch settings, and updated dashboard entrypoint references.
Gateway mounting and caching
src/gateway/main.py
Conditionally mounts /pwa in standalone mode and applies a one-day public cache policy to PWA paths.
PWA validation and documentation
tests/integration/test_cache_control_headers.py, tests/unit/test_gateway_root_page.py, docs/dashboard.md, AGENTS.md, web/src/styles/globals.css
Tests PWA serving, caching, and missing-directory startup behavior; documents installation and theme asset relationships.
Generated dashboard bundle refresh
src/gateway/static/dashboard/assets/*
Updates generated chunk references, lazy-load mappings, entrypoint wiring, and the bundled documentation page.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • mozilla-ai/otari#373: Updates the dashboard’s hashed JavaScript entrypoint and related generated bundle artifacts.

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is descriptive, but it does not use one of the required prefixes exactly and is slightly over the length guideline. Rename it to a plain Conventional Commit title like feat: add PWA home-screen install support for the dashboard.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description follows the template with sections for description, type, issues, checklist, and AI usage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pwa_favicon
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch pwa_favicon

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from khaledosman July 30, 2026 00:53
@github-actions github-actions Bot removed the missing-template PR is missing required template sections label Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/test_gateway_root_page.py`:
- Around line 83-113: Update the PWA asset assertions in the root page test to
require an exact Cache-Control value of public, max-age=86400 for the manifest
response, every response in icon_responses, and apple_icon. Keep the existing
status, content-type, and payload assertions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 853622ff-ccf3-4b53-b695-6b66b5b04fd5

📥 Commits

Reviewing files that changed from the base of the PR and between ffad6b5 and 31aaf79.

⛔ Files ignored due to path filters (8)
  • src/gateway/static/dashboard/pwa/apple-touch-icon.png is excluded by !**/*.png
  • src/gateway/static/dashboard/pwa/icon-192.png is excluded by !**/*.png
  • src/gateway/static/dashboard/pwa/icon-512.png is excluded by !**/*.png
  • src/gateway/static/dashboard/pwa/icon-maskable-512.png is excluded by !**/*.png
  • web/public/pwa/apple-touch-icon.png is excluded by !**/*.png
  • web/public/pwa/icon-192.png is excluded by !**/*.png
  • web/public/pwa/icon-512.png is excluded by !**/*.png
  • web/public/pwa/icon-maskable-512.png is excluded by !**/*.png
📒 Files selected for processing (8)
  • AGENTS.md
  • src/gateway/main.py
  • src/gateway/static/dashboard/index.html
  • src/gateway/static/dashboard/pwa/manifest.webmanifest
  • tests/integration/test_cache_control_headers.py
  • tests/unit/test_gateway_root_page.py
  • web/index.html
  • web/public/pwa/manifest.webmanifest

Comment thread tests/unit/test_gateway_root_page.py
…reen

The manifest and app icons landed without a word in the bundled operator guide,
which is where someone would look for them. Add a short section covering the
Android and iOS install paths, what the installed app looks like, that iOS keeps
its own cookie storage so it needs its own sign-in, and that installing needs a
secure context at all.

The in-page link to the security notes is spelled out in prose rather than as a
"#security-notes" anchor: DocsPage rewrites in-page anchors to the GitHub source
because HashRouter owns location.hash, so an anchor here would send a reader out
of the dashboard to reach a section directly below them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 30, 2026 00:57 — with GitHub Actions Inactive
@njbrake
njbrake temporarily deployed to integration-tests July 30, 2026 01:05 — with GitHub Actions Inactive

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/dashboard.md`:
- Around line 195-198: Update the installation guidance in the PWA section to
state that PWA installation requires HTTPS, with HTTP allowed only for local
access via localhost or 127.0.0.1. Remove the broader secure-context wording and
clarify that plain HTTP on a LAN address is not installable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93e24c09-d25c-4ab2-9397-4e6634755b20

📥 Commits

Reviewing files that changed from the base of the PR and between 31aaf79 and 2c6b56e.

📒 Files selected for processing (18)
  • docs/dashboard.md
  • src/gateway/static/dashboard/assets/ActivityPage-dV0Jwq-x.js
  • src/gateway/static/dashboard/assets/AliasesPage-o-MuWMDz.js
  • src/gateway/static/dashboard/assets/BudgetsPage-BJY3mAj5.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-5GSYMzgW.js
  • src/gateway/static/dashboard/assets/DocsPage-yMEZO6f-.js
  • src/gateway/static/dashboard/assets/KeysPage-zwLllLpE.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-kyNzu8XN.js
  • src/gateway/static/dashboard/assets/ModelsPage-DVly_Cxc.js
  • src/gateway/static/dashboard/assets/OverviewPage-Cv5cEbot.js
  • src/gateway/static/dashboard/assets/ProvidersPage-DIvrvoHT.js
  • src/gateway/static/dashboard/assets/SettingsPage-BUCnNqsG.js
  • src/gateway/static/dashboard/assets/TablePagination-CYPlrLSJ.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-GeVnUPd9.js
  • src/gateway/static/dashboard/assets/UsagePage-kKHjOf-W.js
  • src/gateway/static/dashboard/assets/UsersPage-Dglg_goj.js
  • src/gateway/static/dashboard/assets/index-TSU-shFg.js
  • src/gateway/static/dashboard/index.html

Comment thread docs/dashboard.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds install-to-home-screen support for the standalone Otari admin dashboard by shipping a Web App Manifest plus platform-specific icons, wiring them into the dashboard HTML, and serving them from the gateway under /pwa with appropriate caching. This fits into the existing pattern where the dashboard bundle is committed under src/gateway/static/dashboard/ and served only in standalone mode.

Changes:

  • Added a web app manifest and home-screen icon set under web/public/pwa/, and linked them from the dashboard HTML.
  • Served /pwa/* from the standalone dashboard bundle and applied “public, max-age=86400” caching for those assets.
  • Added unit/integration tests for /pwa serving and cache headers, updated docs, and rebuilt the committed dashboard bundle.

Reviewed changes

Copilot reviewed 24 out of 33 changed files in this pull request and generated no comments.

Show a summary per file
File Description
web/public/pwa/manifest.webmanifest New manifest defining app metadata and icon URLs for installability.
web/index.html Adds manifest, apple-touch-icon, theme-color, and iOS install meta tags.
tests/unit/test_gateway_root_page.py Unit coverage for serving manifest/icons and absence when dashboard isn’t present.
tests/integration/test_cache_control_headers.py Integration coverage ensuring /pwa/* gets public cache headers and no Vary: Authorization.
src/gateway/static/dashboard/pwa/manifest.webmanifest Rebuilt/committed dashboard bundle copy of the manifest.
src/gateway/static/dashboard/index.html Rebuilt/committed dashboard bundle with new head tags and updated asset hashes.
src/gateway/static/dashboard/assets/UsersPage-BlK9Mzl7.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/UsagePage-Bl0X-Mom.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/ToolsGuardrailsPage-Cp3TnuQp.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/TablePagination-LTrsBooR.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/SettingsPage-BH4zz4rZ.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/OverviewPage-DHW5SZpO.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/ModelScopeControl-DSIGh9ic.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/KeysPage-BWN6L0we.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/ConfirmDialog-CG6DCqv7.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/BudgetsPage-BazRO22C.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/AliasesPage-DQSZY8H2.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/static/dashboard/assets/ActivityPage-CfANw7XK.js Rebuilt/committed dashboard bundle asset (hash/import updates).
src/gateway/main.py Adds /pwa static mount in standalone dashboard mode and day-cache policy for /pwa/*.
docs/dashboard.md Documents phone home-screen installation behavior and secure-context requirement.
AGENTS.md Documents where PWA assets live, how they’re served, and the no-service-worker decision.

@khaledosman khaledosman left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean and well-scoped. The mount rides get_dashboard_dir() so it stays standalone-only, the cache tier is correctly distinguished from /assets' immutable year, and test_pwa_manifest_and_icons_are_served verifying that every icon the manifest advertises actually resolves is the right test to write.

Checked and not a problem: .webmanifest resolves to application/manifest+json from Python 3.14's builtin mimetypes map, so the slim Docker image serves the right content type without a system mime.types.

Three non-blocking notes inline.

Landing order: this, #451, and #449 all edit docs/dashboard.md and rewrite the same hashed bundle assets, so whoever lands second needs a rebase plus npm --prefix web run build.

🤖 Generated with Claude Code

Comment thread src/gateway/main.py
Comment thread web/public/pwa/manifest.webmanifest
Comment thread src/gateway/main.py
…licy

Review caught that the `pwa_dir.is_dir()` guard had no test: the existing case
monkeypatches `get_dashboard_dir` to None, which exercises the outer branch where
there is no dashboard at all. The case the guard actually protects is a bundle
built before these assets existed (an older wheel, a stale Docker layer), where
`assets/` is present and only `pwa/` is missing. Mounting StaticFiles on a
missing directory raises at construction, so that bundle would fail to boot
rather than degrade. Removing the guard now fails the new test with exactly that
RuntimeError.

Assert the exact `public, max-age=86400` on the manifest and every icon instead
of merely rejecting `no-store`, so a drift to `no-cache` or another max-age is
caught; these names are not content-hashed, so the policy decides what an
installed app re-fetches.

Also spell out that installability means HTTPS or a loopback address rather than
"a secure context", which is the part a reader has to act on, and record at the
--otari-brand token that two files hardcode the same hex where a custom property
cannot reach: the theme-color meta tag and the manifest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake
njbrake temporarily deployed to integration-tests July 30, 2026 10:27 — with GitHub Actions Inactive
@coderabbitai
coderabbitai Bot requested a review from khaledosman July 30, 2026 10:29
@njbrake
njbrake merged commit e646444 into main Jul 30, 2026
10 checks passed
@njbrake
njbrake deleted the pwa_favicon branch July 30, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants