Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
What this is
A live GPS dispatch tracking tool for trade businesses. Technicians share their location via their phone browser (no download required). The dispatcher sees every technician on a live map. Clients get an automatic SMS with a tracking link when the technician is 15 minutes away.
Day 1: Set up accounts (do these first)README 전문 보기 ↗A live GPS dispatch tracking tool for trade businesses. Technicians share their location via their phone browser (no download required). The dispatcher sees every technician on a live map. Clients get an automatic SMS with a tracking link once the technician is close by (within about 2km).
A separate, additive industrial sector also exists for multi-site/heavy-equipment operations — site check-ins instead of a live map, safety incident tracking, asset telemetry. See "Agent Expansion Pack (Track A) + Industrial Sector (Track B)" below for what it is and how to set it up; it isn't a variant of the description above, it's a genuinely different console with its own data model.
Found 2026-10-24 via a Supabase platform notice sitting unread in the
account owner's inbox ([email protected], "Final notice: new tables
in 'public' need grants from October 30") — surfaced here so it isn't
missed by a future session that only reads project files, not email.
What changes, and when: from 2026-10-30 onward, Supabase stops
auto-granting Data API access (supabase-js/PostgREST/GraphQL) to any
newly created table in the public schema. Every create table
statement written after that date needs its own grant statements in
the same delta file, or the table silently returns "permission
denied" to every caller — including service_role, per Supabase's own
notice, so this isn't just an anon/authenticated concern this time.
Existing tables are explicitly unaffected — confirmed directly from
Supabase's notice ("Your existing tables are not affected. They keep
their current grants") — so nothing already in supabase_schema.sql or
any already-applied delta needs touching retroactively.
Template for any new table from now on:
create table public.your_table (...);
grant select on public.your_table to anon; -- only if a public/client page needs anon read
grant select, insert, update, delete on public.your_table to authenticated;
grant select, insert, update, delete on public.your_table to service_role;
Narrow the anon/authenticated grants to match this project's existing
RLS-plus-column-grant conventions (see SECURITY_NOTES.md) — the
service_role grant is the one this notice makes newly mandatory;
everything else about scoping access correctly is unchanged.
minerva-prod, region: Asia Pacific (Sydney)supabase_schema.sql → click Runtechnicians tableDeno.env.get('SUPABASE_SERVICE_ROLE_KEY') — so you won't
usually need to set or paste it anywhere yourself. The one rule that
matters: never put it in .env.local or any client-side code, since
that would ship it in the browser bundle to every visitor.)SECURITY_NOTES.md for why)pk.eyJ1)missed-call-webhook is deployed
(see "Deploy Supabase Edge Functions" below), go to your number's
configuration page in the Twilio Console → Voice Configuration → set
"A call comes in" to Webhook, paste
https://YOUR_PROJECT_REF.supabase.co/functions/v1/missed-call-webhook,
method HTTP POST. Also set the business's number in the businesses
table (twilio_number column, E.164 format e.g. +61412345678) so the
webhook can look up the right business name for the auto-reply SMS.incoming-sms-webhook is deployed, go to the same number's
configuration page → Messaging Configuration → set "A message
comes in" to Webhook, paste
https://YOUR_PROJECT_REF.supabase.co/functions/v1/incoming-sms-webhook,
method HTTP POST. Same twilio_number column as above does the
business lookup — no extra setup if you already did step 4. A matched
reply gets appended to that client's active job notes and posted to
Slack (if configured); an unmatched one still posts to Slack with the
raw number. A client who replies STOP/UNSUBSCRIBE/CANCEL/END/QUIT is
opted out of that business's future SMS instead (see
SMS_COMPLIANCE_NOTES.md) and gets a one-time confirmation text back;
START/UNSTOP/YES opts them back in.STRIPE_SECRET_KEY
Supabase secret below. Checkout itself happens entirely server-side (a
redirect to a Stripe-hosted session URL).VITE_STRIPE_PUBLISHABLE_KEY in
.env.local. Stale note removed 2026-10-03: this used to say the
publishable key "isn't needed anywhere in this app," which was true
before InvoiceView.jsx grew an optional client-side "Pay now with card"
button (2026-09-14, via Stripe Elements) — that feature silently no-ops
without this key set.# Clone or create your repo, then:
npm install
# Copy the example env file and fill in your real values
cp .env.local.example .env.local
# Edit .env.local with your Supabase and Mapbox keys (Stripe keys are
# server-side only — set via `supabase secrets set`, never in .env.local)
# Start the dev server
npm run dev
# Opens at http://localhost:5173
.env.local)Correction (2026-10-08): this note previously said every secret/
function deploy here went through a "raw Management API" with a pasted
value, and invited pasting one to Claude — that's out of date with a hard
rule this project has hit and corrected multiple times since: Claude
never takes a pasted credential and uses it directly, and never runs a
deploy itself even when it technically could. The actual, current
process: Claude's own execution environment never has Supabase CLI
authentication, so for any secret or function deploy it hands you the
exact npx supabase ... command below to run yourself, in your own
terminal, where supabase login/supabase link (or a one-time
interactive credential prompt) can actually work. The CLI steps below are
that same real path — not a hypothetical "if you're setting this up
somewhere else."
Install the Supabase CLI first:
npm install -g supabase
supabase login
supabase link --project-ref YOUR_PROJECT_REF
Set secrets (replace with your real values):
supabase secrets set TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxx
supabase secrets set TWILIO_AUTH_TOKEN=your_auth_token
supabase secrets set TWILIO_PHONE_NUMBER=+61412345678
supabase secrets set STRIPE_SECRET_KEY=sk_live_xxx
supabase secrets set STRIPE_PRICE_ID_STARTER=price_xxx
supabase secrets set STRIPE_PRICE_ID_STD=price_xxx
supabase secrets set STRIPE_PRICE_ID_PRO=price_xxx
supabase secrets set APP_URL=https://minerva-green.vercel.app
supabase secrets set SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_xxx
supabase secrets set ANTHROPIC_API_KEY=sk-ant-xxx
(ANTHROPIC_API_KEY was only needed for ai-intake-chat — the AI
lead-triage widget embedded on a business's own website — early on, but 16
functions reference it as of 2026-10-03: outreach drafting/follow-ups,
quote drafting, growth-marketing drafts, the voice intake agent, and
several others. Every one of them has an honest, explicitly-tested
non-AI fallback when this key is unset — nothing breaks or looks broken
without it — but AI personalization across most of the app's "drafts
something for a human to review" features stays off until it's set.)
(STRIPE_WEBHOOK_SECRET comes from Stripe Dashboard → Developers → Webhooks,
after you create the endpoint in the next step — come back and set it once
you have it.)
(You don't need to set SUPABASE_URL or SUPABASE_ANON_KEY — Supabase
injects both into every Edge Function automatically at runtime. You'll only
type those two in manually inside .env.local, for the frontend.)
Deploy all functions:
supabase functions deploy send-eta-sms
supabase functions deploy send-setup-sms
supabase functions deploy send-completion-sms
supabase functions deploy send-invoice-sms
supabase functions deploy create-checkout-session
supabase functions deploy sync-technician-billing
supabase functions deploy stripe-webhook --no-verify-jwt
supabase functions deploy missed-call-webhook --no-verify-jwt
supabase functions deploy ai-intake-chat
supabase functions deploy notify-slack
supabase functions deploy calendar-feed --no-verify-jwt
supabase functions deploy nurture-stale-leads
supabase functions deploy chase-unpaid-invoices
supabase functions deploy auto-assign-technician
supabase functions deploy daily-digest
supabase functions deploy retention-checkin
supabase functions deploy reconcile-billing
supabase functions deploy check-inventory-levels
supabase functions deploy generate-growth-drafts
supabase functions deploy launch-ad-campaign
supabase functions deploy send-growth-message
supabase functions deploy check-credential-expiry
supabase functions deploy detect-wasted-trips
supabase functions deploy check-weather-risk
supabase functions deploy send-weather-reschedule-sms
supabase functions deploy send-referral-code-sms
supabase functions deploy update-technician-workload
supabase functions deploy reconcile-technician-state
(sync-technician-billing is called automatically by the app — from a
technician's phone on their first GPS push, and from the dispatcher when
removing a technician — to keep the Stripe subscription quantity in sync
with the number of technicians actually connected. You don't call it
manually.)
The last eighteen functions above power the autonomous-operations layer (optional — everything else in Minerva works without them):
notify-slack — generic internal notifier. Called by the other
functions to post an alert to a business's own Slack channel. No-op
(not an error) if that business hasn't set a webhook URL.calendar-feed — public ICS feed of a business's scheduled jobs.
Deployed with --no-verify-jwt since calendar apps fetch it directly
with no Supabase auth header (same reason as stripe-webhook and
missed-call-webhook).nurture-stale-leads, chase-unpaid-invoices, daily-digest,
retention-checkin, reconcile-billing, check-inventory-levels
— scheduled agents, triggered by pg_cron (see below), not called
manually.auto-assign-technician — event-driven agent, triggered by a
Postgres trigger on job insert (see below), not called manually.generate-growth-drafts — scheduled agent, triggered by pg_cron
(see below). launch-ad-campaign and send-growth-message are
never scheduled — they only run when a business owner clicks
Approve on a draft in the dispatcher's Marketing tab. See "Growth
pillar (Sales & Marketing)" below.check-credential-expiry, detect-wasted-trips,
check-weather-risk, update-technician-workload — scheduled
agents, triggered by pg_cron (see below), not called manually.send-weather-reschedule-sms — click-only: sends an approved
weather_reschedule_drafts row's client SMS from the dispatcher's
Weather tab. Never runs on its own.send-referral-code-sms — fire-and-forget: called by the frontend
the moment an invoice is marked paid, not scheduled. Invoicing itself is
a Pro-tier-only feature (see pricing below), so in practice this only
ever fires for Pro-tier businesses — there's no separate tier check in
the function itself, it just can't be reached any other way.Details on the three newest scheduled agents:
retention-checkin (weekly) — Customer Management pillar. Finds
clients whose last completed job was 30-60 days ago with no newer job or
lead since, and sends one low-pressure "need anything else?" SMS. Existing-
client relationship maintenance, not new-prospect outreach.reconcile-billing (daily) — Financial Automation pillar. Compares
each Pro-tier business's live Stripe subscription quantity against the
actual count of connected technicians, and Slack-alerts on any mismatch.
Does not auto-correct billing — a human should look at why before
changing what a client is charged.check-inventory-levels (daily) — Supply & Inventory pillar. Watches
stock levels entered on the Inventory tab and Slack-alerts once per
low-stock episode when an item drops to/below its reorder threshold.
Minerva has no vendor accounts to actually place orders through, so this
is alerting only, not automated purchasing.generate-growth-drafts (weekly, Sundays) — Sales & Marketing
("Growth") pillar, Pro tier only. Writes two kinds of marketing_drafts
per business, never more than one unreviewed draft of each type at a
time: an ad campaign (Meta ad copy + suggested daily budget targeting
the suburb with the most completed jobs in the last 90 days) and a
win-back SMS (for leads quoted 14+ days ago that never booked, pulled
only from that business's own leads table). This function only ever
writes drafts — it never spends money and never sends a message.
Everything shows up as a pending card on the Marketing tab for the
owner to Approve or Reject.
ad_campaign draft calls launch-ad-campaign, which uses
the business's own Meta access token / ad account / Page ID (set in
Settings) to create a campaign → ad set → creative → ad via the Meta
Marketing API. Minerva never holds or spends from a shared ad account.outreach_sms draft calls send-growth-message, which
sends the already-written, already-reviewed message via Twilio to the
recipients stored on that draft — existing leads only, never a
purchased or scraped list.rejected — nothing else happens.Details on the six newest agents/features (added on top of everything above — purely additive, nothing below changes any existing behaviour):
check-credential-expiry (daily) — Licence/Ticket Expiry Guardian,
Pro tier only. Watches technician_credentials (expiry dates you enter
on the dispatcher's Credentials tab) and Slack-alerts the dispatcher
at 30/14/7 days out (each threshold only fires once per credential), plus
an urgent same-day nudge if a technician who's currently on a job has a
credential that's expired or expiring within 3 days. Never texts the
technician or a client, never blocks assignment — it's a heads-up, not an
enforcement gate. Technicians also see a small read-only "expiring soon"
banner on their own tracking page.detect-wasted-trips (every 15 minutes) — Wasted-Trip/No-Show Proof
Agent. Looks for scheduled jobs where GPS shows the technician was
genuinely on-site (within ~150m of the client address for 15+ minutes)
but the job was never started — i.e. a no-show or a locked gate, not a
technician who just hasn't left yet. Marks the job, texts the client a
no-pressure reschedule prompt, and Slack-alerts the dispatcher. A running
count shows on the Jobs tab.check-weather-risk (daily) — Weather-Risk Reschedule Agent. For
trade types you mark as weather-sensitive (⚙️ Settings), checks
tomorrow's forecast (free Open-Meteo API, no signup needed) for each
scheduled job's location. If rain/wind/heat looks bad, it writes a
pending draft to the dispatcher's Weather tab — it does not text
the client itself. A human has to click Approve & Send for the
reschedule SMS to actually go out (same human-approval-gate pattern as
the Marketing tab), or Dismiss to ignore it. Available on every
tier, not just Pro, since it's framed as a safety feature.update-technician-workload (daily) — Fair-Rotation/Burnout Guard.
Recomputes two rolling 7-day signals per technician from existing GPS
breadcrumbs and job data: an estimated hours-worked figure (shown next
to their name on the dispatcher's roster) and a count of recent
emergency-tagged jobs. If estimated hours cross 55/week, Slack-alerts the
dispatcher once (re-alerts after 7 days if still over) — purely an
internal nudge, never sent to the technician, never changes pay or
scheduling automatically. The emergency-job count also feeds a soft
tiebreak into auto-assign-technician: among comparably-close free
technicians, one who's had fewer recent emergency callouts is preferred
— a genuinely closer technician still wins regardless. New jobs can
optionally be tagged Emergency/Routine on the Add Job form, and
the AI intake widget's own urgency classification carries through
automatically when a lead converts to a job.In the business's Dispatcher view, click ⚙️ Settings → paste in a Slack "Incoming Webhook" URL. Get one from your own Slack workspace: Slack → Settings & administration → Manage apps → search "Incoming Webhooks" → Add to Slack → choose a channel → copy the webhook URL. Once saved, new leads, auto-dispatches, payment reminders, and a weekly activity digest all post to that channel automatically. Leave it blank to opt out — nothing else in Minerva depends on it.
In the Dispatcher view ⚙️ Settings modal, copy the calendar
subscription link. In Google Calendar: Other calendars → + → From
URL → paste it. In Apple Calendar: File → New Calendar Subscription
→ paste it. The feed refreshes every ~15 minutes and lists all of that
business's scheduled jobs. Treat the link like a secret (see
SECURITY_NOTES.md) — anyone with it can see the business's job
schedule.
Any business, on any tier, can export their jobs and leads as a CSV from
the Jobs and Leads tabs (⬇ Export CSV) and import into Xero,
QuickBooks, or Excel — no tier gate on those two tabs. The Invoices
tab itself is Pro-tier only (business.subscription_tier === 'pro' in
DispatcherView.jsx) — Starter/Standard businesses don't have invoicing
at all, so there's nothing to export from that tab until upgrading to
Pro. (Corrected 2026-09-07 — this section previously claimed invoice
export was ungated on every tier, which was never true in code.)
There's also a real, working Xero OAuth integration (xero-oauth-connect,
xero-oauth-callback, xero-sync-invoice — added 2026-09-04, see "Minerva
Max batch" below), but it needs the business owner to register their own
free app at developer.xero.com first (Xero, like Intuit for QuickBooks,
requires the account holder to do this — not something that can be
completed on your behalf). Until that's done and the resulting
XERO_CLIENT_ID/XERO_CLIENT_SECRET secrets are set, the "Connect Xero"
button in Settings shows a setup message instead of connecting. No
QuickBooks/Intuit equivalent is built yet — CSV export remains the only
QuickBooks path.
COMPLIANCE_TEMPLATES.md has generic starting-point templates (terms of
service, privacy notice, job sign-off, subcontractor onboarding paperwork
checklist) for the Compliance & Admin pillar. Not legal advice — have
a solicitor review and customize before using with real clients.
Pro-tier businesses get an Inventory tab in the Dispatcher view to
track stock items (name, quantity, unit, reorder threshold, usual
supplier). The check-inventory-levels agent (see below) Slack-alerts
when an item drops to/below its threshold — Minerva doesn't place orders
automatically since that would require a vendor account integration that
doesn't exist yet.
Pro-tier businesses can set up a separate onboarding checklist (Dispatcher
sidebar → 🧑🔧 Set up technician onboarding checklist), distinct from
the per-job completion checklist. It's shown once, before a technician's
very first "Start Tracking" tap, and gates on technicians. onboarding_completed_at — never shown again after that technician
completes it once.
Pro-tier businesses that want the weekly ad-campaign drafts to be launchable need to connect their own Meta ad account in ⚙️ Settings:
act_...), and Facebook Page ID.Until these are filled in, generate-growth-drafts still writes ad-campaign
drafts (they're useful to review even unlaunched), but clicking Approve &
Launch will fail with a clear "connect an ad account" message instead of
silently doing nothing. The win-back SMS draft type doesn't need any of
this — it only needs the Twilio secrets already set above.
After deploying the functions above, go to Supabase SQL Editor
and run both the AUTONOMOUS AGENTS — setup block AND the
FEATURE ADDENDUM block that follows it further down
supabase_schema.sql (skip both if you don't want this layer — everything
else works fine without it; the addendum's cron.schedule calls are what
actually turn on the six newest agents, so don't stop after just the first
block). Before running either:
YOUR_PROJECT_REF with your Supabase project ref (appears in
both blocks).YOUR_ANON_KEY with your project's anon public key (same one
already in your .env.local — safe to reuse here, see the comment in
the SQL file for why; also appears in both blocks).This enables eleven scheduled jobs — lead nurture (hourly), invoice
chasing (daily), weekly Slack digest, retention check-ins (weekly),
billing reconciliation (daily), inventory-level checks (daily), weekly
growth drafts, credential expiry checks (daily), wasted-trip detection
(every 15 min), weather-risk checks (daily), and technician workload/
burnout checks (daily) — plus one event-driven trigger (auto-dispatch —
only acts for businesses that turn it on in ⚙️ Settings).
launch-ad-campaign, send-growth-message, and send-weather-reschedule-sms
are deliberately not part of this cron block — they only ever run from a
synchronous Approve click in the Marketing/Weather tabs. send-referral-code-sms
is also not in the cron block — it's called fire-and-forget by the frontend
when an invoice is marked paid.
Then in Stripe Dashboard → Developers → Webhooks, add an endpoint:
https://YOUR_PROJECT_REF.supabase.co/functions/v1/stripe-webhook, listening
for checkout.session.completed, customer.subscription.deleted,
invoice.payment_failed, invoice.payment_succeeded,
payment_intent.succeeded (only needed if the optional "Pay now" invoice
flow is used), and charge.dispute.created + charge.dispute.closed +
charge.refunded (all 3 added 2026-10-03 — alert OPERATOR_EMAIL on a
chargeback or a full refund, since neither was visible anywhere before;
see stripe-webhook/index.ts's own header for what each does and does
not automate). Without the first one, the Customer Portal cancellation
link on the pricing page won't work — the business's Stripe subscription
ID never gets saved. If this endpoint is already registered from an
earlier setup, add any missing event types to its existing event list
rather than re-creating it — Stripe doesn't backfill new event types onto
an existing endpoint automatically.
Then, for the missed-call auto-reply, go to the Twilio Console → Phone
Numbers → Manage → Active Numbers → select the business's number →
Voice Configuration → set "A call comes in" to Webhook:
https://YOUR_PROJECT_REF.supabase.co/functions/v1/missed-call-webhook
(HTTP POST). Also make sure that number is saved in the twilio_number
column on the matching businesses row, so the webhook can look up the
right business name. --no-verify-jwt is required here too, since Twilio's
servers call this webhook directly without a Supabase auth header (same
reason stripe-webhook uses it).
Two additive builds, both optional, neither changes any existing behaviour
above. Both are documented with their own standalone schema delta files (run
these in the Supabase SQL Editor same as supabase_schema.sql — see "Deploy
Supabase Edge Functions" for the pattern) and their own cron block.
Track A reorganizes the existing trade-business product into named departments (Front Desk, Dispatch & Crew, Ledger, Watchtower, Growth — see the labels in the Dispatcher view's job queue) and adds four genuinely new features on top:
verify-checklist-photos (every 15 min) — Claude Vision
review of each new checklist photo against the checklist item it claims to
satisfy, for dispute-protection accountability (not a blocker of the
technician's own checklist completion). Falls back to
verification_status: 'unavailable' if ANTHROPIC_API_KEY isn't set.technician_incidents table,
no new function) — disputes, near-misses, or notes tied to a job/tech,
independent of the pass/fail checklist. Not yet surfaced in the Dispatcher
UI — currently a data layer only, add rows directly or via a future panel.winback-lost-leads (daily) — one autonomous re-engagement SMS for
leads marked lost 14+ days ago, mirroring nurture-stale-leads'
two-touch pattern.run-custom-workflows — general-purpose "customized via chat" agent.
Businesses define simple trigger → condition → action rules
(⚙️ Settings → Custom Workflows in the Dispatcher view) — e.g. "when a
job completes, POST to this webhook" or "when an invoice is paid over
$500, Slack-alert". Wired into three real trigger points: lead.created
(ai-intake-chat), job.completed (TechnicianView.jsx), invoice.paid
(DispatcherView.jsx). Every run is logged to workflow_runs. The cron
entry for this function is a no-op tick kept for forward-compatibility —
both supported triggers today are event-driven, not time-based.daily-digest's existing weekly Slack summary is now
grouped by department (Front Desk / Dispatch & Crew / Ledger / Watchtower)
instead of one flat message. Same underlying data, no new function.Schema: supabase_schema_delta_agent_expansion.sql. Deploy:
supabase functions deploy verify-checklist-photos
supabase functions deploy run-custom-workflows
supabase functions deploy winback-lost-leads
Track B is a new, additive industrial sector (businesses.sector,
'trade' or 'industrial') — a parallel console for heavy-equipment/B2B
site-work businesses, chosen at signup (Onboarding.jsx) and routed to
/industrial/:businessId (IndustrialDispatcherView.jsx) instead of
/dispatch/:businessId. Honesty note: this is a real, working data
model + alerting/logic layer — geofence math, maintenance-hours tracking,
safety-overlap detection are all functioning. It does not include real
external data or hardware integration (no LinkedIn/registry scraping, no
RFID/Bluetooth/drone feeds) — those endpoints are real, working ingestion
APIs with comments marking exactly where a real vendor feed would plug in.
Industrial dispatch suggestions (unlike the trade sector's
auto-assign-technician) are suggest-via-Slack, never auto-commit —
a wrong heavy-equipment dispatch is costlier to reverse than a trade
technician auto-assign.
| Function | Cadence | Role |
|---|---|---|
industrial-conductor | every 15 min + on-demand | Central Conductor — suggests nearest free asset for a new lead |
harvest-industrial-leads | ingestion webhook only | Bulk lead intake ({businessId, leads: [...], source}) |
enrich-industrial-leads | daily + on-demand | Signal & Enrich — nudges Slack for leads 24h+ old missing contact data |
monitor-asset-telemetry | ingestion webhook only | Telemetry — geofence-breach + maintenance-hours alerts |
optimize-industrial-routes | every 30 min | Route Optimizer — nearest-free-asset suggestions for unassigned sites |
track-consumables | hourly | Quartermaster — low-stock flag, once per episode |
detect-safety-hazards | every 15 min | The Warden — flags overlapping human/automated on-site activity |
sequence-handoffs | every 15 min | The Pacer — flags automated task-complete with no human follow-up |
package-client-verification | on-demand only | The Closer — bundles checkins/telemetry/incidents into a sign-off package |
verify-industrial-compliance | hourly | Sentry — 24h+ backstop escalation for unacknowledged safety incidents |
Two more industrial-relevant functions live under the asset_intelligence
Minerva Max add-on rather than this base track — detect-idle-assets
(daily) and predict-asset-maintenance (daily), both gated per-business
by that add-on. See the Minerva Max section below.
All 10 functions above now check agent_functions.enabled where
applicable and call record_agent_run() for health tracking (added
2026-09-05 — previously deployed/scheduled but invisible to
test-agent-health and the operator kill-switch; see
DEPLOYMENT_CHECKLIST_PENDING.md).
Schema: supabase_schema_delta_industrial.sql. Deploy:
supabase functions deploy industrial-conductor
supabase functions deploy harvest-industrial-leads --no-verify-jwt
supabase functions deploy enrich-industrial-leads
supabase functions deploy monitor-asset-telemetry --no-verify-jwt
supabase functions deploy optimize-industrial-routes
supabase functions deploy track-consumables
supabase functions deploy detect-safety-hazards
supabase functions deploy sequence-handoffs
supabase functions deploy package-client-verification
supabase functions deploy verify-industrial-compliance
(harvest-industrial-leads and monitor-asset-telemetry use
--no-verify-jwt since they're meant to be called by an external
ingestion source, not the Minerva frontend — same reasoning as
stripe-webhook/missed-call-webhook above. Both also require an
X-Ingestion-Key header matching that business's businesses.ingestion_key
column — see SECURITY_NOTES.md.)
Both tracks' cron schedules (10 jobs total) are in
supabase_schema_delta_agent_cron.sql — pre-filled with your real project
ref/anon key, same format as the existing AUTONOMOUS AGENTS block. Run it
once, after deploying the corresponding functions and running both delta
schema files above.
Setup-stage personalization: Onboarding now asks which
departments/features matter most to the business (businesses. feature_priorities, different checklist per sector) — purely informational,
shown back to the business, never a feature gate.
Phase 1 of a planned 5-phase build. This phase is infrastructure only — it adds two new tables and one new function, and does not change the runtime behaviour of any existing agent. Standalone schema delta files, safe to run independently of Track A/B.
agent_functions — one row per autonomous/cron-driven edge function
(seeded from both the original agents and the Track A/B ones), tracking
agent grouping (outreach / marketing / scheduling / research /
finance / design / core), enabled, and run-health data
(last_run_at, last_status, last_error, cumulative error_count).
Kill-switch — partially implemented at the time this phase shipped.
Setting a row's enabled = false is meant to let you disable a single
misbehaving agent without a code deploy. Honesty note: as of Phase 1,
no edge function actually checked its own agent_functions.enabled flag
yet — flipping it did nothing at runtime. This has since been built
(2026-09-02/03 through the ongoing audit pass, separate from Phase 1): every
autonomous/cron-driven edge function now checks agent_functions.enabled
at the top of its run and skips early when disabled — see Phase 5's
"Known follow-up" note below for the caveat that comes with it.
agent_insights — a shared scratchpad any agent (or the health-check
sweep) can write an observation to (insight_type: anomaly / pattern /
suggestion / health_alert, etc.), optionally scoped to a business_id
(nullable — some insights, like a health alert about an infra function, are
cross-business and have no single owner). Intended for later phases to read
back; nothing reads it yet besides the health-check writing to it.
test-agent-health (every 15 min) — a passive self-test: it does not
invoke any other agent (most have real side effects like sending SMS or
posting to Slack, so re-running them just to "check" would itself be
risky). Instead it reads agent_functions' run-history and flags a
function as unhealthy if it's overdue for its known cron cadence (hand-
maintained cadence map inside the function) or its error_count has
crossed 5. Since there's no single business to Slack-alert for a
cross-tenant infra issue, unhealthy functions are written to
agent_insights (and logged) instead of posted to Slack — see the
function's own header comment for the reasoning. Deduplicated so it only
re-alerts once the function has actually run again since the last alert.
record_agent_run(fn_name, status, error_msg) — the SQL function that
feeds agent_functions. Wired in as a proof-of-pattern into 3 functions
this phase (check-inventory-levels, chase-unpaid-invoices,
reconcile-billing) — fire-and-forget on both their success and error
paths — done deliberately narrow here at first to avoid touching working
production functions without individual review. Since rolled out
everywhere: every autonomous/cron-driven edge function in the codebase
(56 total) now calls record_agent_run on both its success and error
paths, not just these original 3.
Schema: supabase_schema_delta_agent_infra.sql. Deploy/run order:
# 1. Run supabase_schema_delta_agent_infra.sql in the Supabase SQL Editor
# 2. Deploy the new function
supabase functions deploy test-agent-health
# 3. Run supabase_schema_delta_agent_infra_cron.sql in the Supabase SQL Editor
Phase 2 gives the Outreach agent (nurture-stale-leads,
chase-unpaid-invoices, retention-checkin, winback-lost-leads) and the
Finance agent (reconcile-billing, check-credential-expiry) real LLM
reasoning from Claude instead of fixed copy/logic-only anomaly detection —
six functions total. No schema changes were needed; this phase is code-only
and reuses the agent_insights table Phase 1 already created.
No behaviour change today, by design. ANTHROPIC_API_KEY is not yet
configured in production (billing isn't set up on the Anthropic Console
account this project uses), so every one of these six functions is
currently running in its fallback path — the exact fixed SMS templates and
plain-English summaries they always used. Each Claude call is wrapped so a
missing key, a failed request, or an unusable draft (empty, too long, or —
for chase-unpaid-invoices — missing the required dollar amount/invoice
link) falls straight back to the original hard-coded string, never blocking
or altering the send. Nothing about this phase requires the key to be live
to be safe to deploy.
Outreach — AI-drafted copy, same voice/length as the fixed templates:
nurture-stale-leads — both the 1st and 2nd touch nurture SMS are drafted
by Claude (lead name, urgency, business name, touch number) when a key is
present.chase-unpaid-invoices — reminder copy is drafted by Claude using how
many reminders have already gone out and how overdue the invoice is, so
tone escalates (friendly → firmer) purely in wording. The escalation
LOGIC — which invoices get touched, on what cadence — is unchanged. The
draft is discarded in favour of the fixed template if it doesn't contain
the exact dollar amount and invoice link.retention-checkin — check-in SMS is drafted by Claude using the
client's last-job notes/date for light personalization.winback-lost-leads — the one-shot win-back SMS is drafted by Claude
using the lead's original job_description/urgency.All four also get record_agent_run wired in (success + error paths,
chase-unpaid-invoices already had it from Phase 1).
Finance — anomaly reasoning, now actually writing to agent_insights:
reconcile-billing — on every Stripe-vs-local seat mismatch, in addition
to the existing Slack alert, a row is now written to agent_insights
(agent: 'finance', insight_type: 'anomaly', scoped to the business).
The summary is a one-sentence Claude best-guess at the likely cause
(missed technician deactivation vs double-counted GPS ping vs Stripe-side
seat change) when a key is present, otherwise a plain-English fallback
("Stripe billing count (X) does not match locally connected technicians (Y) for <business>.") — the row is written either way.check-credential-expiry — every 30/14/7-day threshold crossing and
every urgent (on-a-job-with-an-expiring/expired credential) ping now also
writes an agent_insights row, with a one-sentence Claude note on what to
check/do first when a key is present, otherwise a plain-English fallback.This is the first phase where any function actually writes to
agent_insights — Phase 1 only had the health-check sweep writing to it;
at the time this phase shipped, agent_insights was not yet read back by
anything, so these rows were purely for later phases/humans to review
directly in the table. This has since changed: agent-council-report
reads every insight from the last 7 days into its weekly digest (Phase 4),
and generate-growth-drafts reads the latest demand_forecast insight
written by forecast-demand to surface a forecast-linked marketing
suggestion — the first real agent-to-agent (not just agent-to-human) use
of the table.
No shared code across function folders. Since every edge function
folder deploys independently via supabase functions deploy <name> with no
cross-folder imports anywhere in this codebase, the small "call Claude, fall
back gracefully" helper is duplicated inside each of the six files rather
than imported from a shared module — consistent with how this codebase
already re-implements small fetch-based helpers (e.g. notifySlack) per
function.
Deploy the six changed functions:
supabase functions deploy nurture-stale-leads
supabase functions deploy chase-unpaid-invoices
supabase functions deploy retention-checkin
supabase functions deploy winback-lost-leads
supabase functions deploy reconcile-billing
supabase functions deploy check-credential-expiry
No SQL to run for this phase — agent_insights/agent_functions and
record_agent_run already exist from supabase_schema_delta_agent_infra.sql
(Phase 1).
Phase 3 brings the Marketing agent (generate-growth-drafts,
launch-ad-campaign, send-growth-message) and the Scheduling agent
(auto-assign-technician, detect-wasted-trips, check-weather-risk,
update-technician-workload) up to the same health-tracking standard as
Phase 1/2 — seven functions total — and adds one genuinely new capability
to each pillar.
record_agent_run wired into all seven. Same fire-and-forget
success/error pattern as Phase 2. For auto-assign-technician
specifically (event-driven, multiple early-return "skipped" outcomes —
already assigned, auto-dispatch disabled, no technician available), every
early return records status: 'ok' — the function completing its check
and correctly choosing to do nothing is a healthy run, not an error.
Deliberately NOT touched: launch-ad-campaign and send-growth-message
remain callable only from a human's "Approve" click in the dispatcher's
Marketing tab — never cron, never autonomous. Only the health-tracking call
was added to these two; the human-approval trust boundary described in
SECURITY_NOTES.md ("Growth pillar") is unchanged. Similarly,
auto-assign-technician, detect-wasted-trips, check-weather-risk, and
update-technician-workload stay fully deterministic (geometry/threshold-
based) — no new Claude/LLM reasoning was added to any of them; only
generate-growth-drafts (which already had a two-pass Claude draft/review
loop from before this phase) touches Claude.
Marketing — audience-opportunity insight, added to
generate-growth-drafts: this function already computes suburb counts
from the last 90 days of completed jobs, but only ever acted on the single
top suburb for its weekly ad-campaign draft. It now also checks the
2nd-highest suburb by job count and, if that suburb has never had a
targeted ad_campaign draft in the last 90 days, writes an agent_insights
row (agent: 'marketing', insight_type: 'suggestion') — e.g. "Waverley
is your 2nd-highest suburb by completed jobs (6 in 90 days) but has never
had a targeted ad — worth considering once you've reviewed the current
draft." This is informational only: it does not create a second
marketing_drafts row and does not touch the existing "max 2 pending
drafts per business" cap. Unlike the ad/SMS copy drafts, this check makes
no Claude call, so it runs the same whether or not ANTHROPIC_API_KEY is
configured.
Scheduling — job-overload heuristic (honest limitation), added to
detect-wasted-trips: the original ask was to "flag scheduling conflicts,"
but jobs has only a single scheduled_time timestamp — no
duration/estimated_duration column anywhere in the schema, so there is no
way to detect a true double-booking (two jobs whose time windows actually
overlap). What's implemented instead is the honest, buildable version: for
each technician, count how many scheduled/active jobs with a set
scheduled_time fall on the same calendar date (today or a future date —
past/backlog dates aren't scanned). If a technician has more than 6 jobs
on one calendar date, that's written to agent_insights
(agent: 'scheduling', insight_type: 'anomaly') as a conservative,
fixed-threshold signal that the day is probably overloaded — not proof
of any specific overlapping time slot. This lives inside
detect-wasted-trips rather than auto-assign-technician because it's the
only scheduling-pillar function on a tight (~15 min) cron cadence that
scans every job across every business; auto-assign-technician only fires
once per new job for auto-dispatch-enabled businesses, so it would miss
manually-assigned jobs. Throttled per technician+date via the new
technicians.overload_alert_date column so the same technician isn't
re-flagged every 15 minutes for the same day.
One new column, one new delta file:
supabase_schema_delta_agent_phase3.sql adds
technicians.overload_alert_date (a date, not a timestamptz — see the
file's own comments for why). Run order:
# 1. Run supabase_schema_delta_agent_phase3.sql in the Supabase SQL Editor
# 2. Deploy the seven changed functions (see below)
Deploy the seven changed functions:
supabase functions deploy generate-growth-drafts
supabase functions deploy launch-ad-campaign
supabase functions deploy send-growth-message
supabase functions deploy auto-assign-technician
supabase functions deploy detect-wasted-trips
supabase functions deploy check-weather-risk
supabase functions deploy update-technician-workload
Phase 4 adds a new function, agent-council-report, and one new table,
agent_council_reports. Every Monday it reads the last 7 days of
agent_insights (written by the Phase 2/3 functions) and agent_functions
health data (written by record_agent_run, across every agent), and writes
one synthesis row — a weekly "state of the agents" report.
This is a report FOR THE MINERVA OPERATOR, not for a customer. Every
other notification in this codebase (daily-digest, notify-slack, the
Phase 2/3 Slack alerts) is scoped to one business and posted to that
business's own slack_webhook_url. The Agent Council report is cross-
platform — it's about the health of Minerva's own agent system across every
customer business — so it deliberately does not go through notify-slack
and is not attached to any single business_id.
What it does:
agent_insights created in the last 7 days (grouped by agent
and by insight_type) and all agent_functions rows (for error_count /
last_status / staleness context).ANTHROPIC_API_KEY is configured, sends that data to Claude and asks
for a three-part report: a plain-English summary of the week across
whichever agents actually had activity (it's explicitly told not to
invent activity for research/design, which aren't real agents yet),
any pattern worth a human's attention, and 2-4 concrete suggestions —
the prompt explicitly instructs Claude to ground every suggestion in the
actual data provided and not give generic advice. max_tokens is capped
at 1200 as a deliberate cost guardrail, since this runs weekly forever
once cron'd.error_count >= 5 or last_status = 'error'), honestly labeled "AI reasoning unavailable — showing raw data summary" rather than pretending to be the AI version.record_agent_run wired in on both the success and error paths, same
pattern as every other Phase 2/3 function.Known limitation — honest, not hidden: no delivery channel yet. This
report has no Slack/email delivery in this phase. It is stored only in
agent_council_reports — to read it today you query the table directly
(Supabase Table Editor, or select * from agent_council_reports order by created_at desc limit 1). Phase 5 is expected to add a dashboard UI to
actually display it; until then this is intentionally a storage-only
report, not a delivered one.
Schema: supabase_schema_delta_agent_council.sql (new
agent_council_reports table). Run order:
# 1. Run supabase_schema_delta_agent_council.sql in the Supabase SQL Editor
# 2. Deploy the new function
supabase functions deploy agent-council-report
# 3. Run supabase_schema_delta_agent_council_cron.sql in the Supabase SQL Editor
Phase 5 is the final phase of the 5-phase build. It adds a read-only
Agents tab to DispatcherView.jsx (src/pages/DispatcherView.jsx) —
the console every Minerva business owner already uses — that finally gives
agent_council_reports (storage-only since Phase 4) an actual UI, and
surfaces agent_functions / agent_insights in one place instead of
requiring a raw SQL query. No new tables, columns, or edge functions —
everything this tab reads was already created in Phases 1–4.
Gating decision — read this before wondering why the tab isn't visible.
agent_functions, agent_insights, and agent_council_reports are all
platform-wide (not scoped to business_id — agent_council_reports has no
business_id column at all, and the Phase 4 header comment explicitly says
that report is "FOR THE MINERVA OPERATOR, not for a customer"). Showing that
cross-tenant data by default inside a random trade business's day-to-day
dispatcher console would be confusing at best (a plumber wondering why
they're seeing "outreach agent" error counts for the whole platform) and a
quiet violation of the spirit of tenant isolation at worst — even though
(see SECURITY_NOTES.md) the underlying RLS policies on these three tables
are already anon-select-all, the same permissive pattern as every other
table in this app, so gating the UI doesn't change what's technically
readable at the database layer. It changes what's shown by default.
Given that, the tab is gated behind an explicit ?agents=1 URL query
param on the dispatch link (e.g. /dispatch/<businessId>?agents=1) — a
"AGENTS" tab button (labeled "Agent Ops (operator only)") only renders when
that param is present. This is intentionally lightweight (no new auth
system, no hardcoded business ID check — this app has no login at all, see
SECURITY_NOTES.md "The model: unguessable links, not logins") but it does
the one thing that matters here: a business owner using their normal
dispatch link never sees this tab or even knows it exists. Only someone
who's been told to append ?agents=1 (i.e. the Minerva operator, on their
own bookmarked link) sees it. This is a UX/product gate, not a security
boundary — see SECURITY_NOTES.md for the explicit note tying this back to
the already-accepted anon-RLS tradeoff.
What the tab shows:
agent_functions counts grouped by agent
(outreach/marketing/scheduling/finance/core), plus research
and design shown explicitly as "0 — not yet built" (no seed rows exist
for either yet, per Phase 1) rather than silently omitted, an unhealthy-
function count (error_count >= 5 or last_status = 'error'), and a
count of agent_insights written in the last 7 days.agent_functions list, unhealthy rows first then alphabetical
by agent, each with a status badge (ok=green/error=red/unknown=amber —
same color convention as IndustrialDispatcherView.jsx's statusBadge),
last run time (via the existing timeAgo helper from src/utils.js),
and error count when non-zero.agent_insights feed — last 7 days, newest first, capped at 20
rows — agent badge, insight type, summary, relative timestamp.agent_council_reports row (summary text, week range,
functions-checked/insights-reviewed/unhealthy-count stats), or — if the
weekly cron hasn't produced its first row yet — a plain "No report yet —
first one lands <next Monday's date>" empty state instead of a blank
panel.useEffect keyed on queueTab === 'agents', guarded by an
agentDataLoaded flag), not on every DispatcherView mount — same
instinct as the existing lazy checklist-photo/materials load in
toggleJobDetails.Fully wired end-to-end after this phase:
agent_council_reports now has a real UI (previously SQL-only). The full
Phase 1–5 loop — health tracking → insight writing → weekly synthesis →
human-readable dashboard — is complete and viewable in one place for the
first time.
Known follow-up, deliberately not built this phase: at the time Phase 5
shipped, this was a read-only dashboard — agent_functions.enabled (the
Phase 1 kill-switch column) wasn't read by any edge function yet, so an
enable/disable toggle would have done nothing. This has since been built
(2026-09-02/03, separate from Phase 5): the Agents tab now has a working
enable/disable toggle per function, and every autonomous/cron-driven edge
function (56 total, up from the original 11 gated at the time this was
built) checks agent_functions.enabled at the top of every run and skips
early when disabled. See SECURITY_NOTES.md for the one caveat that comes with
this — agent_functions rows aren't business_id-scoped, so anyone who
knows to add ?agents=1 can disable another business's agents, not just
their own.
Six additions built from data Minerva already collects, or from self-serve
integrations with no partnership/licence gate — explicitly NOT the
escrow/invoice-financing, SOC 2 certification claim, or covert
BLE/UWB-tracking ideas discussed and declined (those require an Australian
Credit Licence, an actual third-party audit, and raise Privacy Act 1988 /
surveillance-device-law concerns respectively — see chat history, not
built). Deploy after running supabase_schema_delta_minerva_max.sql, then
supabase functions deploy <name> for each, then
supabase_schema_delta_minerva_max_cron.sql for the 3 cron ones.
predict-asset-maintenance, daily cron) —
upgrades monitor-asset-telemetry's reactive threshold check into a real
trend projection: computes each asset's actual engine-hours-per-day
usage rate from its last 14 days of telemetry pings, and flags any asset
projected to cross its maintenance threshold within 7 days. Simple linear
math on this business's own real data — not a cross-client failure model
(not enough data volume for that to mean anything yet, and this build
never shares one business's data with another's).verify-checklist-photos
extended, jobs.ai_verified_at / invoices.ai_verified) — once every
checklist photo on a job has been AI-reviewed (Watchtower) and none are
flagged, the job is marked AI-verified and the badge carries through to
the invoice, shown to both the dispatcher and the client on
InvoiceView.jsx. This is the same photo-plausibility check Watchtower
already does, rolled up to job level — not blueprint/BIM
cross-referencing (no BIM data source exists in this build).estimate-job-carbon, daily cron, new
Carbon Est. tab) — chains a technician's completed-job locations for
the day and estimates transit CO2-e using a static reference vehicle
emissions factor. Explicitly labelled as an estimate everywhere it's
shown: straight-line distance (not road routing), transit-only (no
materials/embodied-carbon component, since there's no live supplier API
to source an Environmental Product Declaration from), and the CSV export
includes a "Factor Basis" column so the constant can be checked against
the current published NGA Factors workbook before it's attached to a
real tender.detect-idle-assets, daily cron) —
flags active industrial assets with no telemetry ping in 14+ days.subcontractors
table) — dispatch can assign a job to an external subcontractor
alongside employed technicians (jobs.assigned_subcontractor_id,
separate from technician_id so payroll/hours logic stays
employee-only). auto-assign-technician now falls back to the nearest
active subcontractor when no employed technician is free, instead of
leaving the job unassigned.businesses.auto_dispatch_max_km,
Settings — "Max auto-dispatch distance (km, optional)", 2026-09-05) — caps
how far auto-assign-technician will auto-dispatch a technician or
subcontractor. Null (default) is unlimited, exact same behaviour as
before. If set, and the nearest free candidate is further than that from
the job's location, the job is left unassigned for a human to route
instead of auto-dispatching someone a long way out just because everyone
closer happened to be busy.On "Minerva Max" as a $500/technician/month tier: not priced or positioned yet. The ROI case for a premium tier should be built from real pilot-customer before/after numbers once one of the six items above is in front of an actual mid-market business, not from a hypothetical model.
Six more additions, built end-to-end (schema + edge functions + UI). Deploy
after running supabase_schema_delta_minerva_round2.sql, then
supabase functions deploy <name> for each (track-review-click needs
--no-verify-jwt), then supabase_schema_delta_minerva_round2_cron.sql
for forecast-demand's weekly schedule.
draft-quote, send-quote-sms, new
Quotes tab, QuoteView.jsx) — a dispatcher describes a job in plain
text, Claude drafts line items from the business's last 30 invoices
(falls back to one blank editable line if AI is unavailable — same
honest-fallback pattern as chase-unpaid-invoices), the dispatcher edits
and sends. The client can Accept/Decline directly on their own quote —
that's a status update on their own row, not an outbound message, so it
doesn't need the Sales & Marketing human-approval gate that sending does.job_assignments table, crew UI in
the Jobs tab, lead/crew gating in TechnicianView.jsx) — a job keeps one
"lead" (jobs.technician_id, unchanged, still drives payroll/GPS/hours),
and dispatch can add extra "crew" technicians who get the same job pushed
to their current_job_id (that's what makes TechnicianView show it to
them at all) but only see a reduced track-only view — starting, completing,
checklist, materials, and invoicing all stay lead-only. Crew members get a
self-service "Leave job" button.send-review-request-sms,
track-review-click — public, --no-verify-jwt, records first click then
redirects — Settings "Google review link" field, "Request review" button
on paid invoices) — same per-message human-approval-gate discipline as
every other Sales & Marketing send.forecast-demand, weekly cron, Jobs tab
banner) — buckets each business's own job history by client_address
into the last 4 weekly buckets and flags addresses where the last 2
weeks are trending up vs the 2 weeks before. This is trend arithmetic on
a noisy proxy for suburb (jobs has no dedicated suburb column), not a
trained forecasting model — labelled as such in the insight text itself.TechnicianView.jsx) — for jobs flagged urgency: 'emergency', one tap
adds a deterministic time-of-day/day-of-week premium as a normal
editable/removable invoice line item. Never auto-applied, never a live
demand model — just a starting-point suggestion the technician can edit
or delete before sending.client_portal_links table,
ClientHistoryView.jsx, "View your service history" button on
TrackingView.jsx's job-complete screen) — a read-only list of a client's
own past jobs and invoices for one business, reached via an opaque
random-token URL (not the client's phone number) upserted once per
business/client-phone pair so repeat visits reuse the same link.Monetization layer on top of the Minerva Max batch + Round-2 batch above —
no new features, just per-business, per-addon gating so those features are
individually salable rather than always-on freebies bundled into base
pricing. Deploy supabase_schema_delta_minerva_max_tier.sql, then
redeploy forecast-demand, predict-asset-maintenance,
detect-idle-assets, send-review-request-sms, xero-sync-invoice, and
xero-oauth-connect (all five now check the relevant addon flag).
src/maxAddons.js is the single source of truth for the
catalog + prices): Emergency Surge Pricing, AI Quote Drafting, Multi-Tech
Job Splitting, Review Request Loop, Demand Trend Alerts, Subcontractor
Pool, Asset Intelligence (predictive maintenance + idle-asset detection),
Carbon/ESG Estimate, Xero Sync.businesses.max_addons /
businesses.max_addon_trials (jsonb). No bundled "Minerva Max" on/off
switch by design — see product discussion this session on why a cold
$300-500/mo ask doesn't convert, vs. proving value addon-by-addon first.DispatcherView) —
computed from this business's own data, not a generic pitch: unclaimed
after-hours premium $ across real emergency jobs, open leads with zero
quotes sent, paid invoices with no review requests sent, unassigned jobs
with no subcontractor pool, etc. Each nudge is dismissible
(upsell_nudge_dismissals table) so it doesn't reappear every session.hasAddon() from src/maxAddons.js), and the corresponding edge
functions independently check the same flag server-side before doing any
work or sending anything — so a locked feature can't be triggered by
calling the function directly.Run through this on TWO real devices (your phone + your laptop):
/start → complete onboarding form → Stripe checkout loads4242 4242 4242 4242 (any future expiry, any CVV)All tiers (no tier gate in code — works on Starter too):
Pro tier only (sign up with the Pro plan to test these):
Autonomous layer (only if you ran the pg_cron setup block):
retention-checkin, reconcile-billing, check-inventory-levels,
generate-growth-drafts are scheduled agents — hard to trigger
manually with real 30-90 day data, so verify these by invoking each
function directly once from the Supabase Dashboard (Edge Functions →
function → "Invoke") and confirming it returns
{ "success": true, ... } with no errorgenerate-growth-drafts has run
at least once, a draft card appears → tap Reject on one → status
updates to REJECTED and the pending badge count dropsad_campaign draft
→ confirm a PAUSED campaign appears in Meta Ads Manager for that
ad account (do this with a real or sandbox ad account, not a live
spending one, until you're confident in the flow)outreach_sms draft → confirm the SMS arrives on the
test lead's phone and the draft's status updates to SENTDo not call a single client until every checkbox above is ticked.
minerva/
├── src/
│ ├── App.jsx # Router — all page routes
│ ├── main.jsx # React entry point
│ ├── index.css # Global styles
│ ├── supabaseClient.js # Supabase singleton
│ ├── sentry.js # Optional error monitoring init, gated on VITE_SENTRY_DSN
│ ├── utils.js # Haversine, geocoding, PIN generator (+ PIN-retry helper), utils.test.js
│ ├── components/
│ │ ├── ErrorBoundary.jsx # App-wide crash guard, wraps <App /> in main.jsx
│ │ ├── RequireBusinessAuth.jsx # Auth gate for /dispatch/:id and /industrial/:id (2026-09-05)
│ │ └── ContactSupportModal.jsx # Shared "Contact support" form (Dispatcher + Technician views)
│ └── pages/
│ ├── LandingPage.jsx # Public marketing page
│ ├── Onboarding.jsx # Business + technician signup, tier selection
│ ├── Login.jsx # Business owner magic-link login (2026-09-05)
│ ├── AdminConsole.jsx # Internal Minerva staff console — /admin (2026-09-05)
│ ├── SuccessPage.jsx # Post-Stripe confirmation
│ ├── DispatcherView.jsx # Live map dashboard (owner) — jobs, leads,
│ │ # Pro tier: assets, invoices, checklist mgmt
│ ├── TechnicianView.jsx # Mobile tracking page (technician) — Pro tier:
│ │ # completion checklist + invoice builder
│ ├── TrackingView.jsx # Client-facing tracking link — also shows the
│ │ # post-job "Request this again" rebooking button
│ ├── InvoiceView.jsx # Client-facing invoice view (Pro tier)
│ ├── QuoteView.jsx # Client-facing quote accept/decline view
│ ├── ClientHistoryView.jsx # Client-facing portal: past jobs + invoices for one client
│ ├── IntakeAssistant.jsx # AI lead-triage chat widget (embedded on client sites)
│ ├── DisputeView.jsx # BONUS: read-only Dispute Pack (GPS route, photos,
│ │ # materials, invoice) for a single job
│ ├── IndustrialDispatcherView.jsx # Track B: industrial sector console
│ │ # (leads, sites, assets, inventory, safety, verification)
│ ├── CaseStudy.jsx # Public case-study/social-proof page
│ ├── TermsOfService.jsx # Public legal page
│ ├── PrivacyPolicy.jsx # Public legal page
│ └── RefundPolicy.jsx # Public legal page
├── supabase_schema.sql # Run this in Supabase SQL Editor first
├── supabase_schema_delta_agent_expansion.sql # Track A: workflows, incidents, photo verification, lost-lead winback
├── supabase_schema_delta_industrial.sql # Track B: industrial sector tables
├── supabase_schema_delta_agent_cron.sql # Cron schedules for both tracks' new functions
├── supabase_schema_delta_owner_auth.sql # businesses.owner_user_id — auth gate (2026-09-05)
├── supabase_schema_delta_pin_unique.sql # technicians.pin unique constraint (2026-09-05)
├── supabase_schema_delta_support_requests.sql # support_requests table for AdminConsole (2026-09-05)
├── supabase_schema_delta_abandoned_signups.sql # businesses.abandoned_flagged_at (2026-09-05)
├── supabase_schema_delta_abandoned_signups_cron.sql # cron for flag-abandoned-signups (2026-09-05)
├── supabase_schema_delta_rls_scoping_v1.sql # admin_users table + owner/admin-scoped SELECT on 5 tables (2026-09-05)
├── supabase_schema_delta_auto_dispatch_radius.sql # businesses.auto_dispatch_max_km (2026-09-05)
├── supabase_schema_delta_reconcile_technician_state_cron.sql # cron for reconcile-technician-state (2026-09-05)
├── supabase_schema_delta_review_requests_unique.sql # unique index, dedupes send-review-request-sms (2026-09-05)
├── supabase_schema_delta_subcontractor_pool_addon.sql # server-enforce subcontractor_pool addon (2026-09-05)
├── .github/workflows/ci.yml # lint + test + build on every push/PR (2026-09-05)
├── vitest.config.js # Test runner config (2026-09-05)
├── .eslintrc.cjs # Lint baseline (2026-09-05)
├── supabase/
│ └── functions/
│ ├── send-eta-sms/ # Fires the 15-min client SMS
│ ├── send-setup-sms/ # Fires technician setup SMS on onboarding
│ ├── send-completion-sms/ # Fires the "job complete" client SMS
│ ├── send-invoice-sms/ # Fires the invoice link SMS (Pro tier)
│ ├── send-job-assignment-sms/ # Fires assignment/reassignment SMS to the technician (2026-09-04)
│ ├── missed-call-webhook/ # Twilio Voice webhook: TwiML + missed-call SMS auto-reply
│ ├── create-checkout-session/ # Creates Stripe checkout (tier + quantity aware)
│ ├── sync-technician-billing/ # Syncs Stripe subscription quantity to connected technicians
│ ├── stripe-webhook/ # Handles checkout.session.completed / subscription.deleted, sends welcome email
│ ├── send-email/ # Generic transactional email (Resend), gated on optional RESEND_API_KEY (2026-09-05)
│ ├── flag-abandoned-signups/ # Agent: flags (never deletes) 48h+ abandoned pre-checkout signups (daily, 2026-09-05)
│ ├── ai-intake-chat/ # AI lead-triage chat backend (Claude)
│ ├── notify-slack/ # Generic Slack alert poster (internal helper)
│ ├── calendar-feed/ # Public ICS feed of scheduled jobs
│ ├── nurture-stale-leads/ # Agent: SMS nudge for leads sitting untouched (hourly) + 2nd touch at 24h
│ ├── chase-unpaid-invoices/ # Agent: payment reminder SMS for unpaid invoices (daily)
│ ├── auto-assign-technician/ # Agent: assigns nearest tech to new jobs (event-driven)
│ ├── daily-digest/ # Agent: daily Slack activity summary + "waiting on you" nudges + silent-lead/stuck-invoice escalation flags
│ ├── retention-checkin/ # Agent: post-job "need anything else?" SMS (weekly)
│ ├── reconcile-billing/ # Agent: Stripe vs local technician-count drift alert (daily)
│ ├── check-inventory-levels/ # Agent: low-stock Slack alerts (daily)
│ ├── generate-growth-drafts/ # Agent: drafts ad campaigns + win-back SMS (weekly) — WRITES ONLY
│ ├── launch-ad-campaign/ # Click-only: launches an approved ad_campaign draft via Meta API
│ ├── send-growth-message/ # Click-only: sends an approved outreach_sms draft via Twilio
│ ├── check-credential-expiry/ # Agent: licence/ticket expiry Slack alerts (daily), Pro tier
│ ├── send-review-request-sms/ # Fire-and-forget: review-request SMS + trackable link
│ ├── track-review-click/ # Public redirect: logs a review-link click, then forwards to the real review URL
│ ├── detect-wasted-trips/ # Agent: GPS-confirmed no-show detection (every 15 min)
│ ├── check-weather-risk/ # Agent: forecast-risk drafts for weather-sensitive jobs (daily) — WRITES ONLY
│ ├── send-weather-reschedule-sms/ # Click-only: sends an approved weather reschedule draft via Twilio
│ ├── send-referral-code-sms/ # Fire-and-forget: referral code + SMS on invoice paid
│ ├── update-technician-workload/ # Agent: burnout-hours + emergency-count roster signals (daily)
│ ├── reconcile-technician-state/ # Agent: self-healing fix for technicians stuck on a finished job (daily, 2026-09-05)
│ ├── verify-checklist-photos/ # Track A: Watchtower — AI photo verification (every 15 min)
│ ├── run-custom-workflows/ # Track A: general-purpose trigger→condition→action workflow agent
│ ├── winback-lost-leads/ # Track A: re-engagement SMS for leads marked lost (daily)
│ ├── draft-quote/ # Minerva Max (ai_quotes): AI-drafted quote text — WRITES ONLY
│ ├── send-quote-sms/ # Minerva Max (ai_quotes): sends an approved quote link via Twilio
│ ├── forecast-demand/ # Minerva Max (demand_forecast): weekly booking-volume projection (weekly)
│ ├── estimate-job-carbon/ # Minerva Max (carbon_estimate): daily transit CO2-e estimate per technician
│ ├── predict-asset-maintenance/ # Minerva Max (asset_intelligence): linear-projection maintenance forecast (daily)
│ ├── detect-idle-assets/ # Minerva Max (asset_intelligence): "ghost asset" no-telemetry detector (daily)
│ ├── xero-oauth-connect/ # Minerva Max: starts the Xero OAuth flow for a business
│ ├── xero-oauth-callback/ # Minerva Max: completes Xero OAuth, stores tokens
│ ├── xero-sync-invoice/ # Minerva Max: pushes a paid invoice to the business's connected Xero org
│ ├── agent-council-report/ # System: cross-agent digest of recent agent_insights (on-demand)
│ ├── test-agent-health/ # System: pings agent_functions rows vs. actual recorded runs (on-demand)
│ ├── industrial-conductor/ # Track B: Central Conductor — asset suggestion for new leads
│ ├── harvest-industrial-leads/ # Track B: bulk industrial lead ingestion webhook
│ ├── enrich-industrial-leads/ # Track B: Signal & Enrich — missing-contact-data nudge (daily)
│ ├── monitor-asset-telemetry/ # Track B: real-time asset ingestion + geofence/maintenance alerts
│ ├── optimize-industrial-routes/ # Track B: Route Optimizer — nearest-asset suggestions (every 30 min)
│ ├── track-consumables/ # Track B: Quartermaster — low-stock flag (hourly)
│ ├── detect-safety-hazards/ # Track B: The Warden — proximity-hazard sweep (every 15 min)
│ ├── sequence-handoffs/ # Track B: The Pacer — handoff-sequencing sweep (every 15 min)
│ ├── package-client-verification/ # Track B: The Closer — sign-off evidence packaging (on-demand)
│ └── verify-industrial-compliance/ # Track B: Sentry — 24h+ safety escalation backstop (hourly)
├── index.html
├── package.json
├── vite.config.js
├── COMPLIANCE_TEMPLATES.md # Disclaimed compliance/legal doc templates
└── .env.local.example # Copy to .env.local and fill in your values
Tier is chosen at signup (Onboarding.jsx) and drives which Stripe price is
charged (create-checkout-session). Only the Pro tier is actually feature-
gated in the app (business.subscription_tier === 'pro' checks in
DispatcherView.jsx/TechnicianView.jsx unlock invoicing, checklists,
Watchtower, and Marketing). Starter and Standard render identically — there
is no code path that restricts the dispatch board, job scheduling, or the
intake chat/lead-scoring widget to Standard+; every business gets those
regardless of tier. (Found and fixed 2026-09-07 — the pricing page and this
README previously claimed a Starter/Standard feature split that never
existed in code. If a real Starter/Standard split is wanted later, it needs
actual gating logic added to those two views, not just copy.) Billed
quantity auto-adjusts to the number of technicians actually connected — see
sync-technician-billing above.
/login — Supabase Auth magic-link login for business owners. No
password to manage; Supabase's own built-in auth email delivery sends
the link, so this needs no new external account. Existing pilot
businesses auto-claim to whichever authenticated user's email matches
businesses.contact_email on first login./dispatch/:businessId and /industrial/:businessId now require
that login and ownership match (RequireBusinessAuth.jsx) — an
additive, app-layer gate only. RLS is unchanged (still using (true) on
every table, see SECURITY_NOTES.md); scoping RLS itself to auth.uid()
is necessary follow-up work, not done in this pass./admin — internal Minerva staff console (AdminConsole.jsx):
cross-business list (tier, Stripe status, tech count, last activity),
manual tier override, and a support inbox (support_requests table,
fed by a new "Contact support" form in both DispatcherView and
TechnicianView). Set VITE_ADMIN_EMAILS (comma-separated staff emails)
in your env before this is usable — it's a client-side allowlist, same
security model as the rest of this app, not a hard boundary.npm test — Vitest, currently covers utils.js's pure functions.npm run lint — ESLint baseline (.eslintrc.cjs), catches real bugs
without imposing a stylistic rewrite on existing code..github/workflows/ci.yml — runs lint + test + build on every push/PR
to main.src/sentry.js, ErrorBoundary.jsx)
— no-op until VITE_SENTRY_DSN is set.Built and maintained by [Your name] | [Your mobile] | [Your email]
DispatcherView.jsxtechnician_locationsupdate-technician-workloadrecorded_atTechnician, Estimated Hours, Days With GPS Activity, Period Start, Period Endsend-weather-reschedule-sms / Weather tab — see above.send-referral-code-sms — Paid-Invoice Referral Loop. The moment an
invoice is marked paid (Pro tier only, since invoicing itself is a
Pro-tier feature), generates a short referral code for
that invoice and texts the client a simple "share this with a friend"
message — a single acknowledgment-tier nudge, not a marketing campaign,
same low-stakes bar as the existing retention check-in SMS. If a future
chat visitor mentions a referral code, the AI intake widget
(ai-intake-chat) opportunistically checks it against real invoices for
that business and, if it matches, tags the new lead as referred — never
prompted for, never stored if it doesn't match a real code.TrackingView.jsx only, no new
function) — once a job shows "Job Complete" on the client's tracking
link, a "Need this again?" button lets them request another booking
directly. Writes straight into the existing leads pipeline (tagged
source: 'rebooking') so it shows up on the dispatcher's Leads tab
like any other lead — no SMS or email sent automatically, a human still
follows up./dispute/:jobId, bonus) — a read-only, shareable
evidence page for a single job (GPS route while on-site, checklist
photos, materials used, invoice), opened via a 📁 Dispute Pack link
next to each job in the dispatcher's Recently Completed list. Built
entirely from existing data — no new table or edge function. Same
no-login link-based trust model as /track/:jobId and
/invoice/:invoiceId (see SECURITY_NOTES.md).reconcile-technician-state (daily cron, 2026-09-05) — self-healing
drift fix. technicians.current_job_id is supposed to clear back to
null when a job finishes, but the clearing write and the job-completion
write aren't transactional, so a dropped connection between them could
leave a technician's pointer stuck on an already-completed job forever —
silently excluding them from all future auto-dispatch with no error
anywhere. This agent finds any technician pointing at a job that's
complete (or gone) and clears it, logging what it fixed to
agent_insights. Safe to auto-correct (not just flag) since it only
repairs an internal consistency pointer, not customer or financial data.TechnicianView.jsx, localStorage
key minerva_last_job_cache, 2026-09-05) — a technician who loses signal
mid-job now still sees the last successfully loaded job's address,
checklist, and client details instead of a blank/error screen. Read-only:
writes (GPS pings, checklist updates) still rely on the pre-existing
offline queue/retry logic, unchanged.xero-oauth-connect, xero-oauth-callback,
xero-sync-invoice, "Connect Xero" in Settings) — a real OAuth 2.0 flow
against Xero's actual API, not a mock. Needs the business owner to
register a free app at developer.xero.com and the operator to set
XERO_CLIENT_ID/XERO_CLIENT_SECRET/SUPABASE_SERVICE_ROLE_KEY
secrets — until then, "Connect Xero" returns a 501 with setup
instructions instead of pretending to connect. Tokens are stored in
integration_credentials, a table deliberately NOT covered by this
app's usual anon-all RLS policy — only service_role can read/write it,
since these are real third-party credentials, unlike the rest of this
build's demo-style data. Synced invoices land in Xero as DRAFT, not
auto-sent, since Minerva has no way to know the business's preferred
chart-of-accounts mapping beyond a best-effort default.