Anja Gutierrez
Production · in daily use

Ops Hub v2

An operations portal for out-of-home transit advertising — 860+ campaigns, 1.3 MB, zero build tools.

React 18 (CDN)Babel StandaloneTailwind CSSThree.jsCloudflare Workers + KVIndexedDBGoogle Sheets API
860+Campaigns managed
25K+Lines of code
1.3 MBTotal app size
0Build tools
8IndexedDB stores
~50Persisted state keys
2Access roles
280+Work orders tracked

What it is

The system a transit advertising operation actually runs on: campaign tracking, installation scheduling, hold management, material logistics, proof-of-posting audits, and multi-carrier shipment tracking.

It ships as a single-page app with no build step at all — React and Tailwind come from a CDN, JSX is transpiled in the browser by Babel Standalone, and deployment is a file upload. No bundler, no Node on the server, no dependency tree to keep patched. That was a deliberate architectural choice: the app has to outlive my attention, and a build pipeline is the thing that rots first.

On top of that sits Victor, an AI concierge with deterministic intelligence — it answers from live campaign state (“tell me about the shipment for that Western Union run”) rather than hallucinating from a prompt.

Architecture notes

The decisions that carry the app.

Four-layer persistence

IndexedDB for bulk records, localStorage for settings and overrides, a Cloudflare KV mirror for cross-device sync, and Google Sheets as the upstream source of truth. Merge-on-read, so a stale layer heals instead of winning.

Canvas gear sidebar

The primary navigation is a hand-rendered HTML5 canvas gear assembly, not a component library. It draws in one pass and costs nothing on re-render.

Admin / viewer RBAC

Two roles gate every mutation. The mobile surface was deliberately retired once its only gate turned out to be a forgeable localStorage flag — shipping less was the correct fix.

The code

Pulled straight from the production codebase — unedited except for trimming.

dataBackup.js javascript
A prefix sweep, not a curated key list. Replacing a laptop wipes everything this app knows, so it exports one self-contained JSON. The subtle part is the comment: the pre-existing curated SETTINGS_KEYS list covered only 18 of the ~50 keys actually in use, because curated lists drift as features land. Sweeping by prefix and excluding two keys by name inverts the default — new state is backed up automatically, and the session token is explicitly denied so a login never rides along to another machine.
    // localStorage capture is a PREFIX SWEEP, not a curated key list. A curated
    // list is what drifts — the existing SETTINGS_KEYS covers only 18 of ~50 keys
    // actually in use. Everything this app writes is stap_/STAP_ prefixed.
    var LS_PREFIXES = ['stap_', 'STAP_'];
    var LS_EXCLUDE = [
        'STAP_SESSION',        // login session + role — must never transfer machines
        'stap_field_last_push' // meaningless off-machine
    ];

    function isBackupKey(key) {
        if (!key) return false;
        if (key.indexOf('demo_') === 0) return false;      // demo app state
        if (LS_EXCLUDE.indexOf(key) !== -1) return false;
        for (var i = 0; i < LS_PREFIXES.length; i++) {
            if (key.indexOf(LS_PREFIXES[i]) === 0) return true;
        }
        return false;
    }

    function collectLocalStorage() {
        var out = {};
        try {
            for (var i = 0; i < localStorage.length; i++) {
                var k = localStorage.key(i);
                if (!isBackupKey(k)) continue;
                var v = localStorage.getItem(k);
                if (v != null) out[k] = v;
            }
        } catch (e) {
            console.warn('localStorage sweep failed:', e);
        }
        return out;
index.html — handleWipeDevice() javascript
Ordering that actually matters. Built for handing a work laptop back. deleteDatabase() only completes once every connection to the database closes — including this page's own — so calling it alone can silently no-op and leave the data sitting there. Clearing every store first guarantees the data is gone regardless, and enumerating the databases means a store added next year is still wiped.
// 1. IndexedDB — clear every store first, THEN drop the database. Clearing
//    first guarantees the data is gone even if deleteDatabase() is blocked by
//    this page's own open connection (deleteDatabase only completes on close).
try {
  if (window.STAP_IDB && window.STAP_IDB.listStores) {
    var stores = await window.STAP_IDB.listStores();
    for (var i = 0; i < stores.length; i++) {
      try { await window.STAP_IDB.clear(stores[i]); } catch (e) {}
    }
  }
} catch (e) { console.warn('Wipe: IDB store clear failed:', e); }

try {
  if (window.indexedDB) {
    if (indexedDB.databases) {
      // Enumerate — never a hardcoded subset. A store added later must not survive.
      var dbs = await indexedDB.databases();
      dbs.forEach(function (d) { if (d && d.name) indexedDB.deleteDatabase(d.name); });
    } else {
      indexedDB.deleteDatabase('stap_ops_hub');
    }
  }
} catch (e) { console.warn('Wipe: IDB delete failed:', e); }

Screens

From the running application.

Ops Hub v2 dashboard in dark mode
Campaign dashboard — installation progress, holds, and material status.
Victor AI concierge chat interface
Victor AI — answers grounded in live campaign records, not a static prompt.

Want to see more of this one?

The repository is private, but I'm glad to walk through the codebase live — architecture, tradeoffs, the parts that went wrong first.