elog-book / runtime-browser.js
johnpitteera's picture
eLOG Book browser demo
abd437d verified
Raw History Blame Contribute Delete
8.63 kB
// eLOG Book browser demo runtime: implements window.claude.use(...) with data kept in this
// browser's localStorage, plus demo "people" the visitor can switch between. No server.
(function () {
'use strict';
const KEY = 'elog-demo-db-v1', WHO = 'elog-demo-person';
const J = o => JSON.parse(JSON.stringify(o));
const PEOPLE = [
{ id: 'u_demo_admin', name: 'Demo Administrator', roles: ['Administrator'], admin: true },
{ id: 'u_demo_op', name: 'Asha Operator', roles: ['Operator'], supervisor: 'u_demo_sup', admin: false },
{ id: 'u_demo_sup', name: 'Ravi Supervisor', roles: ['Supervisor'], admin: false },
{ id: 'u_demo_qa', name: 'Meera QA', roles: ['QA'], admin: false },
{ id: 'u_demo_eng', name: 'Karan Engineering', roles: ['Engineering'], admin: false },
];
const me = () => PEOPLE.find(p => p.id === (safeGet(WHO) || 'u_demo_op')) || PEOPLE[1];
function safeGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
function safeSet(k, v) { try { localStorage.setItem(k, v); return true; } catch (e) { return false; } }
// ---- store
let store = new Map();
function load() { try { const o = JSON.parse(safeGet(KEY) || 'null'); store = new Map(o ? Object.entries(o) : []); } catch (e) { store = new Map(); } if (!store.size) seed(); }
function persist() { if (!safeSet(KEY, JSON.stringify(Object.fromEntries(store)))) { console.warn('eLOG demo: browser storage is full or blocked'); } }
function seed() {
const S = window.ELOG_DEMO_SEED || {};
store.set('cfg/main', { siteName: 'Hugging Face browser demo', demoMode: false, roles: [] });
(S.assets || []).forEach(a => store.set('cfg/main/assets/' + a.id, a));
(S.masters || []).forEach(m => store.set('cfg/main/masters/' + m.id, m));
store.set('cfg/main/schedules/s-env-rm105', { tpl: 'ENV', assetId: 'RM-105', freq: 'Per shift' });
store.set('cfg/main/schedules/s-bal-in07', { tpl: 'BAL', assetId: 'IN-BAL-07', freq: 'Daily' });
PEOPLE.forEach(p => store.set('cfg/main/roles/' + p.id, { roles: p.roles, supervisor: p.supervisor || '', active: true, dept: 'Production - Oral Solids' }));
persist();
}
load();
const listeners = [];
const colOf = p => p.split('/').slice(0, -1).join('/');
const META = { fromCache: false, hasPendingWrites: false };
const snapDoc = p => { const e = store.get(p); return { id: p.split('/').pop(), exists: !!e, data: () => e ? J(e) : undefined, metadata: META }; };
function runQuery(q) {
let docs = Array.from(store.keys()).filter(p => colOf(p) === q.path).sort().map(snapDoc);
for (const [f, op, v] of q.wh) docs = docs.filter(d => { const x = d.data()[f]; switch (op) { case '==': return x === v; case '!=': return x !== v; case '<': return x < v; case '<=': return x <= v; case '>': return x > v; case '>=': return x >= v; case 'in': return (v || []).includes(x); case 'not-in': return !(v || []).includes(x); case 'array-contains': return Array.isArray(x) && x.includes(v); } return true; });
if (q.ob) { const [f, dir] = q.ob; const has = docs.filter(d => d.data()[f] != null), no = docs.filter(d => d.data()[f] == null); has.sort((a, b) => { const x = a.data()[f], y = b.data()[f]; const c = x < y ? -1 : x > y ? 1 : 0; return dir === 'desc' ? -c : c; }); docs = has.concat(no); }
if (q.lim) docs = docs.slice(0, q.lim);
return { docs, size: docs.length, empty: !docs.length, docChanges: () => [], metadata: META };
}
function notify() { setTimeout(() => listeners.forEach(l => { if (!l.dead) l.next(l.kind === 'doc' ? snapDoc(l.path) : runQuery(l.q)); }), 0); }
window.addEventListener('storage', e => { if (e.key === KEY) { load(); notify(); } });
function write(fn) { return new Promise((res, rej) => setTimeout(() => { try { fn(); } catch (e) { rej({ code: 'invalid_argument', message: String(e.message || e) }); return; } persist(); notify(); res(); }, 0)); }
function merge(a, b) { for (const k in b) { if (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k]) && a[k] && typeof a[k] === 'object' && !Array.isArray(a[k])) merge(a[k], b[k]); else a[k] = J(b[k]); } }
const SEG = /^(?!\.\.?$)[A-Za-z0-9_\-.~:@+]{1,200}$/;
function check(p, even) { const s = String(p || '').split('/'); if (!p || !s.every(x => SEG.test(x)) || (s.length % 2 === 0) !== even) throw new TypeError('Bad path: ' + p); }
const canWriteCfg = () => me().admin;
function guard(path) { if ((path.startsWith('cfg/') || path === 'cfg/main') && !canWriteCfg()) throw new Error('Only the Demo Administrator can change setup. Switch person at the top of the page.'); if (path.startsWith('delegations/') && path.split('/')[1] !== me().id && !me().admin) throw new Error('You can only set your own delegation.'); }
const newId = () => 'd' + Math.random().toString(36).slice(2, 12) + Date.now().toString(36).slice(-4);
function docRef(path) {
check(path, true);
return { id: path.split('/').pop(), path,
get: () => Promise.resolve(snapDoc(path)),
set: d => write(() => { guard(path); store.set(path, J(d)); }),
update: d => write(() => { guard(path); if (!store.has(path)) throw new Error('missing document'); const x = J(store.get(path)); merge(x, d); store.set(path, x); }),
delete: () => write(() => { guard(path); store.delete(path); }),
acquire: async () => ({ acquired: true }),
onSnapshot: next => { const l = { kind: 'doc', path, next }; listeners.push(l); setTimeout(() => !l.dead && next(snapDoc(path)), 0); return () => { l.dead = true; }; },
collection: sub => colRef(path + '/' + sub) };
}
function query(q) {
return { where: (f, op, v) => query(Object.assign({}, q, { wh: q.wh.concat([[f, op, v]]) })), orderBy: (f, d) => query(Object.assign({}, q, { ob: [f, d || 'asc'] })), limit: n => query(Object.assign({}, q, { lim: n })),
get: () => Promise.resolve(runQuery(q)),
onSnapshot: next => { const l = { kind: 'col', q, next }; listeners.push(l); setTimeout(() => !l.dead && next(runQuery(q)), 0); return () => { l.dead = true; }; } };
}
function colRef(path) { check(path, false); return Object.assign(query({ path, wh: [], ob: null, lim: 0 }), { path, doc: id => docRef(path + '/' + (id || newId())), add: d => { const r = docRef(path + '/' + newId()); return r.set(d).then(() => r); } }); }
const db = Object.freeze({ doc: docRef, collection: colRef });
// ---- people
const COLORS = ['#1B5E8C', '#1D7549', '#8F5B00', '#7A3E9D', '#AE2A21'];
function avatar(p) { const i = PEOPLE.indexOf(p); const ini = p.name.split(' ').map(w => w[0]).join('').slice(0, 2); return 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" rx="32" fill="' + COLORS[(i + 5) % 5] + '"/><text x="32" y="41" font-family="Arial,sans-serif" font-size="24" font-weight="700" fill="#fff" text-anchor="middle">' + ini + '</text></svg>'); }
const prof = (id) => { const p = PEOPLE.find(x => x.id === id); return { id, name: p ? p.name : '', avatarUrl: p ? avatar(p) : '', color: '#888', email: null, isMe: id === me().id, guest: false }; };
const user = Object.freeze({
isOwner: async () => me().admin, canEdit: async () => me().admin, can: async n => n === 'data.write' ? true : n === 'files.write' ? me().admin : false,
me: async () => { const p = me(); return { id: p.id, name: p.name, avatarUrl: avatar(p), color: '#888', email: null, isOwner: p.admin, canEdit: p.admin }; },
id: async () => me().id, name: async () => me().name, avatarUrl: async () => avatar(me()), email: async () => null,
profiles: async ids => { const o = {}; [].concat(ids || []).forEach(i => { o[i] = prof(i); }); return o; },
search: async q => PEOPLE.filter(p => !q || p.name.toLowerCase().includes(String(q).toLowerCase())).map(p => prof(p.id)),
});
const downloads = Object.freeze({ save: async r => { const b = r.data instanceof Blob ? r.data : new Blob([r.data]); const u = URL.createObjectURL(b); const a = document.createElement('a'); a.href = u; a.download = r.filename || 'download'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(u), 10000); return { status: 'saved' }; } });
window.ELOG_DEMO = {
people: PEOPLE.map(p => ({ id: p.id, name: p.name, roles: p.roles })),
current: () => me().id,
switchTo: id => { safeSet(WHO, id); location.reload(); },
reset: () => { try { localStorage.removeItem(KEY); } catch (e) {} location.reload(); },
};
const caps = { db, user, downloads };
window.claude = Object.freeze({ use: name => Promise.resolve(caps[name] || null) });
})();