diff --git a/src/app.html b/src/app.html
index eaf610b4..36ded2e6 100644
--- a/src/app.html
+++ b/src/app.html
@@ -4,7 +4,7 @@
-
+
diff --git a/src/routes/manifest.webmanifest/+server.ts b/src/routes/manifest.webmanifest/+server.ts
new file mode 100644
index 00000000..f9b4ae4a
--- /dev/null
+++ b/src/routes/manifest.webmanifest/+server.ts
@@ -0,0 +1,75 @@
+import { json } from '@sveltejs/kit';
+import { lookup_site_domain } from '$lib/ae_core/ae_core__site';
+import {
+ PUBLIC_AE_API_PROTOCOL,
+ PUBLIC_AE_API_SERVER,
+ PUBLIC_AE_API_BAK_SERVER,
+ PUBLIC_AE_API_PORT,
+ PUBLIC_AE_API_PATH,
+ PUBLIC_AE_API_SECRET_KEY,
+ PUBLIC_AE_API_CRUD_SUPER_KEY,
+ PUBLIC_AE_NO_ACCOUNT_ID
+} from '$env/static/public';
+
+const api_base_url = `${PUBLIC_AE_API_PROTOCOL}://${PUBLIC_AE_API_SERVER}:${PUBLIC_AE_API_PORT}${PUBLIC_AE_API_PATH}`;
+const api_base_url_bak = `${PUBLIC_AE_API_PROTOCOL}://${PUBLIC_AE_API_BAK_SERVER}:${PUBLIC_AE_API_PORT}${PUBLIC_AE_API_PATH}`;
+
+const api_init = {
+ base_url: api_base_url,
+ base_url_bak: api_base_url_bak,
+ api_secret_key: PUBLIC_AE_API_SECRET_KEY,
+ api_crud_super_key: PUBLIC_AE_API_CRUD_SUPER_KEY,
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ 'Content-Type': 'application/json',
+ 'x-aether-api-key': PUBLIC_AE_API_SECRET_KEY,
+ 'x-no-account-id': PUBLIC_AE_NO_ACCOUNT_ID
+ }
+};
+
+/** @type {import('./$types').RequestHandler} */
+export async function GET({ url, fetch }) {
+ const fqdn = url.host;
+
+ // Inject SvelteKit fetch for the lookup
+ const api_cfg = { ...api_init, fetch };
+
+ const site_domain = await lookup_site_domain({
+ api_cfg,
+ fqdn,
+ log_lvl: 0
+ });
+
+ if (!site_domain) {
+ return json({ error: 'Site not found' }, { status: 404 });
+ }
+
+ const site_name = site_domain.site_name || site_domain.account_name || 'Aether PWA';
+ const short_name = site_domain.site_code || 'Aether';
+
+ // Use the site's header image or logo if available, otherwise fallback to default
+ const icon_src = site_domain.header_image_path || '/favicon.png';
+
+ const manifest = {
+ name: `One Sky IT - ${site_name}`,
+ short_name: short_name,
+ description: `The Aether Progressive Web App for ${site_name}`,
+ start_url: '/',
+ display: 'fullscreen',
+ background_color: 'hsl(220, 65%, 31%)',
+ theme_color: 'hsl(220, 65%, 31%)',
+ icons: [
+ {
+ src: icon_src,
+ sizes: 'any',
+ type: 'image/png'
+ }
+ ]
+ };
+
+ return json(manifest, {
+ headers: {
+ 'Content-Type': 'application/manifest+json'
+ }
+ });
+}
diff --git a/src/service-worker.js b/src/service-worker.js
new file mode 100644
index 00000000..1da73f2e
--- /dev/null
+++ b/src/service-worker.js
@@ -0,0 +1,77 @@
+///
+import { build, files, version } from '$service-worker';
+
+// Create a unique cache name for this deployment
+const CACHE = `cache-${version}`;
+
+const ASSETS = [
+ ...build, // the app itself
+ ...files // everything in `static`
+];
+
+self.addEventListener('install', (event) => {
+ // Create a new cache and add all files to it
+ async function addFilesToCache() {
+ const cache = await caches.open(CACHE);
+ await cache.addAll(ASSETS);
+ }
+
+ event.waitUntil(addFilesToCache());
+});
+
+self.addEventListener('activate', (event) => {
+ // Delete old caches
+ async function deleteOldCaches() {
+ for (const key of await caches.keys()) {
+ if (key !== CACHE) await caches.delete(key);
+ }
+ }
+
+ event.waitUntil(deleteOldCaches());
+});
+
+self.addEventListener('fetch', (event) => {
+ // ignore POST requests etc
+ if (event.request.method !== 'GET') return;
+
+ async function respond() {
+ const url = new URL(event.request.url);
+ const cache = await caches.open(CACHE);
+
+ // `build`/`files` can always be served from the cache
+ if (ASSETS.includes(url.pathname)) {
+ const response = await cache.match(url.pathname);
+
+ if (response) {
+ return response;
+ }
+ }
+
+ // for everything else, try the network first, but fallback to the cache if we're offline
+ try {
+ const response = await fetch(event.request);
+
+ // if we're offline, fetch can return a value that is not a response
+ // instead of throwing - check it is a real response
+ if (response instanceof Response) {
+ if (response.status === 200) {
+ cache.put(event.request, response.clone());
+ }
+ return response;
+ }
+ throw new Error('invalid response');
+ } catch (err) {
+ const response = await cache.match(event.request);
+
+ if (response) {
+ return response;
+ }
+
+ // if there is no match, the throw will propagate to the browser, which
+ // will show its default offline page.
+ throw err;
+ }
+ }
+
+ event.respondWith(respond());
+});