feat(dashboard): install the dashboard to a home screen with the Otari icon - #450
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (20)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughChangesDashboard PWA assets
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (8)
src/gateway/static/dashboard/pwa/apple-touch-icon.pngis excluded by!**/*.pngsrc/gateway/static/dashboard/pwa/icon-192.pngis excluded by!**/*.pngsrc/gateway/static/dashboard/pwa/icon-512.pngis excluded by!**/*.pngsrc/gateway/static/dashboard/pwa/icon-maskable-512.pngis excluded by!**/*.pngweb/public/pwa/apple-touch-icon.pngis excluded by!**/*.pngweb/public/pwa/icon-192.pngis excluded by!**/*.pngweb/public/pwa/icon-512.pngis excluded by!**/*.pngweb/public/pwa/icon-maskable-512.pngis excluded by!**/*.png
📒 Files selected for processing (8)
AGENTS.mdsrc/gateway/main.pysrc/gateway/static/dashboard/index.htmlsrc/gateway/static/dashboard/pwa/manifest.webmanifesttests/integration/test_cache_control_headers.pytests/unit/test_gateway_root_page.pyweb/index.htmlweb/public/pwa/manifest.webmanifest
…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>
There was a problem hiding this comment.
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
📒 Files selected for processing (18)
docs/dashboard.mdsrc/gateway/static/dashboard/assets/ActivityPage-dV0Jwq-x.jssrc/gateway/static/dashboard/assets/AliasesPage-o-MuWMDz.jssrc/gateway/static/dashboard/assets/BudgetsPage-BJY3mAj5.jssrc/gateway/static/dashboard/assets/ConfirmDialog-5GSYMzgW.jssrc/gateway/static/dashboard/assets/DocsPage-yMEZO6f-.jssrc/gateway/static/dashboard/assets/KeysPage-zwLllLpE.jssrc/gateway/static/dashboard/assets/ModelScopeControl-kyNzu8XN.jssrc/gateway/static/dashboard/assets/ModelsPage-DVly_Cxc.jssrc/gateway/static/dashboard/assets/OverviewPage-Cv5cEbot.jssrc/gateway/static/dashboard/assets/ProvidersPage-DIvrvoHT.jssrc/gateway/static/dashboard/assets/SettingsPage-BUCnNqsG.jssrc/gateway/static/dashboard/assets/TablePagination-CYPlrLSJ.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-GeVnUPd9.jssrc/gateway/static/dashboard/assets/UsagePage-kKHjOf-W.jssrc/gateway/static/dashboard/assets/UsersPage-Dglg_goj.jssrc/gateway/static/dashboard/assets/index-TSU-shFg.jssrc/gateway/static/dashboard/index.html
There was a problem hiding this comment.
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
/pwaserving 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
left a comment
There was a problem hiding this comment.
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
…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>
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-iconit never found.This adds a web app manifest and PNG app icons under
web/public/pwa/, all generated from the existing mark inweb/public/favicon.svg(white glyph on the--otari-brandteal):icon-192.png,icon-512.pngicon-maskable-512.pngapple-touch-icon.png(180px)manifest.webmanifestdisplay: standalone,start_url: /, brand theme color,#f6f9fasplash backgroundWiring
web/index.html: manifest link, apple-touch-icon,theme-color, and theapple-mobile-web-app-*tags. Status bar style isdefaultrather thanblack-translucent, since the app shell has no safe-area insets and would otherwise slide underneath the status bar.src/gateway/main.py: mounts/pwafrom the built bundle next to the existing/assetsmount, 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.AGENTS.mdand 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://orhttp://localhostyou 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
Page.getAppManifestreports zero parse errors, and all three manifest icons decode at their declared sizes.PR Type
Relevant issues
None filed; this came from trying to install the self-hosted dashboard on a phone.
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py). No API contract change; the new routes are a static mount excluded from the schema, andmake openapi-checkis clean.AI Usage
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.svgmark withrsvg-convertrather than hand-drawn, and the manifest was checked against a real Chromium via the DevTools Protocol.Summary