CoolFace
Apppublic

mlwithprince/outreach-tracker

sourceHugging Faceupdated 20d agoView on Hugging Face
0likes
App README

Outreach Tracker

A focused, daily-use tracker for manual outreach (Facebook pages, or any platform). It does not send messages and does not generate them — it answers one question really well: who needs my attention today?

Workflow (non-linear outreach lifecycle)

text
Initial ────────────────▶ F1 ──────▶ F2 ──────▶ F3
  │                        │          │          │
  └────────────────┐       │          │          │
                   ▼       ▼          ▼          ▼
                 In Talking ◀─────────────────────┘
                   │
   ┌───────────────┼──────────────────────────────┐
   ▼               ▼                              ▼
 Sample F1 ──▶ Sample F2 ──▶ Sample F3        Close to Won (final)
 Personalized F1 ──▶ Personalized F2 ──▶ Personalized F3

From ANY active state:  → Breakup (final)
  • —Active outreach: Initial, F1, F2, F3 — the Messaged action advances the sequence (F3 is the final normal follow-up).
  • —In Talking: the conversation state — no follow-up countdown. From here you can transfer to Sample F1, Personalized Sample F1, or Close to Won.
  • —Sample Follow-ups: Sample F1 → Sample F2 → Sample F3 — Messaged advances the sample sequence. Sample F3 is the final.
  • —Personalized Sample Follow-ups: Personalized F1 → F2 → F3 — same as above for personalized samples.
  • —Close to Won: final positive state. Reachable only from In Talking.
  • —Breakup: final negative state. Reachable from any active state (Initial, F1-F3, Sample F1-F3, Personalized F1-F3, In Talking). Double confirmation required in the UI.

Persistent status flags

replied, interested, sample_sent are independent persistent flags that are never reset by a stage move. They can be toggled at any time and survive every transition.

When replied is set to true, the lead's replied_at_stage field is cached at the current stage — so the UI can show "Replied during F2" etc. without re-querying the history table on every render.

When transferring to Sample F1 or Personalized Sample F1, sample_sent is set to true automatically — no second manual checkbox needed.

Undo

Each follow-up sequence supports undo:

Current stageUndo target
F1Initial
F2F1
F3F2
Sample F1In Talking
Sample F2Sample F1
Sample F3Sample F2
Personalized F1In Talking
Personalized F2Personalized F1
Personalized F3Personalized F2

Undo only changes the stage field. It never touches replied, interested, sample_sent, or any historical stage dates. The previous stage's entry date was never cleared when the lead advanced, so it becomes the active follow-up date again automatically.

Follow-up timing (calendar days)

The follow-up rule uses calendar dates only — never hours or minutes.

StageHighlight on day
InitialD + 2 (3rd)
F1, F2D + 2 (3rd)
F3D + 3 (4th)
Sample F1, Sample F2D + 2 (3rd)
Sample F3D + 3 (4th)
Personalized F1, F2D + 2 (3rd)
Personalized F3D + 3 (4th)
In Talkingno countdown
Close to Wonno countdown
Breakupno countdown

Custom stage dates (date-only, never a time picker) are supported for every follow-up F-stage. Changing a stage date instantly recalculates its highlight.

Features

  • —Add Leads by manual entry or CSV import with strict validation (header must be exactly name,page_url,followers,website; duplicates are allowed). Live preview before anything is written.
  • —Edit / double-confirm Delete on every lead.
  • —Search and filters by status (replied, interested, sample sent, due today, overdue, etc.) on every active stage view.
  • —Page URL + website links open in a new browser tab.
  • —Insights: stage distribution, follow-up workload, outcome rates against contacted leads, and a 28-day "leads added" chart.
  • —Supabase persistence with Row Level Security from day one — every user only ever sees their own leads. A non-linear workflow history table records every transition, tick, and date change for audit and undo.
  • —Docker image for Hugging Face Spaces (multi-stage build: bun for the build stage, plain node:22-alpine for the runtime stage).

1. What you need

  • —A free Supabase project (for persistence)
  • —Node.js 20+ (or Bun 1.1+) for local development
  • —Docker only if you want to run the container locally

2. Configure Supabase (5 minutes)

