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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ yarn-error.log*
# Misc
.DS_Store
*.pem


evault-cache.json
1 change: 1 addition & 0 deletions infrastructure/control-panel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"flowbite": "^3.1.2",
"flowbite-svelte": "^1.10.7",
"flowbite-svelte-icons": "^2.2.1",
"lowdb": "^9.1.0",
"lucide-svelte": "^0.539.0",
"tailwind-merge": "^3.0.2"
}
Expand Down
143 changes: 143 additions & 0 deletions infrastructure/control-panel/src/lib/components/EVaultList.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<script lang="ts">
import { onMount } from 'svelte';
import { EVaultService } from '../services/evaultService';
import type { EVault } from '../../routes/api/evaults/+server';

let evaults: EVault[] = [];
let loading = false;
let cacheStatus: any = null;
let lastRefresh = '';

// Load eVaults on component mount
onMount(async () => {
await loadEVaults();
// Get cache status for debugging
cacheStatus = EVaultService.getCacheStatus();
});

async function loadEVaults() {
loading = true;
try {
// This will return cached data immediately and refresh in background
evaults = await EVaultService.getEVaults();
lastRefresh = new Date().toLocaleTimeString();
} catch (error) {
console.error('Failed to load eVaults:', error);
} finally {
loading = false;
}
}

async function forceRefresh() {
loading = true;
try {
// Force refresh and get fresh data
evaults = await EVaultService.forceRefresh();
lastRefresh = new Date().toLocaleTimeString();
// Update cache status
cacheStatus = EVaultService.getCacheStatus();
} catch (error) {
console.error('Failed to force refresh:', error);
} finally {
loading = false;
}
}

async function clearCache() {
await EVaultService.clearCache();
cacheStatus = EVaultService.getCacheStatus();
// Reload data
await loadEVaults();
}
</script>

<div class="p-6">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">eVault Control Panel</h1>
<div class="flex gap-2">
<button
on:click={loadEVaults}
disabled={loading}
class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Refresh'}
</button>
<button
on:click={forceRefresh}
disabled={loading}
class="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600 disabled:opacity-50"
>
Force Refresh
</button>
<button
on:click={clearCache}
class="rounded bg-red-500 px-4 py-2 text-white hover:bg-red-600"
>
Clear Cache
</button>
</div>
</div>

<!-- Cache Status -->
{#if cacheStatus}
<div class="mb-6 rounded-lg bg-gray-100 p-4">
<h3 class="mb-2 font-semibold">Cache Status</h3>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<strong>Last Updated:</strong><br />
{new Date(cacheStatus.lastUpdated).toLocaleString()}
</div>
<div>
<strong>Status:</strong><br />
<span
class="rounded px-2 py-1 text-xs {cacheStatus.isStale
? 'bg-yellow-200 text-yellow-800'
: 'bg-green-200 text-green-800'}"
>
{cacheStatus.isStale ? 'Stale' : 'Fresh'}
</span>
</div>
<div>
<strong>Cached Items:</strong><br />
{cacheStatus.count} eVaults
</div>
</div>
</div>
{/if}

