installable PWA + branded app icons (0.40.0)
- web app manifest: name, standalone display, theme, app shortcuts (Browse/Booth/Mansion), maskable + any icons - branded gem app icons (192/512/apple-touch) + refreshed favicon.svg - service worker: network-first navigations (deploys never go stale), stale-while-revalidate for hashed assets, hard /api bypass, offline shell - registered only on production amerc.ai hosts (dev HMR untouched) - nginx: serve .webmanifest as application/manifest+json (all vhosts) - verified live: installable criteria met, SW controls page, 0 console errors
This commit is contained in:
parent
c8f6d0f892
commit
4ad339a94f
@ -4,8 +4,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0e0a14" />
|
||||
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
||||
<meta name="version" content="0.39.0-docs" />
|
||||
<meta name="version" content="0.40.0-pwa" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="amerc" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<!-- Open Graph / social share -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="amerc" />
|
||||
|
||||
BIN
public/app-192.png
Normal file
BIN
public/app-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
public/app-512.png
Normal file
BIN
public/app-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@ -1,7 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="10" fill="#170704"/>
|
||||
<path d="M12 48h40l-6-30H18z" fill="#8b4a22"/>
|
||||
<path d="M20 18h24l4 22H16z" fill="#321207"/>
|
||||
<path d="M27 10h10l9 9-14 14-14-14z" fill="#42dcff"/>
|
||||
<path d="M22 47c4-11 16-11 20 0" fill="none" stroke="#f7bc5d" stroke-width="5" stroke-linecap="round"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<defs>
|
||||
<radialGradient id="glow" cx="50%" cy="42%" r="55%">
|
||||
<stop offset="0%" stop-color="#28b4ff" stop-opacity="0.55"/>
|
||||
<stop offset="100%" stop-color="#28b4ff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="64" height="64" rx="14" fill="#160d22"/>
|
||||
<circle cx="32" cy="29" r="22" fill="url(#glow)"/>
|
||||
<polygon points="32,12 32,32 14,32" fill="#5adcff"/>
|
||||
<polygon points="32,12 50,32 32,32" fill="#3cbef0"/>
|
||||
<polygon points="14,32 32,32 32,52" fill="#1488d0"/>
|
||||
<polygon points="32,32 50,32 32,52" fill="#105aa0"/>
|
||||
<polygon points="32,12 22,18 32,22" fill="#cdf5ff"/>
|
||||
<polygon points="32,12 50,32 32,52 14,32" fill="none" stroke="#f3d27a" stroke-width="2.2" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 383 B After Width: | Height: | Size: 824 B |
23
public/manifest.webmanifest
Normal file
23
public/manifest.webmanifest
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "amerc — agent mercenary tavern",
|
||||
"short_name": "amerc",
|
||||
"description": "Hire, run, publish, and deliver AI agents across software boundaries. No infrastructure, just skills.",
|
||||
"id": "/",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#0e0a14",
|
||||
"theme_color": "#0e0a14",
|
||||
"categories": ["productivity", "developer", "business"],
|
||||
"icons": [
|
||||
{ "src": "/app-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/app-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/app-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
],
|
||||
"shortcuts": [
|
||||
{ "name": "Browse Agents", "short_name": "Browse", "url": "/#/agents" },
|
||||
{ "name": "My Booth", "short_name": "Booth", "url": "/#/booth" },
|
||||
{ "name": "The Mansion", "short_name": "Mansion", "url": "/#/mansion" }
|
||||
]
|
||||
}
|
||||
43
public/sw.js
Normal file
43
public/sw.js
Normal file
@ -0,0 +1,43 @@
|
||||
/* amerc service worker — offline app shell + asset caching.
|
||||
Strategy: navigations = network-first (deploys never go stale), hashed
|
||||
assets = stale-while-revalidate, /api = always bypass (auth + dynamic). */
|
||||
const CACHE = 'amerc-v1';
|
||||
const SHELL = '/index.html';
|
||||
|
||||
self.addEventListener('install', (e) => {
|
||||
e.waitUntil(caches.open(CACHE).then((c) => c.addAll([SHELL, '/favicon.svg', '/app-192.png'])).catch(() => {}));
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (e) => {
|
||||
e.waitUntil(
|
||||
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (e) => {
|
||||
const req = e.request;
|
||||
const url = new URL(req.url);
|
||||
// Only handle same-origin GET; never touch the API or cross-origin (relays, iframes).
|
||||
if (req.method !== 'GET' || url.origin !== self.location.origin || url.pathname.startsWith('/api')) return;
|
||||
|
||||
// Navigations: network-first, fall back to cached shell when offline.
|
||||
if (req.mode === 'navigate') {
|
||||
e.respondWith(
|
||||
fetch(req).then((res) => { const copy = res.clone(); caches.open(CACHE).then((c) => c.put(SHELL, copy)); return res; })
|
||||
.catch(() => caches.match(SHELL).then((m) => m || caches.match(req)))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Static assets: serve from cache fast, refresh in the background.
|
||||
e.respondWith(
|
||||
caches.match(req).then((cached) => {
|
||||
const network = fetch(req).then((res) => {
|
||||
if (res && res.status === 200 && res.type === 'basic') { const copy = res.clone(); caches.open(CACHE).then((c) => c.put(req, copy)); }
|
||||
return res;
|
||||
}).catch(() => cached);
|
||||
return cached || network;
|
||||
})
|
||||
);
|
||||
});
|
||||
@ -7,7 +7,7 @@ import Account from './Account.jsx';
|
||||
import TavernGame from './TavernGame.jsx';
|
||||
import { api } from './api.js';
|
||||
|
||||
const RELEASE = '0.39.0-docs';
|
||||
const RELEASE = '0.40.0-pwa';
|
||||
|
||||
const NAV = [
|
||||
{ id: 'home', label: 'Tavern' },
|
||||
|
||||
@ -19,3 +19,9 @@ createRoot(document.getElementById('root')).render(
|
||||
</Suspense>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
// Progressive Web App: register the service worker on production amerc.ai hosts only
|
||||
// (skip the Vite dev server so HMR keeps working).
|
||||
if ('serviceWorker' in navigator && host.endsWith('amerc.ai')) {
|
||||
window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js').catch(() => {}); });
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user