Read this before you change a Worker, a theme file, or a P&L formula. It is the context of the running estate — not a sign-in tutorial.
1
A UK Shopify merchant. Customers see the shop, the store mailbox, and track. Everything else is back office.
2
profit-admin is the desk product. business-ops is the Our Tech instance and the agent workspace.
3
P&L stays in Python. Decisions stay in D1. Access is not membership.
4
C4 diagrams: estate context, then the desk containers and components.
Our Tech Accessories is a UK-only Shopify shop
(ourtechaccessories.com). It sells everyday tech accessories
(chargers, MagSafe banks, mounts, cases). There is no high-street stock.
A partner packs the box — mostly CJ Dropshipping, with leftover
AliExpress / DSers fulfilments. James is still the merchant:
the card charge, the brand, and “where is my order?” sit here.
The public face is three things: the storefront, the store mailbox,
and /pages/track. Profit Admin, Meta Ads Manager, supplier consoles,
and this git workspace are operator surfaces. Customers never log into those.
flowchart TB
subgraph public [Customer-visible]
store["Shopify storefront"]
hello["Store mailbox"]
track["/pages/track"]
end
subgraph edge [Edge]
mx["Mail forwarding"]
proxy["track-proxy Worker"]
access["Cloudflare Access"]
end
subgraph back [Back office]
desk["Profit Admin"]
gmail["Store Gmail + desk Assist"]
meta["Meta ads + Pixel"]
cj["CJ / DSers / Alibaba"]
agent["Cursor in business-ops"]
end
store --> meta
store --> cj
hello --> mx --> gmail
track --> proxy --> cj
access --> desk
desk --> store
desk --> meta
desk --> cj
agent --> desk
agent --> store
agent --> meta
Money, parcels, and mail each have one owner. Do not add a second path that the customer or the P&L will see twice.
| Repo | Job | Do not |
|---|---|---|
JamesMMiller/profit-admin |
The desk product: Worker gateway, FastAPI engine, React UI, D1.
Submodule at apps/profit-admin.
|
Put Our Tech secrets, theme edits, or agent skills here. |
JamesMMiller/business-ops |
The Our Tech instance: local configuration, skills, storefront ops, track-proxy, inbox, estate docs, and the submodule pin. | Fork P&L into a script in this repo. Call pnl.py. |
Canonical skill edits live in skills/ and shared/skills/,
then ./scripts/sync-skill.sh. Copies under .cursor/skills/
are generated. Apps Script is paste-deployed: a git change does
nothing until someone pastes it into the store Google account.
The public briefing at
jamesmmiller.github.io/profit-admin-docs
is a copy of apps/profit-admin/site/. After you change those
files, run ./scripts/publish-docs.sh. Do not edit the Pages
repo by hand.
| Surface | Where | Notes |
|---|---|---|
| Store | ourtechaccessories.com | Apex primary. Campaign PDP handles are URL-locked. |
| Track | /pages/track | apps/track-proxy — not the ops desk. Keeps the supplier credential off the storefront. |
| Store mailbox → forwarding → store Gmail | Desk Mail Assist drafts pitch/FAQ/deal and tracking; you send. The old inbox bot is retired once desk drafts have proven themselves. | |
| Desk production | desk.ourtechaccessories.com | Access, then D1 membership. Container cold-start can take a minute. |
| Desk staging | Separate staging desk (address kept in the private operator notes) | Desk development only. Own Container. Shared D1, namespaced by environment. |
| Laptop engine | http://127.0.0.1:8787 |
No Worker. Seed owner from the business-ops local configuration. |
The Android Profit Admin PWA is retired. Phone and laptop use the hosted desk.
Worker ota-profit-admin only redirects there.
Ads → Performance defaults to the whole Meta ad account. Split by campaign, ad set, or ad overlays that level on the daily totals. Intra-day lists every ad that delivered in the current scope, with a day sparkline plus spend, share, purchases, ROAS, and CTR. Click a row to plot that ad on the hourly chart (up to six; four when comparing two periods). Dashed lines are the comparison set. Views, named subsets, SKUs, and object pickers live under Advanced — they are not the first screen.
Empty object ids on Insights means account-level. The same rule applies
to the machine API split field
(campaign / adset / ad).
Mail shows the store inbox for an owned address such as the store mailbox. Genuine customer mail that still needs a reply is listed first. Filters include Assist drafts and Needs you. Assist is two-tier triage: a cheap path drafts pitch, FAQ, and volume-deal replies from built-in seed templates; a tool-using agent drafts tracking replies from Shopify and track-proxy facts; refunds, returns, and damage escalate with no draft by default. Mail → Assist has presets (safe drafts, auto cheap, human only, paused), per-category action chips, and an effective-routing summary (auto-send needs an explicit master switch; refund never auto-sends). You still send through the Gmail API unless a category is set to auto-send. A scheduled poll drafts replies in the background. A saved reply can also be added through the API. Edit a template under Mail → Templates (built-ins save as an org override with the same id). The mailbox is still Gmail, with the store address forwarded to it. Once desk drafts have proven themselves, the old inbox bot is retired; fulfilment “on its way” mail goes track-proxy → Profit Admin. The Gmail connection is stored in D1, so a Container replace does not drop the mailbox connection. Empty saves never clear the refresh token or owned addresses; only Disconnect deletes the row.
Components, sequence diagrams, the draft lifecycle, and a “no drafts appearing” checklist are on the Mail system page.
track-proxy verifies the webhook, caches CJ, and can fire branded “on its way” mail.unitCost). pnl.py is the only formula.
Meta has no spend webhook — Ads can lag 15 minutes. A new webhook order may sit on
unitCost until the next full Refresh quotes freight. Never invent cost on a Worker-only path.
Finance on the desk is that same P&L, summed for a period (all time, this month, last 7 days, last 30 days) against the matching prior window. It is a management view, not a tax computation. Bank readings, and any large debtor or creditor, are figures an owner types in. A reading can be removed, and it can store the all-time profit beside the balance. There is no bank feed. Customers pay at checkout, so the desk does not keep an aged debtor ledger.
| Data | System of record | Ephemeral / derived |
|---|---|---|
| Users, orgs, roles, API keys, decisions | D1 on the Worker | Engine disk is not a decision log. MASTER_CONTEXT.md is not either. |
| P&L, snapshots, clocks, views, watches state | Engine org files (/data/… hosted, outputs/profit-admin/org/ laptop) |
The engine sleeps after 2 minutes of quiet. A long Refresh stays up until it finishes. Production's 15-minute cron wakes it; staging runs only the mail automation, hourly. Opening the desk does not wake it — the page is a Worker asset. The first API call after sleep can take about a minute. The engine disk is a cache and is empty after sleep: the last good copy of each org's desk files lives in that org's DeskHub Durable Object and is seeded back. R2 is not required; it is optional snapshot history only if a bucket is bound. |
| Recorded bank readings and debtor / creditor notes | D1 finance_bank_entries and finance_balances |
Not a bank feed. Sales and profit on the Finance tab are summed from the engine P&L. |
| Org membership vs “can reach the hostname” | D1 membership + usable subscription | Cloudflare Access only answers “is this email allowed in?” |
| Store catalog, checkout, theme | Shopify | Git has theme sources and skills; live MAIN is a deploy. |
| Ad objects and spend | Meta | Desk Insights are a pull. New ads stay PAUSED. |
| Brand voice, dated ops notes | Local MASTER_CONTEXT.md Changelog |
Read headings; do not ingest the whole file. Decisions that matter on the desk go to D1. |
apps/profit-admin/pnl.py is the only formula.
Do not reimplement contribution, MER, or ROAS in Worker JS.
POST /api/v1/decisions only.
Do not prepend MASTER_CONTEXT.md. Staging and production share one D1;
rows carry environment.
/api/v1.
site/ and run
./scripts/publish-docs.sh.
1:1.
.xlsx watch, Apps Script, theme paste.
Humans sign in through Cloudflare Access (Google). A separate operator-only fallback exists
and is documented privately. The Worker passes the verified identity, role and organisation
on to the engine, and in the hosted setup the engine refuses requests that did not come
through the Worker (except public paths). Viewer POST to Refresh / Settings is 403.
Machines: POST /api/v1/oauth/token then Bearer access token
(or a long-lived API key for scripts). OpenAPI is at $BASE/docs.
Cloudflare admin credentials are for deployment and Access administration only — never the desk Bearer.
| You want… | Change… |
|---|---|
| P&L, collectors, Refresh, desk UI | apps/profit-admin/ — then pin the submodule in business-ops. If the public briefing should describe the change, edit site/ and run ./scripts/publish-docs.sh |
| Public estate briefing / C4 | apps/profit-admin/site/ then ./scripts/publish-docs.sh (pushes JamesMMiller/profit-admin-docs) |
| Track page / fulfilment mail | page.track.liquid + apps/track-proxy + paste the mailer if HTML changed |
| PDP / homepage | shopify-store skill, dry-run first |
| Inbox FAQ / deal JSON | store-inbox skill, then paste Apps Script / upsert the page |
| Paused Meta ads | meta-ad-builder into an existing ad set |
| Supplier listing research | cj-dropshipping, tvc-mall, aliexpress-ds, alibaba-com — do not auto-publish |
profit-admin skill — only when you are calling the machine API.
Longer product tutorials stay in docs/tutorial. They assume you already have this map.