From d9157663a8df408851018c9205c121852955d559 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sat, 31 Jan 2026 14:46:12 +0000 Subject: [PATCH] V7 super admin --- db/migrations/007_add_second_super_admin.sql | 4 + public/app.js | 159 +++++++++++++++++-- public/index.html | 19 ++- 3 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 db/migrations/007_add_second_super_admin.sql diff --git a/db/migrations/007_add_second_super_admin.sql b/db/migrations/007_add_second_super_admin.sql new file mode 100644 index 0000000..98b94d9 --- /dev/null +++ b/db/migrations/007_add_second_super_admin.sql @@ -0,0 +1,4 @@ +-- Migration: Add second default super admin +INSERT IGNORE INTO users (email, password, role, school_id) VALUES +('admin@flatlogic.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Super Admin', NULL); +-- password is 'password' diff --git a/public/app.js b/public/app.js index be6a38c..82ba72c 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,68 @@ const API_URL = '/api/v1/index.php?request='; +// --- IndexedDB Configuration --- +const dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open('SOMS_DB', 2); + request.onupgradeneeded = (e) => { + const db = e.target.result; + if (!db.objectStoreNames.contains('data')) db.createObjectStore('data'); + if (!db.objectStoreNames.contains('sync_queue')) db.createObjectStore('sync_queue', { keyPath: 'id', autoIncrement: true }); + }; + request.onsuccess = (e) => resolve(e.target.result); + request.onerror = (e) => reject(e.target.error); +}); + +async function dbGet(store, key) { + const db = await dbPromise; + return new Promise((resolve, reject) => { + const trans = db.transaction(store, 'readonly'); + const req = trans.objectStore(store).get(key); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +async function dbSet(store, key, value) { + const db = await dbPromise; + return new Promise((resolve, reject) => { + const trans = db.transaction(store, 'readwrite'); + const req = trans.objectStore(store).put(value, key); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); +} + +async function dbAdd(store, value) { + const db = await dbPromise; + return new Promise((resolve, reject) => { + const trans = db.transaction(store, 'readwrite'); + const req = trans.objectStore(store).add(value); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); +} + +async function dbGetAll(store) { + const db = await dbPromise; + return new Promise((resolve, reject) => { + const trans = db.transaction(store, 'readonly'); + const req = trans.objectStore(store).getAll(); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +async function dbDelete(store, key) { + const db = await dbPromise; + return new Promise((resolve, reject) => { + const trans = db.transaction(store, 'readwrite'); + const req = trans.objectStore(store).delete(key); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); +} + +// --- App State --- const state = { user: JSON.parse(localStorage.getItem('user')) || null, token: localStorage.getItem('token') || null, @@ -16,17 +79,52 @@ const routes = { '/super-admin': superAdminPage, }; +// --- Initialization --- async function init() { window.addEventListener('hashchange', router); + window.addEventListener('online', syncData); + window.addEventListener('offline', () => updateOnlineStatus(false)); + + updateOnlineStatus(navigator.onLine); router(); updateNav(); + + if (navigator.onLine) { + syncData(); + } +} + +function updateOnlineStatus(isOnline) { + const indicator = document.getElementById('offline-indicator'); + if (indicator) { + indicator.style.display = isOnline ? 'none' : 'block'; + } +} + +async function syncData() { + updateOnlineStatus(true); + const queue = await dbGetAll('sync_queue'); + if (queue.length === 0) return; + + console.log('Syncing data...', queue.length, 'items'); + for (const item of queue) { + try { + await apiFetch(item.endpoint, { + method: item.method, + body: item.body + }, true); // forceOnline = true + await dbDelete('sync_queue', item.id); + } catch (err) { + console.error('Failed to sync item:', item, err); + break; // Stop syncing if network fails again + } + } } function router() { const hash = window.location.hash || '#/'; const path = hash.substring(1); - // Auth guard if (!state.token && path !== '/login') { window.location.hash = '#/login'; return; @@ -38,6 +136,8 @@ function router() { function updateNav() { const navLinks = document.getElementById('nav-links'); + if (!navLinks) return; + if (!state.token) { navLinks.innerHTML = ` @@ -77,28 +177,61 @@ function logout() { updateNav(); } -async function apiFetch(endpoint, options = {}) { +async function apiFetch(endpoint, options = {}, forceOnline = false) { const url = API_URL + endpoint; + const method = options.method || 'GET'; + const body = options.body || null; + const defaultOptions = { headers: { 'Content-Type': 'application/json', ...(state.token ? { 'Authorization': `Bearer ${state.token}` } : {}) } }; - const response = await fetch(url, { ...defaultOptions, ...options }); - if (response.status === 401) { - logout(); - throw new Error('Unauthorized'); + + if (method === 'GET' && !forceOnline) { + try { + const response = await fetch(url, { ...defaultOptions, ...options }); + if (response.status === 401) { logout(); throw new Error('Unauthorized'); } + const data = await response.json(); + if (!response.ok) throw new Error(data.error || 'API Error'); + + // Save to offline storage + await dbSet('data', endpoint, data); + return data; + } catch (err) { + console.warn('Network failed, trying offline storage for:', endpoint); + const cached = await dbGet('data', endpoint); + if (cached) return cached; + throw err; + } + } else { + // POST/PUT/DELETE + try { + const response = await fetch(url, { ...defaultOptions, ...options }); + if (response.status === 401) { logout(); throw new Error('Unauthorized'); } + const data = await response.json(); + if (!response.ok) throw new Error(data.error || 'API Error'); + return data; + } catch (err) { + if (forceOnline) throw err; + + console.warn('Network failed, queueing request for:', endpoint); + await dbAdd('sync_queue', { endpoint, method, body, timestamp: Date.now() }); + + // Return a "fake" success to keep the UI responsive + return { success: true, offline: true, message: 'Data queued for sync' }; + } } - const data = await response.json(); - if (!response.ok) throw new Error(data.error || 'API Error'); - return data; } function render(html) { - document.getElementById('content').innerHTML = html; + const content = document.getElementById('content'); + if (content) content.innerHTML = html; } +// --- Page Renderers --- + async function homePage() { render(`
@@ -177,7 +310,7 @@ function loginPage() { const data = await apiFetch('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) - }); + }, true); // forceOnline for login if (data.token) { localStorage.setItem('token', data.token); @@ -268,7 +401,7 @@ async function superAdminPage() { html += '
'; render(html); } catch (err) { - render('
Access Denied: Super Admin only
'); + render('
Access Denied: Super Admin only (or offline)
'); } } @@ -455,4 +588,4 @@ async function leaderboardPage() { } } -init(); +init(); \ No newline at end of file diff --git a/public/index.html b/public/index.html index 12f0c25..19b2418 100644 --- a/public/index.html +++ b/public/index.html @@ -29,6 +29,13 @@ background-color: var(--bs-primary); border-color: var(--bs-primary); } + #offline-indicator { + display: none; + position: fixed; + bottom: 20px; + right: 20px; + z-index: 9999; + } @@ -60,7 +67,17 @@ +
+ + Offline Mode: Data is being saved locally and will sync when online. +
+ - + + \ No newline at end of file