2026 · SELLER TOOLING

Seller-Admin-Tools

A local, offline toolkit that turns a weekly CRM pipeline CSV export into forecast narratives, QBR decks, and account plans — deterministic, vendor-neutral, and built against an enterprise security-sales methodology. 72 commits · 10 PRs · 3 active days.

PUBLISHED
72COMMITS
10PULL REQUESTS
3BUILD DAYS
SOURCE: GITHUB COMMIT HISTORY · 2026-08-25
Build cadence — commits per active day
Aug 11 Heaviest day: 54 commits · Aug 12 Aug 13
Seller-Admin-Tools screenshot

Overview

The weekly ritual for an enterprise seller is predictable: pull the pipeline CSV from the CRM, write the forecast narrative to the manager, assemble the QBR deck for the quarterly review, build the account plan for priority deals. Each artifact draws on the same underlying math — bucket rollups, week-over-week deltas, obligation-to-capability crosswalks — but is typically assembled by hand from the same export. Seller Admin Tools automates that path. Local, offline, deterministic, and vendor-neutral: no column names are hard-coded anywhere; a mapping screen translates any CRM export to a canonical schema, with saved profiles that make the following week's re-import zero-click. Adapted from an enterprise security-sales methodology, then de-branded and generalized before public release. The original four-session Streamlit build was ported to FastHTML 0.14.11 — specifically for offline-first operation with no CDN dependencies — then hardened through a security review. 72 commits · 10 PRs · 3 active days.

Project Design

Four feature sessions built the four tools against a set of PRDs: ingest and column mapping, forecast narrative, QBR assembler, then account plan. After the build sessions, ten pull requests closed out correctness, security, accessibility, and packaging work. The core layer is pure Python functions over pandas 3.0 DataFrames and SQLite — fully exercised headlessly by pytest with no UI dependencies. The web layer is a thin FastHTML ASGI app; routes render strictly from view models in core/views/ so on-screen numbers and export artifacts share a single rendering path and cannot disagree. All behavior-driving logic — stage buckets, risk thresholds, coaching asks, narrative templates, alias normalization, regulatory obligation maps, and product crosswalk — lives in six YAML files in config/ read at call time. Changing a CRM format is a column remap; changing a threshold is a YAML edit; adding a regulatory framework is a new entry in obligation_map.yaml. No Python changes required for any of those. The Streamlit-to-FastHTML migration was driven by one constraint: the offline invariant needed to be mechanically enforced rather than advisory. FastHTML's CDN-header suppression config plus a startup assert at web/server.py made that possible — the server refuses to start if any response header references an external URL.

Four-layer local Python app — config-driven YAML, pure-function core, thin FastHTML web layer, vendored offline UI
Config
stage_map.yamlaliases.yamlrisk_rules.yamlnarrative_templates.yamlobligation_map.yamlproduct_map.yaml
Core
Pure Python + pandas 3.0 DataFramesSQLite append-only snapshot store keyed by file SHA-256core/views/ — single rendering path for both UI and export artifactsschema.py — PIPELINE_SCHEMA and ACCOUNT_SCHEMA with validate_frame()
Web
FastHTML 0.14.11 (CDN headers suppressed; version-pinned)htmx 2.0.7 (vendored in web/static/)Tailwind CSS v3.4.17 standalone CLI (compiled CSS committed)LocalOnlyMiddleware — loopback Host check + same-origin CSRF guard
Tests
17 test modules (pytest 9.0.3)Golden parity sets — forecast narrative, QBR, and account plan .md/.pptx exportsSocket-guard test — offline invariant, enforced in CISecurity middleware — 10 test scenariosPackaging smoke — Docker seed scripts execute cleanly

Key modules

Tool 0

Ingest / Mapping

CSV parsing with date-format resolution (auto/US/International/ISO with live preview), a money parser that handles dollar signs, thousands separators, and parenthesized negatives, and five validation states (blocking vs. warning — rows still import on warnings). Auto-suggests column mapping against PIPELINE_SCHEMA and ACCOUNT_SCHEMA; the user confirms every field, suggestions are never applied silently. Named profiles make re-import zero-click. Re-import detection keyed by file SHA-256 catches the same file submitted twice.

Tool 1

Forecast Narrative

Commit / upside / pipeline bucket rollup using forecast_category when present, stage-derived otherwise. Week-over-week delta runs in two passes: ID-join first on opportunity_id, name-join fallback for ID-less rows. Duplicate non-empty IDs are demoted to name-join; duplicate name keys are demoted to unmatched — every ambiguous case is surfaced, never silently miscounted. Four risk flags (stalled: 45 days; slipped: 90 days; no_sponsor: deals ≥$500K; big_and_late: deals ≥$1M closing within 30 days), each with a plain-English evidence string and a configurable coaching ask from risk_rules.yaml. Exports to .md.

Tool 2

QBR Assembler