If you already have the original schema (the linear `initial → f1 → f2 → f3 → completed` workflow) installed and populated, DO NOT run the full supabase/schema.sql — that would be a destructive reset. Instead, run the safe migration:

  1. 1.Open your Supabase dashboard → SQL Editor → New query.
  2. 2.Paste the contents of `supabase/migrations/20260826120000_outreach_workflow_upgrade.sql` and run it. The migration is non-destructive: it preserves every existing lead, every flag, every date, every RLS policy. It only adds the new date columns, the new states, the history table, the history-capture trigger, and a baseline history backfill for existing rows.

If you are starting from a fresh database (no existing leads table), run the full schema file:

  1. 1.Open your Supabase dashboard → SQL Editor → New query, paste the contents of `supabase/schema.sql` and run it. This creates the leads table (with every workflow state already supported), the lead_stage_history table, indexes, triggers, and RLS policies.
  2. 2.Create the owner account — dashboard → Authentication → Users → Add user → email + password (tick auto-confirm or confirm the email). This is the account the app signs in as.
  3. 3.Get your API credentials — dashboard → Project Settings → API: the project URL and the anon public key. (No service-role key needed — the app authenticates as a real user, so RLS is enforced on every query.)

3. Environment variables

Copy .env.example to .env and fill in:

bash
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJ...          # public anon key — safe for server-side use
OWNER_EMAIL=you@example.com
OWNER_PASSWORD=your-password
# LEADS_BACKEND=memory            # optional: run without Supabase (RAM only)

No secret ever reaches the browser bundle: the app's UI talks only to its own API routes, and those routes talk to Supabase server-side.

4. Run locally

bash
bun install                # or: npm install
bun run dev                # or: npm run dev        → http://localhost:3000

Production build (the same thing Docker runs):

bash
bun run build              # or: npm run build
npm start                  # serves .next/standalone on $PORT (default 7860 in Docker)
Tip: with LEADS_BACKEND=memory you can try the entire UI without Supabase. Data is kept in RAM only — fine for evaluation, not for real use.

5. Tests

bash
bun run test               # or: npm test

The suite (164 tests) covers:

  • —The stage machine: nextStage, prevStage (undo), applyMessaged, applyUndo, applyTransfer, applyStageDate, every stage's date field.
  • —The date rules (3-day vs 4-day calendar logic per stage).
  • —The CSV parser/validator (header rules, quoted fields, invalid rows).
  • —Field validation (followers, page_url, website, buildExternalUrl).
  • —The insights math (stage counts incl. new states, rates, time series).
  • —The full schema (PGlite — a real WASM Postgres): DDL, constraints, the history trigger, multi-user Row Level Security on both leads AND lead_stage_history.
  • —End-to-end API route tests (memory backend): create, messaged, transfer, undo, tick, stage-date, delete, list.

There is also an end-to-end smoke test against the running production server:

bash
./scripts/e2e/run-server-tests.sh

It exercises the full non-linear workflow (Initial → F1 → In Talking → Sample F1 → In Talking → Personalized F1 → undo → In Talking → Close to Won, plus Breakup, CSV import, search, delete).

6. Docker

bash
docker build -t outreach-tracker .
docker run -p 7860:7860 --env-file .env outreach-tracker

The image is a multi-stage build: bun installs the exact lockfile and builds the Next.js standalone output; the runtime stage is plain node:22-alpine running as uid 1000, listening on 0.0.0.0:${PORT:-7860}.

Docker was not available in the build environment used to produce this release; the production build (bun run build) was tested instead and produces the exact same standalone output. Run docker build locally or push to Hugging Face Spaces to verify the container.

7. Deploy to Hugging Face Spaces

  1. 1.Create a new Space → SDK: Docker (blank template).
  2. 2.Push this repository to the Space (or upload it as files). The README.md front-matter already declares sdk: docker and app_port: 7860, which is what the Space uses.
  3. 3.In the Space → Settings → Variables and secrets, add: SUPABASE_URL, SUPABASE_ANON_KEY, OWNER_EMAIL, OWNER_PASSWORD (mark the last two as secret).
  4. 4.The Space builds the Dockerfile itself and serves the app. Done.

Local test of the exact same image: docker build && docker run as above.

8. How the app is organized

