From 0a0e3e2851d42d0a88e242800989f86fdd823322 Mon Sep 17 00:00:00 2001 From: jgor20 <102353650+jgor20@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:47:15 +0000 Subject: [PATCH] feat(ui): add data caching and hash-based routing - Implement localStorage-based caching in data-store to restore accounts, models, and usage data on load, improving initial render performance - Add hash-based routing in global store to sync active tab with URL, enabling browser back/forward navigation and direct linking to tabs --- public/js/data-store.js | 44 ++++++++++++++++++++++++++++++++++++++++- public/js/store.js | 27 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/public/js/data-store.js b/public/js/data-store.js index a55f8aa..0acbb8f 100644 --- a/public/js/data-store.js +++ b/public/js/data-store.js @@ -33,13 +33,54 @@ document.addEventListener('alpine:init', () => { // For simplicity, let's keep relevant filters here. init() { + // Restore from cache first for instant render + this.loadFromCache(); + // Watch filters to recompute // Alpine stores don't have $watch automatically unless inside a component? // We can manually call compute when filters change. }, + loadFromCache() { + try { + const cached = localStorage.getItem('ag_data_cache'); + if (cached) { + const data = JSON.parse(cached); + // Basic validty check + if (data.accounts && data.models) { + this.accounts = data.accounts; + this.models = data.models; + this.modelConfig = data.modelConfig || {}; + this.usageHistory = data.usageHistory || {}; + + // Don't show loading on initial load if we have cache + this.initialLoad = false; + this.computeQuotaRows(); + console.log('Restored data from cache'); + } + } + } catch (e) { + console.warn('Failed to load cache', e); + } + }, + + saveToCache() { + try { + const cacheData = { + accounts: this.accounts, + models: this.models, + modelConfig: this.modelConfig, + usageHistory: this.usageHistory, + timestamp: Date.now() + }; + localStorage.setItem('ag_data_cache', JSON.stringify(cacheData)); + } catch (e) { + console.warn('Failed to save cache', e); + } + }, + async fetchData() { - // Only show skeleton on initial load, not on refresh + // Only show skeleton on initial load if we didn't restore from cache if (this.initialLoad) { this.loading = true; } @@ -67,6 +108,7 @@ document.addEventListener('alpine:init', () => { this.usageHistory = data.history; } + this.saveToCache(); // Save fresh data this.computeQuotaRows(); this.connectionStatus = 'connected'; diff --git a/public/js/store.js b/public/js/store.js index 9c545b2..3802e80 100644 --- a/public/js/store.js +++ b/public/js/store.js @@ -5,6 +5,33 @@ document.addEventListener('alpine:init', () => { Alpine.store('global', { + init() { + // Hash-based routing + const validTabs = ['dashboard', 'models', 'accounts', 'logs', 'settings']; + const getHash = () => window.location.hash.substring(1); + + // 1. Initial load from hash + const initialHash = getHash(); + if (validTabs.includes(initialHash)) { + this.activeTab = initialHash; + } + + // 2. Sync State -> URL + Alpine.effect(() => { + if (validTabs.includes(this.activeTab) && getHash() !== this.activeTab) { + window.location.hash = this.activeTab; + } + }); + + // 3. Sync URL -> State (Back/Forward buttons) + window.addEventListener('hashchange', () => { + const hash = getHash(); + if (validTabs.includes(hash) && this.activeTab !== hash) { + this.activeTab = hash; + } + }); + }, + // App State version: '1.0.0', activeTab: 'dashboard',