Skip to content
Merged
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
34 changes: 33 additions & 1 deletion .serena/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""

# the name by which the project can be referenced within Serena
project_name: "fossiq"

# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []

# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:

# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:

# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []

# override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used.
symbol_info_budget:

# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
30 changes: 29 additions & 1 deletion packages/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
name="description"
content="Fossiq - A modern UI for KQL query analysis built with SolidJS"
/>
<meta name="theme-color" content="#ffffff" />
<meta name="theme-color" content="#0c3b66" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta
name="apple-mobile-web-app-status-bar-style"
Expand Down Expand Up @@ -50,14 +50,42 @@
// Register service worker for offline support
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
let shouldReloadOnUpdate = false;

navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("Service Worker registered:", registration);

registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;
if (!newWorker) {
return;
}

newWorker.addEventListener("statechange", () => {
if (
newWorker.state === "installed" &&
navigator.serviceWorker.controller
) {
shouldReloadOnUpdate = true;
newWorker.postMessage({ type: "SKIP_WAITING" });
}
});
});
})
.catch((error) => {
console.log("Service Worker registration failed:", error);
});

navigator.serviceWorker.addEventListener("controllerchange", () => {
if (!shouldReloadOnUpdate) {
return;
}

shouldReloadOnUpdate = false;
window.location.reload();
});
});
}
</script>
Expand Down
6 changes: 6 additions & 0 deletions packages/ui/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/ui/public/icon-1024.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/ui/public/icon-192.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/ui/public/icon-512.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions packages/ui/public/icon-maskable.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/ui/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"orientation": "portrait-primary",
"theme_color": "#ffffff",
"theme_color": "#0c3b66",
"background_color": "#ffffff",
"categories": ["productivity", "utilities"],
"icons": [
Expand Down
122 changes: 67 additions & 55 deletions packages/ui/public/sw.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,87 @@
// Service Worker for Fossiq PWA
const CACHE_NAME = "fossiq-v1";
const ASSETS_TO_CACHE = ["/", "/index.html"];
// Version is updated during build process
const VERSION = "{{VERSION}}";
const CACHE_NAME = `fossiq-v${VERSION}`;

// DuckDB binaries: large, essentially immutable — cache-first
const DUCKDB_PATTERN = /\/(duckdb-[^/]+\.wasm|duckdb-[^/]+\.worker\.js)$/;

// Vite content-hashed assets: safe to cache forever — cache-first
// Matches e.g. /assets/index-CEEKSdb3.js, /assets/index-D9aWs9Jp.css
const HASHED_ASSET_PATTERN = /\/assets\/.+-[A-Za-z0-9]{8}\.(js|css|woff2?)(\.map)?$/;

// Install event - cache assets
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE).catch(() => {
console.log("Some assets could not be cached");
});
})
);
console.log(`[SW] Installing version ${VERSION}`);
// Skip waiting so the new SW takes over immediately on next navigation
self.skipWaiting();
});

// Activate event - clean up old caches
self.addEventListener("activate", (event) => {
console.log(`[SW] Activating version ${VERSION}`);
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => cacheName !== CACHE_NAME)
.map((cacheName) => caches.delete(cacheName))
(async () => {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map((name) => {
if (name !== CACHE_NAME) {
console.log(`[SW] Deleting old cache: ${name}`);
return caches.delete(name);
}
return Promise.resolve();
Comment on lines +23 to +30
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On activation, the SW deletes every cache except the current CACHE_NAME. Because CACHE_NAME is versioned per build, this guarantees that large immutable assets (DuckDB WASM/worker and Vite hashed assets) will be purged and re-downloaded on every deployment, negating the “cache forever” intent and potentially hurting repeat-load performance. Consider using separate caches (e.g., a stable static cache for DuckDB + hashed assets, and a versioned runtime cache for network-first resources), and only clear the versioned/runtime cache on update.

Copilot uses AI. Check for mistakes.
})
);
})
await self.clients.claim();
})()
);
self.clients.claim();
});

// Fetch event - serve from cache, fallback to network
self.addEventListener("fetch", (event) => {
// Skip non-GET requests
if (event.request.method !== "GET") {
return;
}
if (event.request.method !== "GET") return;
if (!event.request.url.startsWith("http")) return;

event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}
const { pathname } = new URL(event.request.url);

return fetch(event.request)
.then((response) => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type === "error") {
return response;
}
if (DUCKDB_PATTERN.test(pathname) || HASHED_ASSET_PATTERN.test(pathname)) {
// Cache-first: serve from cache, fetch+store on miss
event.respondWith(cacheFirst(event.request));
} else {
// Network-first: always try network, fall back to cache if offline
event.respondWith(networkFirst(event.request));
}
});

// Only cache http and https requests
if (!event.request.url.startsWith("http")) {
return response;
}
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;

// Clone the response
const responseToCache = response.clone();
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
}

// Cache the fetched response for future use
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
return new Response("Service unavailable. Please check your connection.", {
status: 503,
statusText: "Service Unavailable",
});
}
}

return response;
})
.catch(() => {
// Return a fallback if both cache and network fail
return new Response(
"Service unavailable. Please check your connection.",
{ status: 503, statusText: "Service Unavailable" }
);
});
})
);
self.addEventListener("message", (event) => {
if (event.data?.type === "SKIP_WAITING") {
self.skipWaiting();
}
});
49 changes: 49 additions & 0 deletions packages/ui/src/App.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.editorPane,
.resultsPane {
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
min-height: 0;
}

.editorPane {
border-right: 1px solid var(--border-color);
}

.paneHeader {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-shrink: 0;
background-color: var(--bg-secondary);
}

.paneHeaderTitle {
font-size: 0.8rem;
font-weight: 700;
margin: 0;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-primary);
opacity: 0.7;
flex: 1;
}

.paneActions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}

.editorContainer {
flex: 1;
overflow: auto;
background-color: var(--bg-primary);
border: none;
padding: 0;
}
Loading
Loading