text
src/
├── app/
│   ├── page.tsx               # the single-page UI (hash-based views)
│   └── api/                   # server route handlers (all DB access)
│       ├── leads
│       │   ├── route.ts           # GET (list) / POST (create)
│       │   ├── import/route.ts    # POST (CSV bulk import)
│       │   └── [id]
│       │       ├── route.ts              # PATCH (edit) / DELETE
│       │       ├── messaged/route.ts     # POST — advance F-sequence
│       │       ├── transfer/route.ts     # POST — transfer state (In Talking/Sample/Personalized/Close to Won/Breakup)
│       │       ├── undo/route.ts         # POST — reverse most recent follow-up advancement
│       │       ├── tick/route.ts         # POST — toggle replied/interested/sample_sent
│       │       └── stage-date/route.ts   # POST — set a custom follow-up date
├── components/
│   ├── layout/                # sidebar, mobile nav, setup/error screens
│   ├── leads/                 # lead card, ticks, date picker, dialogs, stage views
│   ├── csv/                   # import preview pieces, recent additions
│   └── insights/              # KPI cards + lightweight custom charts
├── lib/
│   ├── dates.ts               # ALL follow-up/date logic lives here (3-day vs 4-day rule per stage)
│   ├── stages.ts              # stage machine (messaged, undo, transfer, dates, helpers)
│   ├── csv.ts                 # CSV parsing + validation
│   ├── validation.ts          # field validators (shared client + server)
│   ├── stats.ts               # insights computations
│   ├── types.ts               # shared Stage / Lead / TransferTarget types
│   ├── router.ts              # hash-based view routing (11 views)
│   └── backend/               # Supabase session, repositories, API helpers
├── providers/leads-provider.tsx  # client store w/ optimistic updates
└── supabase/
    ├── schema.sql             # full schema (run on a fresh database)
    └── migrations/
        └── 20260826120000_outreach_workflow_upgrade.sql  # non-destructive migration for existing databases

Data model (leads table): identity fields (name, page_url — the only required one, followers, website), stage (one of 14 states), the three persistent flags, replied_at_stage (cached stage when reply was toggled to true), per-stage entry dates (f1_started_at, f2_started_at, f3_started_at, completed_at for the legacy final, in_talking_at, sample_f1_started_at, sample_f2_started_at, sample_f3_started_at, personalized_sample_f1_started_at, personalized_sample_f2_started_at, personalized_sample_f3_started_at, close_to_won_at, breakup_at), created_at/updated_at, and user_id referencing auth.users with RLS policies so every user only ever sees their own rows.

A second table, lead_stage_history, is the immutable audit trail: every insert/update on leads is captured by a SECURITY DEFINER trigger with before_snapshot and after_snapshot JSONB fields. RLS on this table also restricts access to the owner's own history rows.

Authentication readiness: the API routes resolve the acting user through Supabase Auth (currently the configured owner account). Swapping in a per-request login session later is a change in one place (src/lib/backend/index.ts) — the schema, RLS and queries already work per-user.

9. Daily workflow

  1. 1.Add Leads — paste a CSV (header must be exactly name,page_url,followers,website) or add a lead by hand. Everything lands in Initial.
  2. 2.Open a lead's page, send your message, tick Messaged — the lead moves to F1 and its follow-up clock starts.
  3. 3.The card shows Follow up <date>; on the third day it turns amber (Follow-up due today) and after that red (Overdue). Tick Messaged again when you send the follow-up.
  4. 4.When the lead replies, tick Replied — the card records replied_at_stage (e.g. "Replied during F2") so you always know where the conversation started.
  5. 5.To enter a conversation state, open the ⋯ menu on the card → Move to In Talking. To send a sample, open ⋯ → Send Sample F1 (or Personalized Sample F1) — sample_sent is set automatically.
  6. 6.To undo a follow-up advancement, open ⋯ → Back to <previous>.
  7. 7.To close outreach on a lead, open ⋯ → Move to Breakup (double confirmation required). The lead is preserved in the database but no longer counts as active.
  8. 8.To mark a lead as effectively won, transfer it to In Talking first, then open ⋯ → Move to Close to Won.
  9. 9.Need to fix a date? Use the card's ⋯ menu → Set date to today or Choose custom date….
  10. 10.Check Insights for the day's workload and your response rates.