One-click from snapshot to a five-slide .pptx deck (title, scorecard, native bar chart, top deals, risks and asks) plus a .md appendix. Per-seller rollup with alias normalization across snapshots. A consistency-guard test parses the commit figure back out of the built deck and asserts it equals the narrative's — the two tools share the same core/forecast functions and cannot drift. DRAFT footer on every slide; tables row- and column-capped; long names truncated.

Tool 3

Account Plan

Joins an account-facts CSV with open pipeline. Obligation → capability → gap crosswalk: each obligation is marked landed, partial (a competitor holds it), or gap. Whitespace estimate sums open pipeline against gap capabilities; products that cannot be resolved to a capability are excluded and reported rather than guessed. Rule-based next actions. Exports to .pptx and .md.

Config

Config layer

Six YAML files in config/ drive all behavior at call time: stage_map.yaml (raw CRM stage string → canonical bucket), aliases.yaml (account and owner name normalization), risk_rules.yaml (four flag thresholds and coaching asks), narrative_templates.yaml (sentence templates for forecast output), obligation_map.yaml (regulatory obligation → required capability), product_map.yaml (product name → capability category). No code change needed for new CRM formats, thresholds, stage labels, or regulatory frameworks.

Security

Security layer

Pure-ASGI LocalOnlyMiddleware (web/security.py) rejects requests with non-loopback Host headers and blocks CSRF by requiring a same-origin Origin or Referer header on all mutating requests. Startup assert scans HTTP headers at boot and raises RuntimeError if any external URL appears. Dependency floors in constraints.txt. Security-audit CI on every push and weekly cron.

Key features

Offline-first as a hard invariant — from Streamlit to FastHTML

The original build ran on Streamlit. Streamlit works, but its frontend dependencies pull from a CDN — which made 'fully offline' a policy rather than a property the code enforced. The port to FastHTML 0.14.11 was chosen specifically because that version's fast_app(default_hdrs=False, pico=False) configuration suppresses all CDN headers. That pin is load-bearing: a different version might not. The startup assert at web/server.py scans every response header for src= or href= values containing an external URL and raises a RuntimeError before the server finishes booting if any appear. htmx 2.0.7 is vendored in web/static/. Tailwind CSS v3.4.17 is a standalone CLI binary — not a pip dependency — and the compiled stylesheet is committed to the repo. There is no network call anywhere in the application, and the process won't start if that changes. The socket-guard test goes further: it monkeypatches socket.socket to raise, then runs a full narrative generation pass, kept green as a hard invariant.

LocalOnlyMiddleware — a purpose-built ASGI security layer for local servers

A local web server accepting requests on localhost faces two classes of attack a remote server does not: DNS rebinding (a malicious page rewrites DNS so its requests appear to originate from 127.0.0.1) and CSRF-to-localhost (a page in the browser makes a cross-origin POST to the local app). LocalOnlyMiddleware (web/security.py) is a pure-ASGI layer added after a security review. It rejects any request whose Host header is not a loopback address — 127.0.0.1, localhost, or ::1 — before the request reaches any route handler. It also rejects all mutating requests (POST, PUT, PATCH, DELETE) that lack a same-origin Origin or Referer header. Docker and Azure App Service demo images can opt in to remote access via the SELLER_ADMIN_TOOLS_ALLOW_REMOTE=1 environment variable; in that mode the loopback check is lifted but the same-origin check still applies. The security test module covers ten scenarios: loopback variants, IPv6 loopback, the remote-allow mode, Referer fallback, and the blocking behavior for each attack class.

Request arrives at the FastHTML ASGI appLocalOnlyMiddleware reads the Host headerNon-loopback Host → 403 returned immediately, route handler never reachedMutating request: middleware checks for same-origin Origin or Referer headerMissing same-origin header on a mutating request → 403 returned immediatelyAllowed request forwarded to the FastHTML route
Non-loopback Host or missing same-origin header on any mutating request → 403 before the route handler sees it

Security & ops decisions

Defense-in-depth for a local-only web server
Incoming HTTP request (loopback or LAN attempt)
Host header check: non-loopback Host → 403 before routing — closes DNS-rebinding surface
Same-origin check on mutating requests: POST/PUT/PATCH/DELETE without Origin or Referer → 403
Startup invariant: server won't boot if any response header references an external URL
Socket-guard test: monkeypatched socket raises on any network call from the core path — enforced in CI
Dependency floors: constraints.txt pins pillow and python-multipart against known advisory ranges
Data isolation: CRM exports and runtime DB are git-ignored; sample data is fictional

Builder notes

Lessons learned

What carried forward

Mechanical invariant enforcement over advisory policies: if a requirement matters enough to write down, it matters enough to fail the process when violated. And the config-not-code pattern: behavior that might need tuning, substitution, or localization belongs in data files read at call time, not in function bodies that require a code change.

Posts from this project

Case study in progress.

"A written rule is a suggestion. A gate is a control."
The operating principle behind every project here. The same bug shipped three times past written rules — and zero times past a CI gate. Deterministic enforcement beats advisory documentation, in agent harnesses and security programs alike.