From 816a5bbbe60710fae147118a0b679db6bb6c9e0f Mon Sep 17 00:00:00 2001 From: Sushruth Sastry Date: Wed, 11 Mar 2026 01:19:28 -0700 Subject: [PATCH 1/4] feat(ui): PWA icons, CSS modules, theme and layout refactor - Add SVG icon variants (favicon, 192, 512, 1024, maskable) - Update manifest.json to reference new icons - Extract component styles into CSS modules (Header, Icon, Layout, ResultsTable, Sidebar, SidebarTables) - Clean up theme.css and add base/codemirror/utilities style files - Refactor vite.config.ts and update index.html Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .serena/project.yml | 34 +- packages/ui/index.html | 30 +- packages/ui/public/favicon.svg | 6 + packages/ui/public/icon-1024.svg | 7 + packages/ui/public/icon-192.svg | 7 + packages/ui/public/icon-512.svg | 6 + packages/ui/public/icon-maskable.svg | 7 + packages/ui/public/manifest.json | 2 +- packages/ui/public/sw.js | 108 +- packages/ui/src/App.tsx | 21 +- packages/ui/src/components/Header.tsx | 9 +- packages/ui/src/components/Icon.tsx | 3 +- packages/ui/src/components/Layout.tsx | 21 +- packages/ui/src/components/ResultsTable.tsx | 181 +--- packages/ui/src/components/Sidebar.tsx | 59 +- packages/ui/src/styles/theme.css | 1018 +------------------ packages/ui/src/vite-env.d.ts | 5 + packages/ui/vite.config.ts | 91 +- 18 files changed, 349 insertions(+), 1266 deletions(-) create mode 100644 packages/ui/public/favicon.svg create mode 100644 packages/ui/public/icon-1024.svg create mode 100644 packages/ui/public/icon-192.svg create mode 100644 packages/ui/public/icon-512.svg create mode 100644 packages/ui/public/icon-maskable.svg diff --git a/.serena/project.yml b/.serena/project.yml index b9514a5..42e0dfb 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -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: diff --git a/packages/ui/index.html b/packages/ui/index.html index 50793e7..71dab67 100644 --- a/packages/ui/index.html +++ b/packages/ui/index.html @@ -10,7 +10,7 @@ name="description" content="Fossiq - A modern UI for KQL query analysis built with SolidJS" /> - + { + 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(); + }); }); } diff --git a/packages/ui/public/favicon.svg b/packages/ui/public/favicon.svg new file mode 100644 index 0000000..23b7fc7 --- /dev/null +++ b/packages/ui/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/ui/public/icon-1024.svg b/packages/ui/public/icon-1024.svg new file mode 100644 index 0000000..d69986f --- /dev/null +++ b/packages/ui/public/icon-1024.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/public/icon-192.svg b/packages/ui/public/icon-192.svg new file mode 100644 index 0000000..b441acc --- /dev/null +++ b/packages/ui/public/icon-192.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/public/icon-512.svg b/packages/ui/public/icon-512.svg new file mode 100644 index 0000000..6050659 --- /dev/null +++ b/packages/ui/public/icon-512.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/ui/public/icon-maskable.svg b/packages/ui/public/icon-maskable.svg new file mode 100644 index 0000000..495b511 --- /dev/null +++ b/packages/ui/public/icon-maskable.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/public/manifest.json b/packages/ui/public/manifest.json index c1b5356..a274cc1 100644 --- a/packages/ui/public/manifest.json +++ b/packages/ui/public/manifest.json @@ -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": [ diff --git a/packages/ui/public/sw.js b/packages/ui/public/sw.js index c7f0794..9264114 100644 --- a/packages/ui/public/sw.js +++ b/packages/ui/public/sw.js @@ -1,69 +1,107 @@ // 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}`; + +// Assets to cache - includes all static files +const ASSETS_TO_CACHE = [ + "/", + "/index.html", + "/manifest.json", + "/favicon.svg", + "/icon-192.svg", + "/icon-512.svg", + "/icon-1024.svg", + "/icon-maskable.svg", +]; // Install event - cache assets self.addEventListener("install", (event) => { + console.log(`[SW] Installing version ${VERSION}`); + 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] Caching ${ASSETS_TO_CACHE.length} assets`); + return cache.addAll(ASSETS_TO_CACHE).catch((error) => { + console.error("[SW] Failed to cache some assets:", error); + // Don't fail the install if some assets can't be cached + return Promise.resolve(); }); }) ); + + // Force the waiting service worker to become the active service worker 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((cacheName) => { + const keepCache = cacheName === CACHE_NAME; + if (!keepCache) { + console.log(`[SW] Deleting old cache: ${cacheName}`); + return caches.delete(cacheName); + } + return Promise.resolve(); + }) ); - }) + + await self.clients.claim(); + })() ); - self.clients.claim(); }); -// Fetch event - serve from cache, fallback to network +// Fetch event - serve from cache, fallback to network with cache update self.addEventListener("fetch", (event) => { // Skip non-GET requests if (event.request.method !== "GET") { return; } + // Skip chrome-extension and other non-http requests + if (!event.request.url.startsWith("http")) { + return; + } + event.respondWith( - caches.match(event.request).then((response) => { - if (response) { - return response; + caches.match(event.request).then((cachedResponse) => { + // Cache hit - return cached response + if (cachedResponse) { + // Also fetch in background to update cache for next time + fetchAndUpdateCache(event.request); + return cachedResponse; } + // Cache miss - fetch from network return fetch(event.request) .then((response) => { // Don't cache non-successful responses - if (!response || response.status !== 200 || response.type === "error") { - return response; - } - - // Only cache http and https requests - if (!event.request.url.startsWith("http")) { + if ( + !response || + response.status !== 200 || + response.type === "error" + ) { return response; } // Clone the response const responseToCache = response.clone(); - // Cache the fetched response for future use + // Cache the fetched response caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseToCache); }); return response; }) - .catch(() => { + .catch((error) => { + console.error("[SW] Fetch failed:", error); // Return a fallback if both cache and network fail return new Response( "Service unavailable. Please check your connection.", @@ -73,3 +111,27 @@ self.addEventListener("fetch", (event) => { }) ); }); + +// Helper function to fetch and update cache in background +function fetchAndUpdateCache(request) { + fetch(request) + .then((response) => { + if (!response || response.status !== 200 || response.type === "error") { + return; + } + + caches.open(CACHE_NAME).then((cache) => { + cache.put(request, response); + }); + }) + .catch((error) => { + console.error("[SW] Background fetch failed:", error); + }); +} + +// Handle messages from clients +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } +}); diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 24e2ff2..d180668 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,5 +1,9 @@ import "@picocss/pico"; import "./styles/theme.css"; +import "./styles/base.css"; +import "./styles/codemirror.css"; +import "./styles/utilities.css"; +import styles from "./App.module.css"; import Layout from "./components/Layout"; import Editor from "./components/Editor"; import ResultsTable from "./components/ResultsTable"; @@ -11,7 +15,6 @@ const STORAGE_KEY_QUERY = "fossiq-query"; const STORAGE_KEY_RESULTS = "fossiq-results"; const AppContent: Component = () => { - // Load persisted query and results from localStorage const savedQuery = localStorage.getItem(STORAGE_KEY_QUERY) || ""; const savedResults = (() => { try { @@ -29,16 +32,13 @@ const AppContent: Component = () => { const [isRunning, setIsRunning] = createSignal(false); const { conn } = useSchema(); - // Persist query to localStorage when it changes createEffect(() => { localStorage.setItem(STORAGE_KEY_QUERY, query()); }); - // Persist results to localStorage when they change createEffect(() => { const currentResults = results(); if (currentResults.length > 0) { - // Convert BigInt to Number for JSON serialization const serializable = JSON.stringify(currentResults, (_, value) => typeof value === "bigint" ? Number(value) : value ); @@ -63,7 +63,6 @@ const AppContent: Component = () => { console.log("Executing SQL:", sql); const result = await connection.query(sql); - // result.toArray() returns Arrow Rows. toJSON() converts to plain object. const rows = result.toArray().map((row) => row.toJSON()); console.log("Query Results:", rows); setResults(rows); @@ -115,8 +114,8 @@ const AppContent: Component = () => { } editorPane={ -
-
+
+
{
} resultsPane={ -
-
+
+
{ gap: "1rem", }} > -

Results {results().length > 0 && `(${results().length})`}

+

+ Results {results().length > 0 && `(${results().length})`} +

diff --git a/packages/ui/src/components/Header.tsx b/packages/ui/src/components/Header.tsx index 4925e0d..c8b9eac 100644 --- a/packages/ui/src/components/Header.tsx +++ b/packages/ui/src/components/Header.tsx @@ -1,5 +1,6 @@ import { Component } from "solid-js"; import Icon from "./Icon"; +import styles from "./Header.module.css"; interface HeaderProps { onThemeToggle: () => void; @@ -9,13 +10,13 @@ interface HeaderProps { const Header: Component = (props) => { return ( -