MVP: Go daemon, password auth, persistent PTY terminals, React/xterm UI, PWA

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 10:25:35 -07:00
co-authored by Claude Fable 5.1
parent 190604247f
commit 436732dac0
35 changed files with 3576 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

+16
View File
@@ -0,0 +1,16 @@
{
"name": "Hamlet",
"short_name": "Hamlet",
"description": "Linux management console",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#111418",
"theme_color": "#111418",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
+17
View File
@@ -0,0 +1,17 @@
// Hamlet service worker: cache hashed assets, never the API, network-first for the shell.
const CACHE = 'hamlet-v1';
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then((ks) => Promise.all(ks.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
});
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (e.request.method !== 'GET' || url.origin !== location.origin || url.pathname.startsWith('/api/')) return;
if (url.pathname.startsWith('/assets/') || url.pathname.startsWith('/icons/')) {
e.respondWith(caches.open(CACHE).then(async (c) => (await c.match(e.request)) || fetch(e.request).then((r) => { if (r.ok) c.put(e.request, r.clone()); return r; })));
return;
}
// navigations + everything else: network first, fall back to cached index
e.respondWith(fetch(e.request).then((r) => { if (r.ok && e.request.mode === 'navigate') caches.open(CACHE).then((c) => c.put('/', r.clone())); return r; })
.catch(() => caches.match(e.request.mode === 'navigate' ? '/' : e.request)));
});