Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Local-first orchestrator for AI-assisted missions" />
<meta name="theme-color" content="#000000" />
<meta name="color-scheme" content="dark light" />
<meta name="view-transition" content="same-origin" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="true" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Haflow" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
Expand Down
55 changes: 55 additions & 0 deletions packages/frontend/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "Haflow",
"short_name": "Haflow",
"description": "Local-first orchestrator for AI-assisted missions",
"start_url": "/",
"scope": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"orientation": "portrait-primary",
"icons": [
{
"src": "/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png",
"purpose": "any"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png",
"purpose": "any"
},
{
"src": "/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/favicon_image.jpg",
"sizes": "512x512",
"type": "image/jpeg",
"form_factor": "wide"
}
],
"categories": ["productivity"],
"shortcuts": [
{
"name": "New Mission",
"short_name": "New Mission",
"description": "Create a new mission",
"url": "/?new-mission",
"icons": [
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
}
]
}
]
}
168 changes: 168 additions & 0 deletions packages/frontend/public/service-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
const CACHE_NAME = 'haflow-v1';
const RUNTIME_CACHE = 'haflow-runtime-v1';
const API_CACHE = 'haflow-api-v1';

// Files to cache on install
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/manifest.json',
'/favicon.ico',
'/favicon-16x16.png',
'/favicon-32x32.png',
'/apple-touch-icon.png'
];

// Install event - cache app shell
self.addEventListener('install', (event) => {
console.log('[ServiceWorker] Install');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(ASSETS_TO_CACHE);
}).catch((error) => {
console.warn('[ServiceWorker] Cache addAll failed:', error);
// Continue even if some assets fail to cache
return caches.open(CACHE_NAME).then((cache) => {
return Promise.all(
ASSETS_TO_CACHE.map((url) =>
cache.add(url).catch(() => {
console.warn('[ServiceWorker] Failed to cache:', url);
})
)
);
});
})
);
self.skipWaiting();
});

// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[ServiceWorker] Activate');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME && cacheName !== RUNTIME_CACHE && cacheName !== API_CACHE) {
console.log('[ServiceWorker] Deleting cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});

// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);

// Skip non-GET requests
if (request.method !== 'GET') {
return;
}

// Skip external URLs
if (url.origin !== self.location.origin) {
return;
}

// API requests - network first, fall back to cache
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstStrategy(request, API_CACHE));
return;
}

// Static assets (JS, CSS, fonts) - cache first
if (isStaticAsset(url.pathname)) {
event.respondWith(cacheFirstStrategy(request, RUNTIME_CACHE));
return;
}

// HTML and other documents - network first, fall back to cache
event.respondWith(networkFirstStrategy(request, RUNTIME_CACHE));
});

/**
* Cache first strategy: try cache, fall back to network
*/
function cacheFirstStrategy(request, cacheName) {
return caches.match(request).then((response) => {
if (response) {
return response;
}
return fetch(request)
.then((response) => {
if (!response || response.status !== 200 || response.type === 'error') {
return response;
}
const responseToCache = response.clone();
caches.open(cacheName).then((cache) => {
cache.put(request, responseToCache);
});
return response;
})
.catch(() => {
console.warn('[ServiceWorker] Fetch failed:', request.url);
// Return offline page or error response
return new Response('Offline - resource not available', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/plain'
})
});
});
});
}

/**
* Network first strategy: try network, fall back to cache
*/
function networkFirstStrategy(request, cacheName) {
return fetch(request)
.then((response) => {
if (!response || response.status !== 200 || response.type === 'error') {
return response;
}
const responseToCache = response.clone();
caches.open(cacheName).then((cache) => {
cache.put(request, responseToCache);
});
return response;
})
.catch(() => {
console.warn('[ServiceWorker] Network failed, trying cache:', request.url);
return caches.match(request).then((response) => {
if (response) {
return response;
}
return new Response('Offline - resource not available', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/plain'
})
});
});
});
}

/**
* Check if URL is a static asset
*/
function isStaticAsset(pathname) {
return /\.(js|css|woff|woff2|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp)$/i.test(pathname) ||
pathname.includes('/assets/');
}

// Handle messages from clients
self.addEventListener('message', (event) => {
console.log('[ServiceWorker] Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
26 changes: 26 additions & 0 deletions packages/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

// Register service worker for PWA support
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then((registration) => {
console.log('[App] Service Worker registered:', registration);

// Listen for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
// Notify user of update - could trigger UI notification here
console.log('[App] Service Worker updated, refresh to use new version');
// Optionally reload the page
// window.location.reload();
}
});
}
});
}).catch((error) => {
console.warn('[App] Service Worker registration failed:', error);
});
});
}

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ export default defineConfig({
'@': path.resolve(__dirname, './src'),
},
},
server: {
headers: {
'Service-Worker-Allowed': '/',
},
},
})
Loading