<!-- Last Refresh Info -->
{#if lastRefresh}
<div class="mb-4 text-sm text-gray-600">
Last refresh: {lastRefresh}
</div>
{/if}

<!-- eVault List -->
<div class="grid gap-4">
{#if evaults.length === 0}
<div class="py-8 text-center text-gray-500">
{loading ? 'Loading eVaults...' : 'No eVaults found'}
</div>
{:else}
{#each evaults as evault}
<div class="rounded-lg border p-4 transition-shadow hover:shadow-md">
<h3 class="text-lg font-semibold">{evault.name || 'Unnamed eVault'}</h3>
<div class="mt-2 text-sm text-gray-600">
<strong>Namespace:</strong>
{evault.namespace}<br />
<strong>Pod:</strong>
{evault.podName}<br />
<strong>Status:</strong>
<span
class="rounded px-2 py-1 text-xs {evault.status === 'Running'
? 'bg-green-200 text-green-800'
: 'bg-red-200 text-red-800'}"
>
{evault.status}
</span>
</div>
</div>
{/each}
{/if}
</div>
</div>
114 changes: 114 additions & 0 deletions infrastructure/control-panel/src/lib/services/cacheService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { Low } from 'lowdb'
import { JSONFile } from 'lowdb/node'
import type { EVault } from '../../routes/api/evaults/+server';

// Define the cache data structure
interface CacheData {
evaults: EVault[];
lastUpdated: number;
isStale: boolean;
}

// Cache file path (will be created in the project root)
const CACHE_FILE = './evault-cache.json';

// Default cache data
const defaultData: CacheData = {
evaults: [],
lastUpdated: 0,
isStale: true
};

class CacheService {
private db: Low<CacheData>;
private isInitialized = false;

constructor() {
// Initialize LowDB with JSON file adapter
const adapter = new JSONFile<CacheData>(CACHE_FILE);
this.db = new Low(adapter, defaultData);
}

/**
* Initialize the cache service
*/
async init(): Promise<void> {
if (this.isInitialized) return;

try {
await this.db.read();
this.isInitialized = true;
console.log('Cache service initialized');
} catch (error) {
console.warn('Cache file not found, using default data');
this.db.data = defaultData;
await this.db.write();
this.isInitialized = true;
}
}

/**
* Get cached eVaults (fast, returns immediately)
*/
async getCachedEVaults(): Promise<EVault[]> {
await this.init();
return this.db.data.evaults;
}

/**
* Check if cache is stale (older than 5 minutes)
*/
isCacheStale(): boolean {
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
return this.db.data.lastUpdated < fiveMinutesAgo;
}

/**
* Update cache with fresh data
*/
async updateCache(evaults: EVault[]): Promise<void> {
await this.init();

this.db.data = {
evaults,
lastUpdated: Date.now(),
isStale: false
};

await this.db.write();
console.log(`Cache updated with ${evaults.length} eVaults`);
}

/**
* Mark cache as stale (force refresh on next request)
*/
async markStale(): Promise<void> {
await this.init();
this.db.data.isStale = true;
await this.db.write();
}

/**
* Get cache status
*/
getCacheStatus(): { lastUpdated: number; isStale: boolean; count: number } {
return {
lastUpdated: this.db.data.lastUpdated,
isStale: this.db.data.isStale,
count: this.db.data.evaults.length
};
}

/**
* Clear cache
*/
async clearCache(): Promise<void> {
await this.init();
this.db.data = defaultData;
await this.db.write();
console.log('Cache cleared');
}
}

// Export singleton instance
export const cacheService = new CacheService();
82 changes: 80 additions & 2 deletions infrastructure/control-panel/src/lib/services/evaultService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,54 @@
import type { EVault } from '../../routes/api/evaults/+server';
import { cacheService } from './cacheService';

export class EVaultService {
/**
* Get eVaults with stale-while-revalidate pattern:
* 1. Return cached data immediately (fast)
* 2. Fetch fresh data in background
* 3. Update cache for next request
*/
static async getEVaults(): Promise<EVault[]> {
try {
// 1. Get cached data immediately (fast response)
const cachedEVaults = await cacheService.getCachedEVaults();

// 2. Check if we need to refresh in background
if (cacheService.isCacheStale()) {
// Fire and forget - fetch fresh data in background
this.refreshCacheInBackground();
}

// 3. Return cached data immediately
return cachedEVaults;
} catch (error) {
console.error('Error getting cached eVaults:', error);
// Fallback to direct API call if cache fails
return this.fetchEVaultsDirectly();
}
}

/**
* Fetch fresh eVaults from API and update cache
* This runs in the background to avoid blocking the UI
*/
private static async refreshCacheInBackground(): Promise<void> {
try {
console.log('🔄 Refreshing eVault cache in background...');
const freshEVaults = await this.fetchEVaultsDirectly();
await cacheService.updateCache(freshEVaults);
console.log('✅ Cache refreshed successfully');
} catch (error) {
console.error('❌ Failed to refresh cache:', error);
// Mark cache as stale so we try again next time
await cacheService.markStale();
}
}

/**
* Direct API call to fetch eVaults (fallback method)
*/
private static async fetchEVaultsDirectly(): Promise<EVault[]> {
try {
const response = await fetch('/api/evaults');
if (!response.ok) {
Expand All @@ -10,11 +57,42 @@ export class EVaultService {
const data = await response.json();
return data.evaults || [];
} catch (error) {
console.error('Error fetching eVaults:', error);
console.error('Error fetching eVaults directly:', error);
return [];
}
}

/**
* Force refresh cache and return fresh data
* Useful for manual refresh buttons
*/
static async forceRefresh(): Promise<EVault[]> {
try {
console.log('🔄 Force refreshing eVault cache...');
const freshEVaults = await this.fetchEVaultsDirectly();
await cacheService.updateCache(freshEVaults);
return freshEVaults;
} catch (error) {
console.error('Error force refreshing eVaults:', error);
// Return cached data as fallback
return await cacheService.getCachedEVaults();
}
}

/**
* Get cache status for debugging/monitoring
*/
static getCacheStatus() {
return cacheService.getCacheStatus();
}

/**
* Clear cache (useful for troubleshooting)
*/
static async clearCache(): Promise<void> {
await cacheService.clearCache();
}

static async getEVaultLogs(
namespace: string,
podName: string,
Expand Down Expand Up @@ -60,7 +138,7 @@ export class EVaultService {
}
return await response.json();
} catch (error) {
console.error('Error fetching eVault metrics:', error);
console.error('Error fetching metrics:', error);
return null;
}
}
Expand Down
2 changes: 2 additions & 0 deletions infrastructure/web3-adapter/src/db/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./mapping.db";
//# sourceMappingURL=index.d.ts.map
1 change: 1 addition & 0 deletions infrastructure/web3-adapter/src/db/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading