File size: 8,633 Bytes
abd437d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 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) });
})();