Skip to content

Latest commit

 

History

History
2553 lines (2188 loc) · 134 KB

File metadata and controls

2553 lines (2188 loc) · 134 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[llms-2plot-dev 1.5.0] - 2026-08-31

Sync item 18 — the 1.6.41 remainder, nineteen files as one contract.

Fixed

  • .. exec:: reached only the browser. A markdown2dash directive that renders Dash components puts its output in the React tree alone, while the machine lane, the prerender and the crawler HTML are built from the markdown SOURCE with the directive line stripped. Four pages published prose that referred to a component the reader could not see — /showcase/robots-sandbox/llms.txt read "Move the switches. The document on the right is generated by…" and then stopped. The directive now expands into the module's source through the same fence-aware pass .. source:: uses: one parse, two consumers.
  • The battery probed a path that does not exist and never probed one that does. network_smoke.HIDDEN_DOC_PATHS listed /admin/llms.txt (a redirect, not a page) and omitted /admin/traffic/llms.txt, added with the ledger round. It is now pinned against the page registry.
  • A bare test client sends Werkzeug/x.y, which dimll ≥2.8 puts on the crawler lane, so mark_hidden pages 404 and every-page-200 loops go red at a floor bump. tests/test_agent_key_route.py now names the browser lane and keeps the internal token.

Added

  • A skip link as the first tab stop on every page, and prop tables that scroll inside their own box.
  • nav: frontmatter for short sidebar labels (no page needs one here — the longest name is 18 characters).
  • /changelog carries a real <lastmod>, derived from the newest dated release heading. The sitemap honesty pin learns that second source rather than being loosened.
  • tests/test_exec_lane_parity.py — content pins, derived from the docs tree, that MUTATION-CHECK themselves. The expansion was disabled and every content pin confirmed red before being restored.
  • CD fails fast when superseded (a later commit already serving, via the compare API) instead of timing out, and refuses to push when a release/* branch would make release an unusable ref.
  • GITHUB_URL must resolve on the wire — a wrong repo slug is a live 404 that no amount of "profile vs repo" framing catches.

Changed

  • header.py and test_nav_contract.py are byte-copies again. Both seams this fork reported at 1.6.38 as template-side were closed at 1.6.41 (LOGO_ASSET et al., and a registry-derived aside pin), so DIVERGENCES.md 8 is retired. The byte-identity evidence recorded against 519d496 was stale and is superseded by a re-measurement against 4ac02e0.
  • /api remains unregistereddash_improve_my_llms ships no component metadata, so all four empty-/api mechanisms are moot here.

[llms-2plot-dev 1.4.0] - 2026-08-30

Sync items 16 and 17 — the navigation contract and the battery's lane.

Added

  • /changelog — this file as a Timeline, linked under Home in the sidebar and from the footer, and served as its own llms.txt.
  • A footer — © Pip Install Python LLC, the owner's GitHub profile, Discord and YouTube, every icon labelled.
  • An Admin section in the sidebar, visible to the OWNER. Both admin pages (/admin/control-board, /admin/traffic) were previously hidden from everyone including the owner by a blanket /admin/* path filter; they are now listed for is_admin_user() and for nobody else — the anonymous tree carries no /admin/ href at all.
  • An Other Apps menu in the top bar, built from network_directory.PRIMARY rather than a hand-typed list.
  • lib/aside.py — the aside column collapses on pages with no .. toc::, so /changelog and / render full width.

Changed

  • The sidebar is generated from frontmatter, not from three hand-typed link clusters in components/navbar.py. Every page already declared category:; the navbar simply ignored it. Each page now also declares order:. /audiences/web-crawlers is listed once — it appeared twice, as "Web Crawlers" under This package and as "A · What the crawler sees" under Showcase.
  • The sidebar's Pip Components and Other Apps I've built sections are gone — the network is listed once, in the top bar's menu. Resources is third-party only: dmc. No community.plotly.com, no 2plot.dev.
  • pages/home.py renders through markdown2dash, not dcc.Markdown, so home's headings, tables and code fences match every docs page.
  • /admin/traffic uses dmc.DatePickerInput instead of dcc.Dropdown, and gains a People section — the day's human hits, visitors, sessions and median session — above the crawler tables, with the line that humans never enter the read ledger.
  • Mobile fit: code blocks inside a List item, Blockquote or Timeline scroll in their own box instead of widening the document; the rules are in main.css against public Mantine class names, never per page.
  • /api is NOT registered here, and that is a measurement. dash_improve_my_llms ships no component metadata — no metadata.json, no bundled JS, no generated component classes (checked on 2.8.0). It is a library of routes, middleware and config objects, so there are no props to tabulate. API_PACKAGES is [].
  • The live battery's default User-Agent names the browser lane (item 17). At dimll ≥2.8 a UA with no browser engine token is crawler-lane, so scripts/network_smoke.py's bare internal token made every default-UA check read the prerendered crawler document. A Chrome token now leads, the internal token follows it (substring match, so internal-traffic exclusion still holds), and CRAWLER_UA is untouched.

Fixed

  • Admin URLs no longer leak into the machine corpus. Five pages hyperlinked /admin/control-board in their prose, which put an admin path in /llms.txt. Found by item 16's rewritten tests/test_excluded_links_hidden.py. The prose keeps the words and drops the link — the URL is useless to anyone who cannot authenticate, and publishing it invited crawlers to an admin path.

[llms-2plot-dev 1.3.0] - 2026-08-30

Round 3.4, the posture flip — consumed as sync item 15, with boilerplate.2plot.dev as the other canary.

Changed

  • The AI-training wall is retired (owner decision, 2026-08-30). run.py sets RobotsConfig(block_ai_training=False). It stood while a refused read was the only thing this site could say about a training crawler; since the 2.8.0 ledger round every read is RECORDED and reconcilable — vendor, tier, verdict, bytes, verified — and the hub's Ledger tab agrees with this host's own count (llms 52 = 52). A wall that refuses the read also refuses the evidence. Per-vendor block/meter through the callable vendor_policy seam is the instrument now: aimed, live, revocable per request, which a blanket bucket never was. Claude-User, Claude-SearchBot, ChatGPT-User, OAI-SearchBot and PerplexityBot were never in the training bucket and are unaffected.
  • In-process, both real UAs (ClaudeBot, GPTBot), before → after: / 403 → 200 (13,778 B crawler document) · /llms.txt 200 → 200 · /healthz 403 → 200. On the wire at 14:09Z the pre-flip triple was identical to the in-process one, which means every 403 this host serves is the APP's — no separate edge wall has been observed here.
  • Three robots.txt fingerprint checks became posture checks (tests/test_llms_routes.py, scripts/network_smoke.py, scripts/smoke_live.py). ClaudeBot -> Disallow: / was asserted as proof the running artifact was the intended package; it was really a statement of policy, and policy changed. The vendor SPLIT still fingerprints the package (OAI-SearchBot, Claude-User, Claude-SearchBot all Allow: /), and ClaudeBot/GPTBot/CCBot are now asserted NOT to carry Disallow: /.
  • tests/test_vendor_policy.py's default-posture pin now asserts 200 where it asserted 403, and tests/test_showcase.py asserts the panel shows the served document rather than a 403 — the showcase exists to show the truth about this host, so it moves with the posture.
  • On the wire after deploy (build 625c91c, 22:05Z), both real UAs: / 200 (14,133 B crawler document) · /llms.txt 200 (13,801 B) · /healthz 200, and /robots.txt carrying Allow: / for ClaudeBot, GPTBot and CCBot. No edge wall appeared when the app's came down — every 403 this host ever served was its own, and the owner has since confirmed no Cloudflare rule exists on the zone. The DIVERGENCES.md posture fence is re-dated to 200/200/200.

[llms-2plot-dev 1.2.0] - 2026-08-29

Consumed SYNC-1.6.22-1.6.35 items 12 and 13 at dash-documentation-boilerplate 1.6.35 (4c63992). The ledger round and the release-branch round. Both items are contract-class: ported into this fork's shape, not byte-copied. Per-item dispositions are in the sync report.

Added

  • /admin/traffic — this host's own crawler ledger, behind the control board's exact gate (fails CLOSED without Clerk, for the same reason). Vendor × day, vendor → tier for a picked day, top paths per vendor, and the v3 headline numbers for the same day so the two accountings can be read side by side. Plain tables, no charts, no interval callback: a 14 × 40 table of strings is about a millisecond, five charts were ten seconds (fleet fact 18).
  • The read table. dash-improve-my-llms 2.8.0 emits one event per corpus document it serves (on_document_read) and does no I/O with it; run.py registers AnalyticsTracker.record_read once, which keeps the row as a reads table in the SAME analytics file — same buffer, lock, flush cadence and retention as visits, with client_ip dropped unless ANALYTICS_KEEP_CLIENT_IP=1. reads is JOINED by the rollup, never summed into human_hits / bot_hits / pages.
  • Rollup v4, additive and present only on a day with reads: vendors[] (one row per vendor key × verified × policy, with per-tier counts and bytes, null key kept as the unverifiable bulk) and reads. Every v3 key is byte-identical; the reporter POSTs what daily_rollup returns and is unchanged.
  • CD promotes mainrelease. .github/workflows/cd.yml's deploy job now fast-forwards release to the run's own sha after the matrix is green, and Render watches release (render.yaml). needs: [test] is the whole gate, so release cannot receive an uncertified commit by construction. tests/test_cd_promotes_release.py pins the structure.
  • A posture fence in DIVERGENCES.md — measured, not intended: ai_bots, healthz: full, runtime: python, deploy: release-branch.

Notes on the first promoted run

  • The first promoted run went red on its build-match wait, and that was the owner step outstanding, not a defect. 5c73a53 reached main at 17:43:00Z and release at 17:45:24Z; at 18:22:44Z /healthz still served ae1dce6. This service is not Blueprint-managed, so render.yaml's branch: is documentation and the dashboard Branch field is the switch — item 13 says exactly this in its notes. The owner switched it at ~18:00Z and the next run (33281935425, 0081f65) went fully green in five minutes: promote, wait, and verify including its /healthz build == github.sha step. main == release == wire.
  • On the same run, item 13's verify fix proved itself: the job was skipped rather than run. Under the previous always() && != 'cancelled' && != 'skipped' gate it would have run after the failed deploy and reported GREEN against ae1dce6 — the previous build. That is the defect the gate change exists to prevent, observed here on the first run that could exhibit it.

Changed

  • human_hits DROPS and bot_hits RISES from this release. UA-less and library clients (httpx, Go-http-client, node-fetch, an empty User-Agent) move from the human lane to the crawler lane, because classify() puts them there and this app no longer disagrees with it. The hub's day-over-day view will show a step on the adoption date. That is the number becoming true, not a regression.
  • There is ONE classifier. lib/analytics_tracker.py delegated is_bot and detect_bot_type to dash_improve_my_llms.classify() and now ends with ZERO User-Agent strings. Its own lists had filed ClaudeBot — Anthropic's TRAINING crawler — under "search", still named the retired anthropic-ai / claude-web tokens, and knew nothing of bytespider or Claude-User. The names and signatures are kept for callers. Crawler rows gain vendor_key, vendor_class, verified, lane; human rows are byte-identical to before, and the INTERNAL_UA_TOKEN drop still happens FIRST.
  • The floor moves to dash-improve-my-llms>=2.8.0 in every encoding: requirements.txt (four lines), run.py's LLMS_PKG_FLOOR, and CI's install line plus both version asserts. Not a degradable feature — lib/analytics_tracker imports classify and _ledger.EVENT_FIELDS at module scope.
  • verify runs only on needs.deploy.result == 'success'. The old always() && != 'cancelled' && != 'skipped' admitted failure, so a failed promote could still be followed by a green verify of the PREVIOUS build. Verify's first step now also asserts /healthz build == this run's sha itself. This fork's SITE_URL guard (DIVERGENCES.md 4) is kept as a second conjunct.

Removed

  • The Render deploy-hook secret, and every trace of its name from cd.yml. A push to main is no longer a deploy; it is a candidate.

Fixed

  • tests/test_proxy_scheme.py sends BROWSER_UA. At the 2.8.0 floor an absent User-Agent is the crawler lane, so the UA-less probe received the crawler document — which carries no twitter:url — and failed on "no tag" without saying anything about the forwarded scheme. Either lane can be the one you did not mean to test.

[llms-2plot-dev 1.1.0] - 2026-08-27

Kit adoption — consumed SYNC-1.6.10-1.6.16, SYNC-1.6.17-1.6.21 and SYNC-1.6.22-1.6.29 at dash-documentation-boilerplate 1.6.29 (5589318). This host was a live kit-lineage site that had never been on the fleet roster; it joins with this release. Per-item dispositions are in the sync report; the deliberate differences are now recorded in DIVERGENCES.md.

Added

  • The .claude/ development kitCLAUDE.md (this site's own guide above the network's behavioral contract and verification traps, both ported verbatim), settings.json pointed at THIS host, and the three shipped skills (wire-verify, sync-template, report) byte-verbatim. .gitignore moves from a blanket .claude/ ignore to the template's ALLOW-LIST form, which is what keeps credentials under .claude/ structurally uncommittable, and gains the session-document block.
  • DIVERGENCES.md — six recorded divergences with reasons, plus the machine-readable byte-owned fence (empty: this fork makes no byte-level claim on any sync-verbatim path, and drift is never fenced).
  • tests/test_claude_kit.py, tests/test_auth_demos.py — byte-verbatim kit cargo. tests/test_python_version.py — the one-fleet-Python agreement pins.
  • python on /healthz, all three backends, plus the battery's python_matches_declared check: the serving interpreter is now on the wire, so a stale image can be contradicted from outside.
  • The configured-auth branch is certified. Two pins render lib.auth.register() with a FAKE non-empty Clerk config — the branch a zero-secret suite had never executed, including the pk_live satellite-mode auto-enable that can only exist in production.
  • CI asserts Docker's own health verdict (docker inspect .State.Health.Status), failing on none: the external curl proves the app answers, never the HEALTHCHECK instruction itself.

Changed

  • One fleet Python: 3.14, in every encoding at once — Dockerfile (python:3.14-slim, a MINOR tag: the old 3.11.8 patch pin could never receive a 3.11.x security release), the CI matrix main, the lint and pip-audit jobs, cd.yml's verify job, and render.yaml's PYTHON_VERSION (full X.Y.Z, as Render's native runtime requires). The window legs are 3.13 and 3.12.
  • Dockerfile honors $PORT — shell-form CMD with the default at the point of use (${PORT:-8550}), and the HEALTHCHECK probes the same variable. Exec-form CMD never expands env, so the old form hardcoded the port whatever the platform asked for.
  • render.yaml names THIS host. Every identity field still said boilerplate — service name, domain, APP_BASE_URL, SATELLITE_APP_KEY, AD_APP_ID — inherited at fork time and never corrected, while production served llms.2plot.dev with app: "llms". POLICY_STORE_FILE is now declared on the mounted disk beside the other two stores.
  • CD is sized for the worst build: the build-match wait runs 100 × 15s under a 30-minute job timeout (a floor bump busts the pip cache by design, so this pipeline's most important deploy is also its slowest), a hookless deploy emits ::warning rather than a quiet notice, and the verify job now stands down on a skipped deploy as well as a cancelled one.
  • .github/dependabot.yml — the 1.6.24 rewrite: the pip ecosystem is removed entirely. On range requirements dependabot can only propose FLOOR RAISES, so the old allow-list group structurally produced the very PR class it existed to suppress. Floors move through sync specs, every encoding at once. Security updates ride GitHub's separate channel.
  • scripts/smoke_live.py is the template's current file: the auth POST now carries the same SSL context as fetch (without it every POST died in the macOS handshake and read as missing auth wiring), and wake() tolerates a legacy fetch stub. A source pin in tests/test_auth_wiring.py holds the SSL half — no wired test can, they all monkeypatch post.
  • The gate card promises only what ships — "and the AI assistant" is gone from the demo-card copy; nothing here wires one.
  • lib/auth_demos.py points at a demo this site can render. The inherited /examples/visualization entry named a page and a module that exist on no fork; every gate card rendered demo-less and silent since fork time. This site's hero is /showcase/robots-sandbox.

Removed

  • The vestigial Node layer (template issue #12, CVE-2026-1615, removed upstream in 1.6.9 and carried here with this Dockerfile sync). package.json / package-lock.json were dash-mantine-components' component-build toolchain, inherited through the fork lineage and used by nothing in this repo — no webpack config, no src/ts, no CI job, no served asset — while the image apt-installed nodejs+npm and npm installed a known-vulnerable jsonpath@1.1.1 into every production build.

[llms-2plot-dev 1.0.0] - 2026-08-22

This repository forked here. Everything below this entry is the history of dash-documentation-boilerplate, the template this site was forked from at 1.6.7 — kept because the machinery is inherited and its reasoning still applies. Everything from here up is llms-2plot-dev, the documentation site for dash-improve-my-llms and the 2plot network's owner-control bench.

Added

  • The site's own content. Three audience pages (/audiences/mcp-clients, /audiences/web-crawlers, /audiences/llm-context — URLs preserved byte for byte from the retiring service), a five-page Reference section, and three showcases that run the package's own pure handlers in-process.
  • lib/policy_store.py — the writable layer. Flock-guarded JSON, validated on write, atomic on replace, fail-open on read, re-stat'd on every call. Reaches dash-improve-my-llms 2.7.0 through its callable seams, so a control-board toggle lands on the next request in every worker with no restart.
  • The control board's country guardrail — a click-to-select world map over the inherited page-visibility board, with the admin gate re-checked server-side in the write callback.
  • BUGS-2.7.0.md — the pre-release soak that gates the package's tag.

Changed

  • Identity, on every surface: brand, description, origin, favicons from the hook mark, header logo and wordmark, the GitHub link, the social-card object, and the JSON-LD blocks.
  • The template's documentation is DELETED, not hidden (owner decision). excluded_links hid the eleven tutorial pages from the sidebar but left them in sitemap.xml, /llms.txt, /llms-full.txt and the MCP resource set — so this host would have published the boilerplate's documentation as its own. This overrides the migration kickoff's "NEVER deleted (wave-sync purity)" rule: template syncs touching docs/ now need resolving by hand, and that cost was accepted for a site that stands on its own.
  • Sixteen 301 redirects for retired and deleted URLs.

Notes

  • requirements.txt still floors at dash-improve-my-llms>=2.6.1. 2.7.0 is unpublished, so every 2.7.0 call site sits behind the LLMS_HAS_27 capability probe in run.py and the app boots on either release.

[1.6.7] - 2026-08-22

Added

  • Auth-wiring guards, both halves (the flexlayout finding): dash-clerk-auth wires either side of Dash(...)register() is the UI half, configure_app(app) the server half (/api/auth/* routes + per-request identity). Flexlayout's batch-2 pass shipped the first call without the second: components rendered and ClerkJS reported signed-in while every server render read signed-out — the control board served the owner the sign-in card forever, POST /api/auth/session answered 405 through Dash's GET-only page catch-all, and sign-out never revoked. Invisible to every suite, because Clerk is off in test environments and configure_app no-ops without keys. Two guards now, one per environment: tests/test_auth_wiring.py pins structurally (AST) that run.py calls BOTH halves; scripts/smoke_live.py gains an "Auth wiring" block that POSTs both endpoints on the live host (registered = 2xx/4xx; unregistered = 404/405), gated on the package's inline bootstrap being present in the served shell so clerk-off hosts skip rather than fail. Measured baselines: boilerplate answers 401/200, flexlayout answered 405/405. Note: the battery's POST probes need real egress — sandboxed environments that allow only GET report transport-0.

[1.6.6] - 2026-08-22

Changed

  • dimll floor 2.6.0 → 2.6.1 (requirements incl. the commented backend extras, run.py's boot floor + its message, and the test — the floor lives in more than one place; all moved together). 2.6.1 makes the universal prerender VISIBLE to non-JS consumers: below it the injected block carries a literal hidden attribute, so every visibility-respecting reader (html-to-text extractors, arguably crawler content-weighting) saw only "Loading..." — the outside-audit finding of 2026-08-22, diagnosed live across six hosts and fixed at the package. The generic-UA prerender test now asserts the fixed shape: div without hidden, plus the marked synchronous hide script that keeps JS browsers flash-free (React's mount wipes the pair, so nothing changes for humans). The fleet inherits 2.6.1 on each host's next deploy with no requirements edit; this release is the reference host's own pickup plus the floor that makes the guarantee permanent.

[1.6.5] - 2026-08-22

Batch-1 closeout: the wave's other three hosts (emojimart, modelviewer, excalidraw) shipped dark, and four of their findings trace to this template. All four are fixed at the source so batch 2 and every future fork inherit the fix instead of rediscovering it.

Added

  • Runtime-imports guard (tests/test_runtime_imports.py, the modelviewer finding): a fork died in production on a function-local import PIL that every dev machine happened to satisfy — suite green, boots locally, dies in a clean image, and one docs example took all ten pages down because Dash imports every page at construction. The test AST-walks every runtime module and asserts each absolute import resolves in the environment CI installs (requirements.txt and nothing else); nesting is deliberately ignored because it does not predict boot-fatality. The optional-backend exemption (fastapi/quart select by env) is earned by two companion tests: the extras must stay documented as commented requirements lines, and the carrier modules must never be hoisted to run.py's unconditional top level. A third companion pins that runtime code never imports build-time scripts/.
  • CSS hygiene guard (tests/test_css_hygiene.py, the excalidraw finding, landed at the source): fails on any hashed .m_* Mantine selector in assets/*.css. Three forks have paid for this class — leaflet's floating drawer, emojimart's 63vh drawer, and excalidraw inheriting two dead-or-harmful hashed rules from this template.
  • modelviewer + excalidraw joined the canonical network directory (lib/network_directory.py): both were deliberately absent until they deployed; both are live and build-identity-verified as of 2026-08-21/22. The fleet re-copy carries the entries everywhere.
  • Markdown tables scroll in their own box (table.m2d-table, GitHub's recipe: content-width, capped at the container, scrollable past it — the excalidraw finding): a <table> is min-content sized, so one wide prop table dragged an entire page 105px sideways at 414px. A no-op for tables that already fit; covers kwargs prop tables too, since markdown2dash stamps the class on every table.

Fixed

  • The three hashed-selector fossils removed from assets/main.css (dmc-docs fork era, present since the initial commit): .m_46b77525 put an !important margin on every Input wrapper in every docs example; .m_5caae85b was dead in DMC 2.7 and 2.8; .m_9cdde9a restated Mantine's own aside declarations around one intentful pixel — the TOC's 15px breathing gap, which moved to the static aside.mantine-AppShell-aside rule.
  • scripts/make_favicons.py now flattens the apple-touch icon onto opaque white (the emojimart finding): iOS composites the icon's alpha onto its own background — black on some surfaces, white on others — so every fork that ran this script shipped an icon that renders differently everywhere it appears. Every other size keeps its transparency. The template's own apple-touch-icon.png is regenerated (the other seven files regenerated byte-identical, confirming provenance), and a header-level PNG colour-type test pins opacity without needing Pillow in CI.
  • The header wordmark now hides below xs with the accessible name preserved — the pattern both modelviewer and excalidraw needed and implemented divergently. visibleFrom keeps the node in the DOM (the typing animation still finds it) but display:none DOES remove it from the accessibility tree, so the home link now carries a permanent aria-label and the logo img is explicitly decorative (alt=""). Without the label, phones would get a home link with no name at all — the modelviewer defect, which excalidraw's pass reasoned incorrectly about and likely still ships.

[1.6.4] - 2026-08-21

Two fleet-class fixes surfaced by the wave's first pair, landed at the source so the other eighteen forks inherit them.

Fixed

  • CD now verifies the artifact it shipped, not "whatever is live" (the muicharts finding): with RENDER_DEPLOY_HOOK_URL unset, the old workflow skipped the wait and ran the live battery seconds after the push — against the previous release, every run, invisibly. /healthz now reports the running instance's commit (RENDER_GIT_COMMIT, optional field — the fleet probe contract is unchanged), and the CD wait holds until it matches the run's SHA, falling back once (with a warning) on builds predating the field.
  • The byte-copy identity trap (the pannellum finding): the reporter must stay byte-identical across forks, so its fallback app key says "boilerplate" everywhere — while a fork's other modules default to the fork's own key. run.py now claims the identity via os.environ.setdefault("SATELLITE_APP_KEY", ...) before any hub-facing import — the marked FORK POINT; forks change that one string and keep the reporter byte-identical. A real env value always wins.

[1.6.3] - 2026-08-21

Changed

  • Vendored dash-clerk-auth 1.0.4 → 1.0.5 (sha256 a2f9062e…b74f3, full provenance in requirements.txt). Fixes the return-trip stale gate the owner observed live: landing back on an auth-gated page after signing in on the primary showed the gate card until a manual refresh, because the first server render precedes __dca_identity minting. 1.0.5 syncs the session and reloads once, with a sessionStorage no-loop marker shared by both reconciliation paths. The provenance rule is now general: only the recorded sha admits a tarball — stale early builds have bitten on both of the last two releases and are indistinguishable by name, size, or date.

[1.6.2] - 2026-08-21

The pre-wave hygiene pass, from the four-repo review.

Fixed

  • Date-skew corrections (leaflet handoff §8): seven committed provenance stamps read 2026-08-22 for events whose verified date is 2026-08-21 (git author dates corroborate) — CHANGELOG headers 1.5.3–1.6.1, components/header.py, lib/ad_client.py, requirements.txt. All corrected; the three release commit SUBJECTS carrying the wrong date are immutable and stand corrected by this entry. A date nobody can trace is worse than no date.
  • docs/authentication/authentication.md now documents the control-board override layer (override → frontmatter → PAGE_DEFAULT_TIER, hub ceiling on top) instead of contradicting shipped behavior; lastmod bumped accordingly.
  • run.py's floor failure message now names what a 2.5.x actually loses first — silently swallowed lastmod, the lying sitemap — matching the comment that raised the floor.
  • lib/auth.py's signout-shim docstring caught up with reality (upstream fix shipped in 1.0.3/1.0.4; the shim is a deliberate duplicate until the fleet-wide retirement pass).

Changed

  • README caught up three releases: dimll floor 2.5.1 → 2.6.0 in five places, a new Access Control & Live Page Management section (control board, admin allowlist, gate teasers), the mobile-drawer standard under UI/UX, and the admin env vars in Configuration.
  • .env.example gains the admin surface (ADMIN_EMAILS, ADMIN_USER_IDS, ALLOW_UNGATED_ADMIN) — the gate for the 1.6.0 headline feature was previously undiscoverable from the env template.
  • .claude/CLAUDE.md Customization Points now lists the control board, the override store, and the auth-demo teasers.

[1.6.1] - 2026-08-21

Fixed

  • Accessibility + agentic-browsing names on the header's icon controls (hamburger, theme toggle, GitHub link — create_link now requires a label), and the network-ad image reserves a square box via aspect-ratio so the aside no longer layout-shifts when the creative loads. All three were Lighthouse findings on the pilot host measured against template code — every fork inherits the fix.

[1.6.0] - 2026-08-21

Every fork gets its own live control board — the leaflet pilot's proven UX, ported with its scar tissue included.

Added

  • /admin/control-board (pages/control_board.py): flip any docs page between public / auth / admin / hidden and toggle its llms.txt exposure, live — changes apply on the next render, no restart. Gated by the ADMIN_EMAILS/ADMIN_USER_IDS allowlist + owner; fails CLOSED without Clerk (ALLOW_UNGATED_ADMIN=1 for local work), and the write callback re-checks the gate server-side (pattern-matching callbacks stay callable by anyone who can POST). The board stays OUT of both tier ledgers — its machine surfaces are silenced package-side via mark_hidden() (sitemap, llms.txt, MCP, prerender, crawler HTML all treat it as absent) so access.gating_configured() stays False on all-public forks and the hot path stays check-free.
  • lib/page_visibility.py — the override store, with both fleet lessons built in: mtime-throttled cross-worker reload (a toggle lands on every gunicorn worker within ~1s — the pilot's coin-flip defect) and loud persistence guards (boot warns when PAGE_VISIBILITY_FILE is unset OR points under /var/ without a real mount — the twice-observed silent-reset-per-deploy class).
  • Override-first resolution in lib/access.py: board override → frontmatter → env default, with the hub ceiling still applied on top (an override can loosen a local declaration, never a network restriction). pages/markdown.py registers every docs page on both ledgers from the one declared value.
  • The sign-in card's live-demo teaser now ships ARMED: DEMOS carries a working entry (/examples/visualization → the theme-aware chart), so gating that page shows "Live demo — try it" above "Authentication required — You're looking at a live preview of {page}. Create a free account to unlock the full documentation — every interactive example, the complete API reference, and the AI assistant."
  • render.yaml + .env.example: PAGE_VISIBILITY_FILE on the /var/data disk, with the blueprint-vs-dashboard drift warning inline. 14 new tests (tests/test_control_board.py).

[1.5.4] - 2026-08-21

Changed

  • Navigation order: "Other Apps I've built" now sits above "Resources", and "Resources" is the LAST section — own-work ranks above third-party links, and the only section that navigates away from the network closes the list.

[1.5.3] - 2026-08-21

Changed

  • Vendored dash-clerk-auth 1.0.3 → 1.0.4 (sha256 7a7c333a…cf701a, recorded in full in requirements.txt with the stale-first-build warning). What 1.0.4 fixes, from the live network certification: the FastAPI auth endpoints were never callable (un-annotated request param → required query field → 422 on every POST — inert on this Flask host, fatal on fastapi ones), and the ghost-cookie fresh-load case — a page loading with ClerkJS signed-out while the server still held the identity now reconciles with a signout POST + single reload, which is the cross-host sign-out path no click shim can cover. revokeServerSession also verifies its response now. The 1.5.1 shim remains an idempotent duplicate; retirement is one clean release cycle after the fleet is on >=1.0.4.

[1.5.2] - 2026-08-21

Changed

  • Vendored dash-clerk-auth 1.0.2 → 1.0.3 (sha256 2c6b40f4…da1944, recorded in full in requirements.txt — the tarball IS the release; there is no PyPI for this package). 1.0.3 fixes sign-out revocation package-side (both entry points + the signed-in→signed-out listener transition, so sign-outs propagate across tabs and hosts), replaces the DiceBear default avatar with an inline SVG data URI (no third party in the UI path), and discards non-absolute satellite_sign_in_redirect values loudly. 1.5.1's app-side signout shim is idempotent alongside it and retires next release.
  • Provenance caveat recorded in requirements.txt: vendor from the hook repo's dist/ artifact ONLY — its main currently holds a broken build (boot-time collection error on Python 3.10/3.11) until the import-fix PR lands; verify the sha before re-vendoring.

[1.5.1] - 2026-08-21

Pilot-week hotfix: Sign Out that actually signs out, and an honest floor comment.

Fixed

  • Sign Out now revokes the server session. dash-clerk-auth 1.0.2's logout runs window.Clerk.signOut() client-side and reloads — but the server keeps trusting the signed __dca_identity cookie (max-age session_lifetime_days, default 7 days) and the Flask session it minted at sign-in, so a signed-out browser kept rendering every auth-gated page; on a shared computer the next person inherited the previous user's access. The package ships the endpoint that fixes this (POST /api/auth/signout) but nothing ever called it. New lib/auth.py:_install_signout_delegation() — a capture-phase delegate on the logout menu item (the sign-in delegation's proven pattern) — owns the click and sequences Clerk.signOut() FIRST (so the slow path can't re-verify __session and re-mint), then the server signout, then the reload, awaited so the reload never races the cookie clears. The package-side fix ships in dash-clerk-auth 1.0.3; this delegate is idempotent alongside it and retires a release after the fleet vendors >=1.0.3.

Changed

  • Floor-comment honesty (run.py, pages/markdown.py): 1.5.0's claim that passing lastmod= "TypeErrors on anything older" was false — measured on 2.5.1 by the pip-docs+ stage-4 session, the signature is (path, name=None, description=None, llms_doc=None, **kwargs), so older packages accept the date and silently ignore it. The 2.6.0 floor stays load-bearing, but for honesty (below it, every stamped date is swallowed and the sitemap goes back to swearing everything changed at build time), not crash avoidance.

[1.5.0] - 2026-08-20

The reference host proves dimll 2.6.0 (stage 2 of the network rollout order). The floor is load-bearing: pages/markdown.py passes lastmod= unconditionally, which TypeErrors on anything older.

Changed

  • dash-improve-my-llms[flask]>=2.6.0 (was 2.5.1), and LLMS_PKG_FLOOR = (2, 6, 0). What arrives: icon autodiscovery, truthful sitemap <lastmod>, JSON-LD publisher.logo, and the llms.txt viewer banner de-dup (package-side, free).
  • Every docs page's frontmatter now declares lastmod: with its REAL git last-commit date (2025-11-09 through 2026-08-19 — eleven pages, zero invented dates). The Meta model gains the field with a YAML-date-to-ISO validator; register_page_metadata passes it through; unset pages omit the tag — truth or silence. Deliberately not scripted from file mtimes, which reset on every Docker build and would re-invent the daily-lie sitemap 2.6.0 exists to end.
  • configure_seo(icons=)'s .ico entry moved to the assets/favicon/favicon.ico copy (byte-identical to the root one index.html links) so the declared list is SET-equal to what 2.6.0's discovery finds.

Added

  • tests/test_seo_icons.py: discovery-vs-declaration set-agreement (the proof the fleet can rely on discovery alone once its pixels are right — order-inequality is not a failure, per the release notes) and sitemap-honesty pins (every emitted <lastmod> traceable to a frontmatter declaration; the undeclared home page carries none).

[1.4.1] - 2026-08-19

Changed

  • dash-clerk-auth is now installed by requirements.txt (from the vendored tarball) rather than riding the image uninstalled. 1.4.0 shipped the whole sign-in surface — avatar, gate cards, delegation — but the deployed reference site could not render any of it because the package it wires was never on sys.path. Runtime posture is unchanged: with no CLERK_* keys the site is exactly as public as before, so forks inherit the capability, never a login wall. Alongside it, the fleet security floors are now asserted rather than merely permitted: clerk-backend-api>=7.0.0,<8 and cryptography>=50.0.0 (the four-advisory baseline dash-clerk-auth 1.0.1 widened its cap for).

[1.4.0] - 2026-08-19

The interactive gate and the real-time half of the fleet's analytics land on the template. Humans meet a sign-in card on gated pages while agents keep reading the machine surfaces through the data window — the two lanes split onto separate axes, each flipped per host by one env var. The satellite reporter grows a presence beacon so the hub board can show "active right now" without waiting for a rollup. On THIS host the gate ships dark twice over: every tier is public, and dash-clerk-auth is deliberately not in requirements.txt (the vendored tarball exists for the docs' optional-auth install command) — the presence beacon is what this deploy turns on.

Added

  • The interactive gate (lib/gate_layouts.py): every markdown docs page renders through a per-request verdict — sign-in card at HTTP 200 (with an optional live teaser demo via lib/auth_demos.py, table empty in the template), forbidden and 404 cards, the content on allow. The verdict is the new access.resolve_page_access(): docs fall open without Clerk, admin fails closed, and ?key= never unlocks a browser layout. The gate switch is PAGE_DEFAULT_TIER=auth per deployment; /, /getting-started and the corpus pseudo-paths are pinned public so no env flip can gate the funnel. Card buttons ride assets/auth_gate.js/.css (satellite mode navigates to the primary with ?returnTo=; local dev opens the Clerk modal).
  • The second tier axis, llms_public (frontmatter, or LLMS_PUBLIC_DEFAULT, default open): a gated page's machine twin — /<page>/llms.txt, crawler HTML, the prerender — stays public while the interactive page is gated. That split is the data-window posture, and the later agent flip is LLMS_PUBLIC_DEFAULT=0, env only. The exemption never applies to a hub-imposed tier: a satellite's env default cannot loosen what the network restricted.
  • GET /api/agent-key (lib/agent_key.py, all three backends): turns the browser's Clerk session into the hub-minted ?key= that the "Copy for LLM" button (assets/llms_copy.js) now appends, so a copied URL keeps working inside an assistant that has no cookie. 204 for anonymous / Clerk-off / hub-down; Cache-Control: private, no-store always; the token is read from the __session cookie, never the query.
  • The presence beacon (lib/satellite_reporter.py): a second, fail-silent daemon thread POSTs {app, active} to the hub's /api/satellite/active every 60s (SATELLITE_PRESENCE_INTERVAL_S, floor 30, 0 disables) — distinct human visitors inside the session window, the same derivation as the hub's own count. Display-only and ephemeral hub-side; the daily rollup stays the sole source of the daily numbers. A hub that predates the endpoint 404s harmlessly.
  • Clerk avatar in the header (components/header.py::create_clerk_avatar), rendered only when Clerk is configured.

Changed

  • render.yaml: rollup cadence SATELLITE_REPORT_INTERVAL_S=900 (the fleet is on paid instances and the hub board now reads near-real-time), the full Clerk satellite env block, and the two gate knobs — remembering that env/plan changes apply on Blueprint sync, not git push.
  • lib/auth.py: the hand-rolled 0.9.0/0.9.1 satellite fixups are retired — both are upstream in the vendored dash-clerk-auth 1.0.2. What remains is capture-phase delegation (_install_satellite_signin_delegation, back-ported from the leaflet pilot 2026-08-19): late-rendered #clerk-login-buttons get exactly one handler, preferring buildSatelliteRedirect() with ?returnTo=, falling back to redirectToSignIn on origin+pathname so stale __clerk_* params never ride into the next sign-in.
  • The corpus pseudo-paths (/llms-small.txt, /llms-full.txt) register public explicitly instead of falling through the tier default, so PAGE_DEFAULT_TIER can never gate them; / likewise (it registers via pages/home.py, which no frontmatter ever tiers).
  • Vendored dash_clerk_auth 0.9.1 → 1.0.2 (the clerk-backend-api <8 cap for the cryptography>=50 floor, plus the avatar session fix).

Fixed

  • The peer-host key-leak test judges parsed origins, not substrings — bare-host matching flags a site's own links whenever a peer host is a substring of its own (2plot.devleaflet.2plot.dev; found by the leaflet pilot, this repo was saved only by its hostname). The invariant stated properly: any URL carrying a key must be same-origin.
  • lib/agent_key.py records why it must not use from __future__ import annotations: PEP 563 turns the FastAPI Request annotation into a string resolved against module globals, where the locally imported class does not exist — the parameter silently becomes a required query field and the route 422s.

[1.3.0] - 2026-08-15

Instrument first: the 402 groundwork lands on the template. The network's metered lane is gated on ~30 days of crawl data (owner decision 2026-08-10); this release is what makes that data exist and stay true on every satellite forked from here — machine-surface demand reported per document, counted once, tested, and tierable per deployment. No payment code ships here. Rollout plan: kickoff/KICKOFF-x402-instrumentation-rollout.md (local).

Added

  • The daily rollup now reports the machine surfaces (the network's v3 analytics fields): unique bot visitors per day (bot_visitors, a daily distinct count), and llms.txt / robots / sitemap / page.json rows in pages with a per-row bot split — mirroring the hub's own self-report semantics exactly. These fetches were always recorded; they were only hidden from the report. A day with only machine-surface fetches is now reported instead of skipped — crawlers hammering llms.txt with zero human visits is exactly the signal the hub's day-pass board exists to see.
  • The machine-surface rollup is tested (tests/test_traffic_rollup.py) — 15 hand-checkable cases pinning the partition (every path is a page visit or a machine-surface hit, never both), the machine-only-day report, the per-row bot split, and the distinct bot_visitors count. This data is the evidence base for the network's 402 pricing decision; untested measurement code deciding a revenue model was the wrong risk to carry.
  • Tier registrations for the corpus documents. Every satellite built from this template now declares access tiers for /llms-small.txt and /llms-full.txt (served by dash-improve-my-llms ≥ 2.4.0; inert on older versions): LLMS_SMALL_TIER / LLMS_FULL_TIER env vars set them locally (unset = public; documented in .env.example and visible in render.yaml so every fork sees the knob), and the hub's page-tier ceilings can tighten either network-wide with no redeploy here. The dependency-floor message notes the 2.4.0 requirement for the tier documents.
  • Generic version placeholder {{VERSION:<distribution>}} (new lib/versions.py, used by both markdown loaders). Prose may now state the installed version of any package — not just dash-improve-my-llms — so every satellite can write {{VERSION:<its-pypi-name>}} for the library it documents and a package upgrade propagates to the browser page, the copy button, /llms.txt and every /<page>/llms.txt on the next deploy, with no prose edit. {{DIMLL_VERSION}} remains as a legacy alias. Fenced code blocks and inline code spans are left verbatim (the network-standard page shows the syntax in a fence), and a placeholder naming an uninstalled distribution fails the boot instead of leaking. The identity tests now also sweep for bold version claims next to any PyPI link, not only dash-improve-my-llms's.

Fixed

  • Machine-surface fetches were double-counted. _SKIP excluded /llms.txt, /robots and /sitemap from page visits by substring — but /llms.txt does not substring-match /llms-small.txt, so the tier documents and page.json twins landed in BOTH load_visits and load_agent_hits, inflating human_hits/bot_hits/pages for exactly the surfaces the 402 board prices. _SKIP now names all three. The hub's traffic_insights._SKIP has the same gap (its comment claims the exclusion; its tuple doesn't deliver it) — port this fix there before the data window opens.
  • Dash-built components rendered empty props tables. The numpy-docstring branch in lib/directives/kwargs.py (for dash-mantine-components' hand-written docs) shadowed the base markdown2dash parser for the Keyword arguments: format that dash-generate-components emits — the format of every component a library satellite documents — so their .. kwargs:: tables rendered silently empty. Found on muicharts' /api; pannellum's likely affected too. The directive now falls back to the base parser for that shape.

[1.2.5] - 2026-08-01

Fixed — scripts/smoke_live.py failed CD on healthy sites

The post-deploy battery is the fleet's deploy gate: cd.yml runs it against the live host after every merge and its exit code decides whether the run goes green. Its fetch was a single urlopen — no retry, no wake-up — while most of the fleet sits on Render tiers where a cold start or a dropped connection is routine. Measured on dash-flows-upgraded: two runs minutes apart against the same host, FAIL canonical on /interactions then ok canonical on /interactions. A misdiagnosed failure is worse than a slow one; it sends you to look at canonical tags that were correct all along.

Both fixes already existed in-fleet and never met (blueprint LESSONS §21 states the rule outright):

  • A wake-up loop before the first check. /healthz is polled up to 24 times, 10s apart — deliberately wider than §21's "12×5s is plenty", because a free-tier cold start routinely takes 60–90s and the window only costs time when the host is actually down. Awake means ok: true, not any 200: Render's loading page and a CDN error page can both be 200s. A host that never wakes is ONE failure ("nothing else was tested"), not a cascade of forty per-check failures that all mean the same thing.
  • A retry ladder inside fetch — the shape scripts/network_smoke.py already had, and that leaflet's copy of this very script grew without the fix ever flowing back to the canonical here. Transport errors and 5xx retry with backoff; 2xx/3xx/4xx return immediately, because a 404 is a verdict and retrying it only slows the battery. Retries print to the CD log — a green run that shows retries is a host worth watching.

Proven live before shipping, twice over: on the first run after the change, flows' llms.txt dropped the connection mid-body (IncompleteRead) and passed on retry — the exact flake that triggered this fix — and a run against email.2plot.dev saw its OWN pages do the same on two fatal checks that would have turned that deploy red.

Tunables (env, so satellites stretch them without editing the file): SMOKE_WAKE_ATTEMPTS, SMOKE_WAKE_INTERVAL_S, SMOKE_FETCH_RETRIES. Exit semantics unchanged; no check weakened, removed, or reordered. The file remains the canonical copy — satellites take it verbatim on their next touch.

[1.2.4] - 2026-08-01

Fixed — the network bulletin was never wired up

NETWORK_BULLETIN_URL has been set in production, pointing at a hub endpoint that works, against code that never read it. The wiring sat commented out in run.py under a note saying "2plot.dev does not serve /api/network/bulletin yet". The hub started serving it; the comment did not change.

Nothing failed. configure_bulletin is opt-in, so an unwired app makes no request at all and the viewer header renders perfectly well on the package's built-in tips and an "No announcements." empty state. The only symptom was an announcement that never appeared — which nobody goes looking for.

Now lib/bulletin.py, shaped like lib/proxy.py and lib/access.py: a configure() that returns whether it wired, and a boot line that says which of the two states the process is in. No commented-out code to go stale, and tests/test_bulletin.py::test_run_py_wires_it_rather_than_leaving_it_commented_out fails the moment someone comments it out again — commented wiring cannot define the name it asserts on.

Two details worth keeping:

  • app_id comes from SATELLITE_APP_KEY, reused from lib.satellite_reporter.app_key() rather than hard-coded. The hub scopes announcements by ?app= and uses it to see which satellites actually render the bulletin, so a fork left announcing itself as boilerplate would receive this template's news and be miscounted. One notion of "which satellite am I", not two that can disagree.
  • The TTL is floored at 60s. It is configurable via NETWORK_BULLETIN_TTL_S, and a small value would refetch on nearly every llms.txt view; junk falls back to the default rather than raising at boot.

Verified end to end against the live hub: the rendered header carries the hub's own tip wording ("Append /llms.txt to any URL") rather than the package's default ("Append /llms.txt to any page URL"), and the current announcement.

One thing that cost time and is worth recording: on macOS the package's bulletin client fails with CERTIFICATE_VERIFY_FAILED, because it uses a bare urlopen with no CA bundle and the system Python has no OS trust-store integration. That is a local-development artifact only — Linux containers have a working store — but locally it looks exactly like a broken fetch. Run with SSL_CERT_FILE=$(python -c "import certifi;print(certifi.where())") to tell the two apart. scripts/smoke_live.py and scripts/audit_links.py already carry their own certifi context for the same reason.

Changed — one identifier for this app on every hub surface

AD_APP_ID now defaults to boilerplate, not dash-documentation-boilerplate. Four modules present an identity to the hub — lib/ad_client.py, lib/satellite_reporter.py, lib/hub_client.py and lib/bulletin.py — each with its own fallback, and the ad client was the odd one out. The visible cost was a column on /admin/ad-board that did not line up with /traffic; the invisible one is that hub_client.app_id() falls back to AD_APP_ID when SATELLITE_APP_KEY is unset, so a deployment that set the long name for ads alone was silently presenting it as its hub identity too.

lib/satellite_reporter.app_key() still refuses to chain to AD_APP_ID. The two agreeing here is a convenience, not a contract — leaflet.2plot.dev runs AD_APP_ID=dash-leaflet2 against directory key leaflet, and setting one for ads must never re-key a satellite's analytics series.

render.yaml now sets AD_APP_ID explicitly rather than leaning on the code default, so the deployed value is visible in the blueprint.

This splits ad history. The ad server keys impressions and clicks by app, so anything already logged under dash-documentation-boilerplate stays there — worth a look at /admin/ad-board on 2plot.dev before assuming the numbers reset.

Added — .env.example

The repo had none, so every configurable was discoverable only by reading lib/. Each block states what turns ON when set and what the app does when it is not — because almost every one of these fails silently rather than loudly: no APP_BASE_URL deindexes a fork, no CROSS_APP_WEBHOOK_SECRET means the hub simply never charts this app, no NETWORK_BULLETIN_URL renders a header that looks complete.

Not gitignored (the pattern is .env, exactly), and .dockerignore already whitelists it against the .env* exclusion added in 1.2.2.

render.yaml gains NETWORK_BULLETIN_URL so the deployment documents itself rather than depending on someone remembering to set it in the dashboard.

[1.2.3] - 2026-08-01

The social card, finished. 1.2.2 closed three of four defects and left this one open because the artwork did not exist. It does now.

Added — scripts/make_social_card.py

Renders the 1200×630 card: the artwork composited onto a frame carrying the brand, tagline and domain, using the manifest's own background_color and theme_color so the card, the browser chrome and the install splash cannot disagree. Output lands in build/social-cards/<domain>.png, which is gitignored.

A TEMPLATE FILE, and that is the point — pass --brand/--tagline/--domain and every satellite is framed identically, instead of each card being made by hand once and drifting. Three details that are not incidental: the artwork's alpha bounding box is cropped before fitting (assets/ddb.png carries ~66px of transparent margin that would otherwise be centred as if it were image); a brand too long for two lines shrinks once rather than colliding with the domain strip; and fonts resolve from a candidate list (macOS, then Debian/Ubuntu) rather than being bundled, because shipping a licensed TTF in a template every satellite forks is a question best not answered.

Pillow stays out of requirements.txt. Nothing at runtime renders images, and a docs site should not carry an image library into production for a script run by hand every few months.

1200×630 = 1.91:1, the Open Graph documented ideal, which also degrades cleanly into Twitter's 2:1 summary_large_image slot. Deliberately not leaflet's 1280×515 (2.49:1), which is wider than both and gets cropped on each — and what sits at that URL today is the 2plot wordmark rather than a per-site card at all. This is the shape for the network to converge on.

Changed — og:image moved to the CDN

was:  https://boilerplate.2plot.dev/assets/ddb.png   784×741  (1.06:1)
now:  https://cdn.2plot.ai/github_assets/boilerplate.2plot.dev.png  1200×630

The old image's declared dimensions were honest, so nothing was broken — it was simply near-square, and summary_large_image letterboxed it into a wide slot with bars either side.

Moving it off the app is the network rule and it is about cold starts, not tidiness: a card the app serves is fetched by the scraper at unfurl time, and on a cold free-tier container that request lands mid-wake and times out. The preview renders blank once, and the platform caches the miss — so the first person to share the link poisons it for everyone. The CDN has no cold start.

og:image:secure_url and og:image:type join the auxiliaries in templates/index.html, matching what leaflet carries. Both are tags Dash does not emit, which is the only reason they belong in the template.

Added — the live card check that no offline test can make

The card's dimensions are now declared in three places: lib/constants.py, templates/index.html, and the CDN object itself. test_social_card.py pins the first two against each other, but nothing offline can look at the third — so replacing the uploaded file with a differently-shaped one would leave every test green while the platform reserves the wrong box and crops into it.

scripts/smoke_live.py now fetches the real file after every deploy and reads its actual pixel dimensions out of the PNG's IHDR chunk, checking them against the declared tags, plus the ratio, plus that og:image is neither empty nor app-served. Two tests prove the check fires rather than merely existing: test_a_reshaped_card_on_the_cdn_fails_the_deploy and test_an_empty_og_image_fails_the_deploy.

That second case is not hypothetical — it is 2plot.dev's live state today, and the reason kickoff/ now holds a handoff for it.

fetch() in that script changed from errors="replace" to errors="surrogateescape" to make this possible. "replace" substitutes U+FFFD for every invalid byte and is one-way, so the PNG header was gone before it could be read; surrogateescape round-trips exactly and behaves identically for text.

Changed — the two peer tests narrowed to peers

test_smoke_script_rejects_a_peer_serving_its_spa_shell and test_a_dead_peer_is_reported_but_does_not_fail_the_deploy stubbed every off-host URL, which now included the CDN-hosted card and failed the (correctly fatal) card checks. The card is off-host but it is this deployment's own responsibility, not a peer's — the distinction 1.2.2 drew between "this host is fatal, somebody else's host is a warning" holds, the stubs just needed to respect it.

Changed — build/ and kickoff/ are gitignored

build/ because the card is published to the CDN and never committed or served. kickoff/ because handoff notes start a session in another repo: a task list for 2plot.dev has no business in the template's checkout, and every satellite forking this repo would inherit a to-do that was never theirs.

[1.2.2] - 2026-08-01

Finishing 1.2.1, and the three things it exposed. 1.2.1 shipped the right template and half the change. Everything below was measured against the live site rather than a local boot, because the local/deployed gap is precisely what hid the first defect for a day.

Fixed — the 1.2.1 files were never committed

assets/favicon/ (the whole icon set plus site.webmanifest) and tests/test_social_card.py were sitting UNTRACKED. The committed template pointed at /assets/favicon/…, the deploy builds from git, so production 404'd the manifest, the apple-touch-icon and every PNG icon link — the entire installable-app surface — while every local boot looked perfect because the files were on disk. Nothing in the app reported it; git status was the only place it appeared. Measured on the live site:

/assets/favicon/site.webmanifest      404
/assets/favicon/apple-touch-icon.png  404
/assets/favicon/favicon-32x32.png     404

The guard test was untracked too, so the one thing that would have caught this had never run in CI either. Both are now tracked, and test_every_asset_the_template_references_resolves widens the check from "the manifest icons resolve" to "every /assets/… the template references resolves" — because the failure was never about icons, it was about a template referencing a file the repository does not have. A checkout is what CI tests, so it fails there the moment something is not committed.

The manifest's contents needed no change; they were already correct.

Fixed — the fork source's brand on every share card

PAGE_TITLE_PREFIX still read "Dash Pip Components | ", inherited from the upstream this template was forked from and never changed. That is not only a browser-tab string: Dash passes each page's title straight into og:title and twitter:title (dash/_pages.py:_page_meta_tags), so every unfurl of boilerplate.2plot.dev advertised a different site, while <title>, og:site_name and the /llms.txt H1 all correctly said this one.

Now f"{SITE_SHORT_NAME} | ", matching the network convention the other satellites already use (dash-leaflet2 | , Dash Email | ) and derived from the brand rather than retyped, so the two cannot drift. tests/test_site_identity.py pins the prefix, the derivation, the rendered og:title/twitter:title, and sweeps the identity surfaces for any surviving mention of the old brand.

Nobody sees their own share cards, which is the whole reason this needed a test rather than a look at the page.

Fixed — twitter:url advertised http:// (lib/proxy.py)

Dash builds that tag from request.url, and on Flask request.url comes from wsgi.url_scheme. Requests arrive over Cloudflare → Render → gunicorn and the last hop is plaintext, so production told every social scraper http://boilerplate.2plot.dev/. og:url looked fine throughout because the template hard-codes it.

gunicorn does try to fix this — it rewrites the scheme from X-Forwarded-Proto, but only when the immediate peer is in forwarded_allow_ips, which defaults to 127.0.0.1. Reading the header ourselves one layer above gunicorn sidesteps the question entirely: HTTP_X_FORWARDED_PROTO is in the environ either way.

Notes on the implementation, all of them load-bearing:

  • Only the scheme is taken. Host is not rewritten from X-Forwarded-Host; BASE_URL is already this project's single source of truth for the public origin, and a second header-derived notion of "what host am I" is how a fork ends up serving two.
  • The FIRST entry of the header wins. Proxies append, as with X-Forwarded-For, so the last entry is the hop nearest the app — the plaintext one being seen past. Reading from the wrong end reinstates the bug and still passes a single-proxy test, so there is a test for it.
  • TRUST_PROXY_HEADERS=0 turns it off. This trusts a header from whoever connected, which is correct behind Render (it overwrites the header on every inbound request) and wrong for an app exposed directly, where a client could forge it.
  • The server object is wrapped, never reboundapp.server stays the Flask/FastAPI/Quart instance that gunicorn imports as run:server and that run.py hangs before_request off. All three backends are handled.

The sibling leaflet.2plot.dev already serves https in the same tag from an identical Cloudflare/Render/gunicorn stack with no proxy configuration of its own; the difference we could observe is that it deploys as a Docker service rather than a native one, which would plausibly put the proxy on loopback and satisfy gunicorn's default. That is inference — Render's internal topology is not visible to us — and the fix deliberately does not depend on which explanation is true.

Added — client-side URL sync on SPA navigation

Ported from leaflet.2plot.dev and adapted: that site hard-codes a static canonical and this one does not (dash-improve-my-llms injects a per-page one), so this version only ever corrects tags that exist and never creates one.

Three tags go stale after the first client-side route change, each for a different reason: og:url is static in the template, twitter:url is server-rendered from the entry request, and the injected canonical is right on arrival and wrong thereafter. Dash routes through history.pushState, which fires no event, so the tags advertise the landing URL for the rest of the session. The origin is read from the existing og:url tag rather than hard-coded a second time.

This helps Google, which runs JS. It cannot help social scrapers, which do not — which is why the scheme half had to be fixed server-side.

Changed — test_exactly_one_canonical_tag_for_browsers counts elements

It counted the substring rel="canonical", and the new sync script's selector (link[rel="canonical"]) is not a canonical tag. Same lesson as the dv-banner chrome check it sits beside: match the markup, not the words, so a file may legitimately discuss what it is being checked for.

Still open — the card image is not on the CDN

og:image remains /assets/ddb.png, 784×741, served by the app. The declared dimensions match the file honestly, so nothing is broken, but it misses two network rules: cards belong on cdn.2plot.ai so a cold free-tier container cannot blank a preview, and summary_large_image wants roughly 1.91:1 (leaflet's is 1280×515). https://cdn.2plot.ai/github_assets/boilerplate.2plot.dev.png does not exist yet, and pointing og:image at a 404 is strictly worse than the present state, so this waits on the asset. When it lands, the change is OG_IMAGE_URL plus the width/height constants, plus the og:image:secure_url and og:image:type tags leaflet carries.

[1.2.1] - 2026-07-31

The social card and the installable app — the two surfaces that live entirely outside the app, and so fail where nobody is looking. Found while rolling the standard onto leaflet.2plot.dev, which inherited the same shapes from this template. Satellites copy tests/test_social_card.py verbatim.

Fixed

  • Two og:image tags per page, and the wrong one won. templates/index.html declared og:image / twitter:image statically while Dash also emits both per page. With no image_url= passed, Dash inferred an image from the assets folder, found assets/logo.svg, and emitted it alongside the static tag. Every major scraper rejects SVG, and the inferred tag came last — so the card described so carefully in the template lost to an image nothing can render. lib/constants.OG_IMAGE_URL is now passed to register_page, and the template keeps only the auxiliaries Dash omits.
  • The same duplication across nine other tagsdescription, og:type, og:title, og:description, twitter:card, twitter:url, twitter:title, twitter:description, twitter:image were all declared statically and emitted by Dash. The static copies described the site where Dash's describe the page, so the duplicate was both redundant and the less accurate of the two. test_no_meta_tag_dash_emits_is_also_declared_statically pins the rule.
  • The home page published an empty description. pages/home.py never passed one, so Dash emitted description, og:description and twitter:description as content="" on the most-linked page on the site.
  • The web app manifest was inert, and named the wrong product. Its link and the apple-touch-icon were commented out behind a note saying the files were missing — a note that outlived their arrival in assets/favicon/ — and the commented hrefs pointed at /assets/, one level above where they live. The manifest itself still read "Dash Email — Email components for Plotly Dash", copied in from another repo; that string is what an installed app would have shown on the home screen. Fixed, linked, and its theme_color reconciled with the theme-color meta tag.

Added

  • tests/test_social_card.py — a template file. Asserts the image is declared exactly once, is absolute, is not an SVG and resolves; that the manifest is linked, served, correctly named and has resolving icons; and that templates/index.html is still wired in, since it looks removable ( dash-improve-my-llms appears to cover OpenGraph) and is not — its injection runs only on the prerender path, which social scrapers do not take.
  • lib/constants.OG_IMAGE_URL / _WIDTH / _HEIGHT / _ALT — the per-site values a fork changes.

[1.2.0] - 2026-07-31

The 2plot network standard, landed on the template.

2plot.ai (the network root) and 2plot.dev (the section hub) shipped this first; satellites are next, and this repo is the one they fork. So the point of this release is not that boilerplate.2plot.dev complies — it is that the files a satellite copies verbatim now carry the standard with them. The new Network Standard page is the per-site checklist.

The three obligations below share a shape, and it is worth naming: every failure they prevent is silent. Nothing errors, no dashboard turns red, and the damage accumulates for months. That is why each one is now pinned by a test rather than by a convention.

Added — explicit site identity (lib/constants.SITE_BRAND)

One constant, "Dash Documentation Boilerplate — the 2plot network's template", now reaches every surface that states what this site is: Dash(title=), register_page_metadata(path="/", name=…), the first line of pages/home.md, and templates/index.html (og:site_name, og:title, twitter:title, the schema.org SoftwareApplication.name, the <noscript> heading).

What this fixes is not cosmetic. dash-improve-my-llms resolves the /llms.txt H1 and the llms viewer's brand chip through resolve_site_title(home_page_name, app.title), and given nothing useful it publishes what it finds. On this host that was the Dash() constructor's default title: every agent that fetched boilerplate.2plot.dev/llms.txt cold was told the site is called "Dash". The page rendered perfectly the whole time. 2.3.4 fixed half of it — generic candidates (Home, Index, Dash) are now skipped rather than served — but a package cannot invent a name; the other half is stating one.

Naming rules, from the standard: the brand says what the site is; the package name (dash-documentation-boilerplate) belongs in the description; "Pip Install Python" is the byline and never the site name.

tests/test_site_identity.py pins all of it, including the direction that is easy to lose — that SITE_BRAND is not itself one of the generic values the package skips.

Added — the internal-traffic contract, both halves

The point of truth is 2plot.ai's satellite-analytics document, "Internal traffic": any request whose User-Agent contains 2plot-internal is network machinery talking to itself and is counted nowhere.

Inbound. lib/analytics_tracker.track_visit drops token-carrying requests at write time, before detect_device_type. The ordering is the whole point: a health sweep and a CI battery both look like bots, so classified first they land in bot_hits and get reported to the hub as crawler interest in these docs. /healthz and /health stopped being stored at all — lib/traffic_rollup already filtered them on the way out, but a row that exists and must be discounted is still a row somebody has to know about.

Outbound — the half that was missing here. Every call this host makes to another network host now sends INTERNAL_UA:

  • lib/ad_client.py2plot.dev, once per docs page view;
  • lib/satellite_reporter.py2plot.ai, hourly;
  • lib/hub_client.py2plot.dev, per agent-key verify and tier fetch;
  • scripts/network_smoke.py, scripts/smoke_live.py, scripts/audit_links.py.

The ad client is the one that mattered. All of these were arriving as python-requests/2.x, which matches the hub's own bot patterns — so this satellite's readers were inflating 2plot.dev's bot_hits, once per page view, and had been for as long as the ad slot has existed. The battery scripts keep their Googlebot and Chrome tokens and append the internal one: the target still exercises exactly the path under test, it just knows the caller is machinery. The click beacon is the deliberate exception — a browser cannot set a User-Agent, and a click is a real person.

tests/test_internal_traffic.py proves the exclusion reaches the numbers the hub actually charts (human_hits / bot_hits in daily_rollup), proves the positive case still counts (a rule that drops everything would satisfy the negative assertions), and asserts the outbound header on all three clients and all three scripts.

Added — scripts/network_smoke.py, run in three seats

The same named checks against the CI container, against production after a deploy, and in-process from tests/test_network_smoke.py, so a failure reads identically wherever it happens. It proves identity (the /llms.txt H1 is the brand, verbatim), the deployed artifact (the robots.txt crawler split, which is the only fingerprint visible from outside — pip metadata is not), that no owner-only surface leaks, that a crawler gets prose and not the JavaScript stub, and that agents and browsers get different content types under a Vary: Accept.

The in-process seat is not redundant: a script that only ever runs in CI and after a deploy is exactly the code that rots, where a typo turns a check into a silent pass. That test also breaks a check on purpose and requires the battery to report it.

Changed — CI on the network baseline

.github/workflows/ci.yml is now a template file in its own right: least-privilege permissions: contents: read, timeout-minutes on every job (the default is six hours, which is how one hung curl burns a day of runner minutes), docker/setup-buildx-action with a type=gha cache, and version fingerprints asserted inside the built image rather than in the runner. The container is booted and probed by the battery before anything is allowed to merge. cd.yml runs the battery against the live host before smoke_live.py.

tests/conftest.py now boots the app secretless, the way CI's container does: every CLERK_*, CROSS_APP_WEBHOOK_SECRET and SESSION_SECRET is pinned to "" before run.py is imported, because load_dotenv() runs during that import and a developer's local .env would otherwise flip the app into a configured posture and quietly invalidate every fail-closed assertion in tests/test_access.py. The analytics ledger moves to a temp dir in the same block — the suite had been appending its own hits to the repo's checked-out visitor_analytics.json.

Added .github/dependabot.yml with a dash-network group (a package release lands as one reviewable PR per repo, not five) and an advisory pip-audit job.

Changed — dependency floors

  • dash-improve-my-llms >= 2.3.4 (from 2.3.2). The network standard; run.py's startup floor and CI's in-image fingerprint both assert it. There is no vendored copy of this package anywhere in the repo — the stale comments in Dockerfile, render.yaml and README.md that still described one are gone. vendor/ holds dash_clerk_auth alone.
  • gunicorn >= 23.0.0 (from 21.2.0). 21.x carried two HTTP request-smuggling CVEs (CVE-2024-6827, CVE-2024-1135), both fixed in 23.0. markdown2dash 0.1.2 declares gunicorn>=21.2.0,<22.0.0 — a markdown parser pinning a WSGI server — which pip cannot reconcile with that floor, so markdown2dash is installed with --no-deps and its real dependencies (docutils, jsonpath, mistune) are listed in requirements.txt instead. Every install path does the same two commands: requirements.txt, scripts/dev.sh, the Dockerfile, render.yaml's buildCommand, CI, and the README. CI's in-image assert is what keeps the dodge honest.

Added — .dockerignore

Found by booting the image locally as part of verifying this release: the Dockerfile ends in COPY . ., so a developer's .env was being baked into the production image. The container died at boot with Could not import dash.backends._fastapi — the local file said DASH_BACKEND=fastapi and the image has no FastAPI extra. It never appeared in CI, where the checkout has no .env, which is precisely what made it worth a file rather than a lesson: the same COPY would carry real Clerk keys and the webhook secret into an image layer on any machine that has them. The ledger, session store, virtualenv and node_modules are excluded too. docs/**/*.md deliberately is not — those files are the app.

Note on versioning

1.1.0 was declared in README.md and lib/constants.APP_VERSION but never cut here; everything previously sitting under [Unreleased] ships as part of 1.2.0. templates/index.html's softwareVersion and APP_VERSION now agree, which tests/test_config.py asserts.


Previously unreleased, now shipping as part of 1.2.0 — three threads of work: the CI/CD system, network analytics reporting, and the upgrade to dash-improve-my-llms 2.2.0.

2.1.0 was assigned during that package's development and never published, so there is no 2.1.0 anywhere and 2.0.0 upgrades straight to 2.2.0. Work described here as "2.1-era" in earlier drafts shipped as part of 2.2.0.

Changed — dash-improve-my-llms from PyPI (2.3.3); vendored copy removed

The four-host verification gate passed, dash-improve-my-llms published, and this repo switched from the vendored sdist to the PyPI pin (dash-improve-my-llms[flask]>=2.3.2) — the Phase-5 step the vendor block always anticipated. vendor/dash_improve_my_llms-*.tar.gz is gone; CI's ASGI legs and the Dockerfile install from PyPI too. vendor/ still carries dash_clerk_auth (not on PyPI, deliberately outside requirements.txt).

The floor resolves to 2.3.3, which recategorises the Anthropic crawlers: ClaudeBot — the actual training crawler — moves to Disallow, while the user-triggered and search fetchers Claude-User / Claude-SearchBot are allowed, matching the intent the OAI-SearchBot fix established for OpenAI. It also strips unexpanded directive lines from resolved prose. The artifact fingerprint in tests/test_llms_routes.py and scripts/smoke_live.py now asserts the full crawler split, so a host running a stale build fails its post-deploy battery by name.

Verifying that fingerprint exposed a real misconfiguration: run.py set block_ai_training=False, so the training bucket was never emitted and every training crawler was silently allowed — the opposite of the "blocks AI training, allows AI search" policy this project documents, and it would have made 2.3.3's ClaudeBot recategorisation invisible on this host. Now block_ai_training=True, matching the documented policy and the rest of the network.

Changed — production rollout: re-vendor 2.3.2 / 0.9.1, live hub contract

Deployment prep for boilerplate.2plot.dev (rollout step 4; the hub's auth endpoints are now live in production).

  • dash-improve-my-llms 2.3.0 → 2.3.2 (vendored). The vendored 2.3.0 was a pre-fix build whose robots.txt disallowed OAI-SearchBot — ChatGPT search's crawler, exactly the audience these surfaces exist for. 2.3.2 allows it. User-agent: OAI-SearchBotAllow: / in a live host's /robots.txt is the fingerprint that it runs the fixed artifact (pip metadata is invisible from outside); test_robots_artifact_fingerprint now asserts it locally so a vendored regression fails CI, not production. 2.3.1 was assigned during development and never published.
  • dash-clerk-auth 0.9.0 → 0.9.1 (vendored, built from the Dash-Clerk-Auth-Hook working tree). 0.9.0 ships a bug hitting every Clerk satellite forked from this template: clerk-js v5 auto-instantiates from the script tag's data-* attributes and reads the instance domain, so on a satellite the user button never mounts (dead avatar) while server-side session verification keeps working. 0.9.1 emits data-clerk-domain="<satellite_domain>" on the tag when is_satellite=True. This app runs no Clerk by design — the bump is for the template's sake. lib/auth.py's fixup #1 guards on the attribute's absence, so it degrades to a no-op under 0.9.1 and stays for forks still on 0.9.0.
  • lib/hub_client.py aligned with the hub's real contract. Two functions predated the hub going live. current_key() now sends {"token": <Clerk session token>, "app": ...} — the hub 401s any caller-asserted identity (user_id in the payload is the forgery path) and verifies the token against Clerk's JWKS, minting at scope=auth, never admin. Call it on copy-button click, never on page render; None degrades to copying the plain URL. hub_tiers() is no longer a stub: signed POST /api/page-tiers {"app": ...}{"tiers": {path: tier}, "ttl": s}, cached for the returned TTL with failures cached 60s — so a down hub costs one timeout per window, not one per request, and resolves to the local tier, which the ceiling rule guarantees can never loosen anything. verify() already matched the hub and is untouched.

Added — AI/LLM surfaces (dash-improve-my-llms 2.2.0)

  • lib/network_directory.py — the peer/affiliated/external directory, defined once here and copied verbatim into every satellite. Publishes <link rel="related"> tags, a ## Network section in /llms.txt, and followed links in the prerendered body, so an agent landing on one satellite can enumerate the rest. Filters the app's own URL out of peers.
  • Wordmark"2" + morse(plot) + "ai", drawn as columns of dots and dashes in the header of the rendered llms.txt view. No period glyph: the morse block already separates the halves, and a literal . beside it reads as punctuation dropped into a graphic. The renderer turns a suffix ending in i into an upward flourish, so "ai" draws as a plus that mark, with the real domain in label for screen readers and the SVG <title>. It lives in the shared module rather than per-app, which is what keeps one mark across the network instead of twelve near-identical ones.
  • Page llms.txt documents are no longer dead ends. Each now opens with the site index, the network index one level up the hub chain (2plot.dev, correct for a *.2plot.dev subdomain), and the sitemap. These documents are usually read in isolation — pasted into a chat, handed to an agent — and an agent fetches a URL rather than crawling from one, so previously its exploration simply stopped there.
  • The same URL content-negotiates. Agents, crawlers and curl get the Markdown byte for byte; browsers get it rendered behind a header carrying the network identity. ?raw=1 and ?format=html override, both variants send Vary: Accept, and the rendered view is noindex so it never competes with the page it documents. Verified identical on Flask, FastAPI and Quart.
  • docs/networks/networks.md — the guide for satellite authors: the three tiers, why per-host SEO can't express any of this, the wordmark and bulletin conventions, the one-URL-two-audiences contract, and the verification commands.
  • Network bulletin left deliberately unwired. configure_bulletin() sits commented next to add_llms_routes with a pointer to the contract. 2plot.dev does not serve /api/network/bulletin yet, and pointing at a dead endpoint gains nothing: the client degrades silently and the header renders fine without it — the "Tips for getting started" and "What's new" panels use the package's built-in defaults, which a bulletin only overrides.

Added — Clerk authentication and llms.txt access control

Opt-in, and off in a default clone. This is the template every *.2plot.dev subdomain is forked from, so the goal was a pattern good enough to copy rather than a one-off. Requires dash-improve-my-llms 2.3.0 (configure_access, configure_viewer_identity) and the vendored dash-clerk-auth 0.9.0, which is deliberately not on the active requirements line — a default install should not pull in an auth stack the site does not use.

  • lib/auth.py — adapted from 2plot_leaflet/lib/auth.py, the implementation already sharing authenticated state across 2plot.ai2plot.devleaflet.2plot.dev in production. Keeps both satellite fixups for dash-clerk-auth 0.9.0 (clerk-js reads domain as a constructor option from data-clerk-domain, and a satellite must redirectToSignIn() rather than open a modal that 403s), the pk_live auto-enable so production cannot silently boot in primary mode, DISABLE_CLERK=1, and call-time env reads. Changed for the template: the satellite domain derives from APP_BASE_URL, which every deployment must set anyway — one variable rather than two, and one fewer way to announce another site's domain to Clerk.
  • lib/page_tiers.pypublic < auth < admin < hidden, declared in markdown frontmatter (tier: admin) because this template is already frontmatter-driven and marking one page should not require a control board. Two rules: everything except hidden falls open when Clerk is unavailable (documentation must not brick over a missing credential), and effective_tier = more_restrictive(local, hub) so a satellite may restrict further but never loosen.
  • lib/hub_client.py — the client for the hub's /api/agent-key/current and /api/agent-key/verify. Authenticates the caller with the network's existing CROSS_APP_WEBHOOK_SECRET HMAC scheme, the one lib/satellite_reporter already uses: it authenticates who is asking and derives nothing, which is what keeps "satellites hold no key material" true while still keeping the verify endpoint from being an open key-guessing oracle. Verdicts cached on a SHA-256 fingerprint of the key rather than the key, because that cache is process memory a debugger or error reporter can dump. allow cached 900s, deny 60s — a brief hub outage must not gate readers who were fine a minute ago, while a revoked key should stop working promptly.
  • lib/access.py — the policy, and its ordering is the design: tier → local Clerk session → hub, only for ?key=. A signed-in visitor resolves entirely on this host, so the hub being down gates nothing for them; only the agent path, which arrives with no cookie, needs the hub at all. Reversing it would couple every satellite's availability to one host for no benefit. Kept out of run.py so satellites inherit one file.
  • docs/authentication/ — three layers, so a reader stops at the one they need: the default (nothing to do), a standalone site with its own Clerk, and joining or running a network. Names the two traps: the Clerk token's iat is the token's age, not the sign-in's, so wiring it renders a clock that resets every minute; and identity must never travel in the bulletin, which is TTL-cached and shared across every satellite.
  • handoff/ — kickoff prompts for the two repos this unblocks: an addendum pairing with the pip-docs+ hub brief, carrying the request shapes and cache TTLs the client already sends, and a per-subdomain port guide.
  • tests/test_access.py — 17 tests against a fake hub. The two that justify the design: signed-in browser with the hub unreachable still resolves to allow, and a valid key with the hub down degrades to gated rather than 500 or prose. One asserts the ordering rather than the outcome — a signed-in reader must trigger zero hub calls, since "allowed" could otherwise come from a hub that happened to agree.

Inert until a tier says otherwise. With the wiring in place, no Clerk keys, and every page public, all 43 surfaces are byte-identical to the build before any of it existed — measured, with a control run to strip out the per-request ids Dash puts in page HTML.

Changed — dash-improve-my-llms 2.2.0 → 2.3.0

Vendored, as before; 2.3.0 is additive and opt-in. Verified as a no-op on the surfaces that matter: every Markdown document, the root index, sitemap.xml, robots.txt and the crawler HTML are byte-identical. The HTML viewer variants grow by 192 bytes each — three CSS rules for the identity block that ship whether or not identity is configured. Behaviourally a no-op; not literally byte-identical everywhere, which is worth stating precisely since this baseline is what a later regression gets attributed to.

Added — CI/CD and tests

  • .github/workflows/ci.yml — flake8 (blocking), then the full test suite across a matrix of Python version × backend × Dash version: Flask, FastAPI and Quart on Python 3.12, Python 3.11 and 3.13 on Flask, and the bottom of the ~=4.4.1 range pinned explicitly on Flask and FastAPI so a 4.4.0-only regression cannot hide behind pip resolving to 4.4.1. Asserts the resolved Dash and dash-improve-my-llms versions before running anything, boots the app under gunicorn (a page can render under a test client and still fail under a real WSGI worker), and builds and probes the Docker image.

  • .github/workflows/cd.yml — runs CI, POSTs the RENDER_DEPLOY_HOOK_URL secret, waits for the new instance to be sustainably healthy (Render swaps instances rather than restarting in place, so a single 200 from /healthz proves nothing), then verifies the live site. Skips the deploy step when the secret is absent instead of failing, so a fork isn't red on day one.

  • tests/ — a pytest suite that boots run.py itself rather than a test app. conftest.py normalises the three backends' test clients behind one synchronous .get(), including driving Quart's async client from a fixture-owned event loop. Covers page registration and reachability, stub bodies, rendered prose, canonical tags, sitemap/robots/llms.txt, content negotiation in both directions, the navigation block, the banner and its panels, the network directory and wordmark, docs frontmatter and directive targets, heading anchors, and the BASE_URL guard.

  • scripts/smoke_live.py — post-deploy checks against a live satellite, standard library only. Covers the failures that are silent in production: a canonical on the wrong host, a page serving the JavaScript stub, viewer chrome leaking into an agent's Markdown, a missing Vary: Accept, a missing network directory, and dead peer llms.txt links. Run in CD and by hand (python scripts/smoke_live.py https://emojimart.2plot.dev), and itself tested against the in-process app so a typo can't turn every live check into a silent pass.

  • scripts/dev.sh — starts the development server with this project's interpreter, resolved from the script's own location rather than from an IDE setting or PATH.

  • scripts/audit_links.py — walks every page's llms.txt, extracts every link, resolves internal paths in-process and checks the rest over the network. A dead link in an llms.txt is worse than one on a page: the agent holding that document has no navigation to fall back on and no way to tell a typo from a host that is down.

    Classified rather than lumped together, because the classes want different responses: internal is a real defect, self-host is correct once deployed, network is a peer awaiting the rollout, unpushed is a file that exists locally and 404s only until the branch is pushed, and external is someone else's problem to route around. Code spans and fenced blocks are skipped — a URL inside backticks renders as <code>, not <a> — and a transport failure is retried once, because an audit that cries wolf gets ignored.

  • LICENSE — the MIT text the README badge, pages/home.md and the Schema.org block have all claimed since 0.1.0 without the file ever existing.

  • render.yaml — Render Blueprint for boilerplate.2plot.dev: gunicorn, /healthz health check, custom domain, and a persistent disk for the analytics ledger (on an ephemeral filesystem a mid-day deploy wipes it and the next hourly report overwrites the day's real total).

  • .flake8, pytest.ini.

Added — Network analytics reporting to 2plot.ai

  • lib/satellite_reporter.py — hourly signed rollup POSTed to https://2plot.ai/api/satellite/traffic, so a deployed docs site shows up on the hub's owner-only /traffic dashboard. HMAC-SHA256 over "{timestamp}." + body with CROSS_APP_WEBHOOK_SECRET, matching the network's existing webhook scheme. Off by default: no secret, no reporting. Re-posts yesterday during the first hours of a new day so the final hits of a day aren't left out, and uses a lease file so only one web worker reports per interval instead of every worker racing. python -m lib.satellite_reporter --dry-run prints the payload without sending it.
  • lib/traffic_rollup.py — derives the reported numbers (human_hits, bot_hits, visitors, sessions, median_session_s, top pages, countries) using the hub's own definitions, so this app's figures are comparable with every other app on the chart. Infrastructure paths (/healthz, /llms.txt, /robots.txt, /sitemap.xml, assets, Dash internals) are excluded from the report but stay in the local ledger.
  • lib/health.py/healthz on Flask and Quart, matching the endpoint the FastAPI build already declared. The hub's hourly sweep probes it for up/down + latency, which previously only worked on one of the three backends.
  • Quart now tracks visitors too; previously only Flask and FastAPI did.

Changed — dependencies

  • dash-improve-my-llms 2.0.0 → 2.2.0, installed from vendor/ until it is published to PyPI. App 1 of 4 in a staged rollout, first because every satellite documentation site is forked from this repo — a convention set here propagates, and so does a mistake.

    Page metadata now merges instead of assigning, so no later bookkeeping call can erase a page's prose; the prerender reaches every visitor rather than only recognised crawlers; and the Markdown renderer emits real anchors, tables, code fences and rules. Measured on this app: link counts in crawler bodies went from 3 per page to 3–11, code fences from 0 to 5–29 per page, and horizontal rules stopped rendering as literal --- text. No page serves the crawler stub, before or after — this repo was never affected by the prose-erasure bug, having no bridge loop over dash.page_registry.

  • Dash pinned to ~=4.4.1 (was >=4.4.0). Verified matrix, from real apps on each backend with the failure reproduced on stock Dash:

    Dash Flask FastAPI Quart
    4.1.0 ok n/a — no pluggable backends n/a
    4.2.0 ok ok ok
    4.3.0 ok broken — every non-root page 500s ok
    4.4.0 ok ok ok
    4.4.1 ok ok ok

    4.3.0 added an early-return path guard to the ASGI middleware that returns before set_current_request, while the page catch-all still calls get_current_request() — so it raises RuntimeError: No active request in context. The catch-all is byte-identical between 4.2.0 and 4.3.0; only the middleware changed. 4.4.0 set the context inside the catch-all as well, so a future middleware guard cannot reintroduce it: 4.4.x is structurally safer, not merely currently-passing.

    ~=4.4.1 lets patch releases flow without twenty pull requests while blocking 4.5.0, so a minor bump goes through the matrix deliberately. Pinned for the most constrained backend network-wide, including Flask-only appsDASH_BACKEND is an env var and this is a shared template, so a Flask deployment becomes a FastAPI deployment with one env change and no code change.

  • Dependency floors are enforced at startup, not advised. A version below the floor stops the boot, names what would degrade, and prints sys.executable alongside the expected interpreter. ALLOW_STALE_DEPS=1 opts out for anyone deliberately testing an older release. The Dash floor is fatal only on FastAPI, where 4.3.0 is an outage rather than a degradation. See Fixed — environment and tooling for why this is a hard failure.

  • network_directory.apply() gates the wordmark argument on the installed signature. During a staged rollout this module reaches satellites before the new package does, and Python raises TypeError on an unknown keyword — so passing it unconditionally would turn an older satellite's boot into a crash rather than a missing graphic. Same technique run.py uses for Dash's enable_mcp.

Changed — hosts, branding and repo hygiene

  • BASE_URL moved to lib/constants.py and reads APP_BASE_URL from the environment, defaulting to https://boilerplate.2plot.dev. require_owned_base_url() refuses to boot in production when APP_BASE_URL is unset or points at a platform hostname (*.onrender.com and friends). This is the template's highest-consequence footgun: a fork that leaves the default in place emits the boilerplate's canonical URL on every one of its pages, which asks Google to deindex it, and nothing about the app looks broken while it happens.
  • YouTube links now point at @2plotai; plotly.pro is replaced by 2plot.ai throughout, and the deployment host by boilerplate.2plot.dev. A test fails the build if a live link to plotly.pro reappears.
  • .claude/ is untracked and gitignored. Local session workspace; noise in a template other people fork.
  • Dockerfile copies vendor/ before the pip layer (the build fails otherwise while the package installs from an sdist), declares a HEALTHCHECK against /healthz, and no longer leaves apt lists in the image.

Fixed — SEO and template

  • Every page shipped two <link rel="canonical"> tags. templates/index.html hard-coded one pointing at the site root while the package injected the correct per-page one. A conflicting pair is treated as no signal at all, so the per-page canonicals were doing nothing. The template no longer sets one.
  • Two advertised LLM endpoints were 404s. <meta name="llms-page-json"> and llms-architecture pointed at /page.json and /architecture.txt, both removed in dash-improve-my-llms 2.0. The <noscript> block linked to them too.
  • The Open Graph image never existed. Every share rendered a blank card against assets/og-image.png, a file not in the repo. Now points at a real asset with its actual declared dimensions.
  • apple-touch-icon.png and site.webmanifest 404'd on every page load — both <link>ed but neither shipped. Commented out with instructions.
  • piratesbagain.com in the navbar (missing r) — a dead outbound link on every page.
  • Placeholder metadata left in the template: "Your Organization Name", "Your Name or Organization", yourdomain.com, and a price of "29_000_000" in the SoftwareApplication schema (not a valid number, and the project is MIT-licensed).

Fixed — every page shipped the same hard-coded title

templates/index.html hard-coded a <title> and contained no {%title%} placeholder anywhere, so the per-page titles pages/markdown.py registers were discarded and every page's title depended entirely on dash-improve-my-llms rewriting that one element. LLMSConfig(prerender=False) — the documented one-argument rollback — silently reverted every page on every satellite to one identical string.

Now <title>{%title%}</title>, with app.title set from a new constants.APP_TITLE. Without that second half the placeholder resolves to Dash's default, the bare string "Dash", which is worse than what it replaced.

The trap, for anyone editing that block. The package finds the element with re.compile(r"<title>.*?</title>", DOTALL | IGNORECASE) and rewrites the first match:

  • Delete the element and no closing tag remains to anchor on — nothing is rewritten and no page has a title at all.
  • Spell the tag name in angle brackets inside a nearby comment and the match starts there instead, running to the next closing tag and replacing every line in between. The comment, and any markup after it, vanishes from the served page. With rewriting on it still looks correct, so the damage is only visible in the served bytes.

The comment above the element used to contain a literal <title> for exactly this reason, and the first attempt at this fix reintroduced it while explaining it. The block now describes the tags in words, and three tests pin it: the placeholder is present, the title regex matches nothing but the element itself, and no comment spells the tag in angle brackets. A fourth asserts every page serves a distinct title.

Fixed — dead links in the llms.txt documents

Found by scripts/audit_links.py across all 10 documents and 102 links.

  • The MIT LICENSE file did not exist. pages/home.md and the README both linked to it, and the Schema.org block declared the licence — so the one link a reader follows to check the terms was the one that 404'd. Added.
  • The development-server port was wrong. pages/home.md said http://localhost:8553; run.py binds 8559. The Docker instruction (8550) was right for the container but rendered as a live link that 404s for anyone not running the image — both are now code spans, so they read as instructions rather than as something to click.
  • The SKILLS.md link pointed at the wrong pathdash-improve-my-llms/blob/main/SKILLS.md, but the file lives under docs/. Fixed to blob/main/docs/SKILLS.md.

Fixed — Markdown rendering

  • A heading containing inline code crashed the site at startup. markdown2dash's renderer does create_heading_id(text[0]), and when the first inline token is formatted, text[0] is a component rather than a string — AttributeError at import, taking every page down. Fixed in lib/directives/headings.py.
  • TOC anchors pointed at ids that didn't exist. Even when it didn't crash, the renderer slugged only the first inline token (## Wiring **it** upid="wiring") while the toc directive slugged the raw markdown (wiring-**it**-up). Both now use one slugify, so the link and its target agree. Plain headings slug exactly as before, so no existing anchor moved.

Fixed — MCP wiring

  • The MCP server was never enabled. run.py did from dash import mcp_enabled, but the symbol lives in dash.mcp — the import always raised, and the app printed "MCP not available in dash 4.4.1 (needs >=4.3)" while running 4.4.1. mcp_enabled is also the decorator for marking a function as an MCP tool, not a server switch. The server is started from Dash's constructor, so enable_mcp= / mcp_path= is now passed there, and it works on all three backends rather than only FastAPI. Passed as **kwargs so naming a 4.3+ keyword can't break the boot on an older Dash.

Fixed — environment and tooling

  • The app booted silently against another project's virtualenv. An IDE run configuration pointing elsewhere started this app against whatever versions that environment held — on dash-improve-my-llms 2.0.0 there is no llms_viewer.py at all, so /<page>/llms.txt served plain Markdown to every visitor and nothing in the log said why. It cost a debugging session across two repositories, chasing a stale process and a browser cache that were both innocent, and survived a server restart and an incognito window because neither was the variable.

    Made worse by this repo's own enable_mcp fix, which removed the TypeError that had been failing loudly on the wrong interpreter — trading a crash for a plausible wrong answer.

    Warnings were tried first and were not enough: they scroll past above a wall of page-loading output while the app keeps serving. The floors are now fatal (see Changed — dependencies), and scripts/dev.sh removes the choice of interpreter entirely. A test asserts the same floor, so pytest in the wrong environment reports the cause instead of thirty downstream symptoms.

  • CI installed a tarball path that no longer existed. ci.yml hardcoded the vendored filename for the FastAPI and Quart legs, so a version bump broke exactly two of the matrix entries. It now globs vendor/.

  • Header lookups in the test client were case-sensitive. Werkzeug returns Content-Type, httpx returns content-type, so the content-negotiation assertions passed on Flask and failed on FastAPI and Quart — reading like a backend bug when the served headers were identical and correct.

  • A peer serving its SPA shell counted as a live document. The peer check asserted only status == 200, but a Dash app answers its catch-all with the app shell for any unmatched path — 2plot.dev/api/this-endpoint-cannot-exist returns 200 text/html, as does /api/network/bulletin, which does not exist. A status-only check therefore passes against every host in the network whether or not it publishes anything. smoke_live.py now rejects an HTML body for a document URL, and the same reasoning applies to the network-wide check in ROLLOUT.md.

  • smoke_live.py extracted malformed peer URLs. Its pattern stopped only at whitespace and ), and the 2.2.0 navigation block writes links as [https://host/llms.txt](https://host/llms.txt) — so it produced https://2plot.dev](https://2plot.dev/llms.txt, which would 404 in CD and fail a perfectly good deploy. Invisible locally, because the test shim answers 200 for off-host URLs.

  • Viewer-chrome detection keyed on a bare class name. docs/networks legitimately documents dv-banner, so a substring check failed on the page's own prose. Both the suite and smoke_live.py now match rendered markup (<div class="dv-banner"), which a Markdown document can never contain — otherwise the check quietly teaches people to stop documenting the viewer.

Fixed — Analytics accuracy

  • AI-search crawlers were not being counted. The visitor hook was registered after add_llms_routes, and the package's bot middleware short-circuits ClaudeBot / ChatGPT-User / PerplexityBot with its own response — so those requests never reached the tracker. The hook is now registered first on Flask/Quart (and last on FastAPI, where Starlette runs the most recently added middleware outermost).
  • Every visitor looked like one visitor behind a proxy. The tracker used remote_addr, which on Render/Cloudflare is the proxy. It now reads CF-Connecting-IP, True-Client-IP, X-Real-IP and X-Forwarded-For first, and takes the country from Cloudflare's CF-IPCountry header when present — free, instant and accurate.
  • Concurrent workers overwrote each other's hits. The ledger was read, modified and rewritten with no lock; under four workers most hits were lost. Writes now take an flock and land via an atomic replace.
  • Geolocation no longer blocks page views. The ip-api.com lookup ran inline with a 2s timeout on the first hit from each new IP. It now runs in a bounded background thread and is backfilled into the buffered hit before it is written, so the country is still recorded. Disable with ANALYTICS_GEO_LOOKUP=0.
  • The ledger is bounded and no longer rewritten on every request. Hits are buffered (10 hits / 30s) and pruned to ANALYTICS_RETENTION_DAYS (45) and ANALYTICS_MAX_VISITS (20000); the hub holds the durable history.
  • The ledger path is now absolute (TRAFFIC_ANALYTICS_FILE, else repo root) — a relative default wrote a different file depending on the working directory.
  • Tablets are no longer counted as mobile (iPads and most Android tablets send a mobile token too, and the mobile test ran first).

1.0.0 - 2026-06-14

First stable release. The boilerplate moves to Dash 4.x with pluggable backends and dash-improve-my-llms 2.0, and retires the experimental TOON format entirely. This is a significant architectural release — see the migration notes at the end of this section.

Versioning note: the 0.5.00.8.0 entries below were the December 2025 TOON line. That work has been removed (see "Removed" below) and the project resumes a single, monotonic version line at 1.0.0. A short-lived second 0.5.0 (the May 2026 dash-improve-my-llms 2.0 preview) has been folded into this entry.

Added — Pluggable backends (Flask / FastAPI / Quart)

  • lib/backend.py — single source of truth for backend selection. Reads the DASH_BACKEND environment variable (flask | fastapi | quart), falls back to flask, and exposes BackendInfo (label, color, icon, async flag) so UI components stay in sync with the running backend.
  • run.py constructs Dash(backend=resolve_backend(), ...) and attaches app._backend_info for layout components.
  • components/backend_badge.py — a navbar/header badge that shows which backend the site is currently running on.
  • lib/asgi_middleware.py and lib/asgi_routes.py — ASGI middleware and showcase routes (/healthz, /api/backend, /api/pages) that light up on the FastAPI/Quart backends.
  • New documentation sections:
    • Pluggable Backends (docs/backends/) — run the site on any of the three backends with one env var.
    • Backend Deep Dive (docs/backend-comparison/) — architecture, strengths/weaknesses, deployment, and best practices for each backend.
    • FastAPI Showcase (docs/fastapi-showcase/) — OpenAPI docs, a native JSON API, ASGI middleware, async demo, endpoint explorer, and a stress test, showing what the ASGI backends unlock.

Added — AI/LLM integration via dash-improve-my-llms 2.0

  • LLMS_DOC pattern. Pages expose a module-level prose string (or call register_page_metadata(path, llms_doc=...)); the package serves it verbatim at /<page>/llms.txt under whichever backend is active.
    • pages/markdown.py registers the expanded markdown body (with .. source:: directives inlined) for every markdown-driven page.
    • pages/home.py exports LLMS_DOC = content for the root prose.
  • Multi-backend AI/LLM surfaces. add_llms_routes(app) auto-detects the backend and serves /llms.txt, /<page>/llms.txt, /sitemap.xml, and /robots.txt under Flask, FastAPI, and Quart alike — no if IS_FLASK: gate.
  • MCP resource bridge. Each page's prose registers as a dash.mcp resource on Dash 4.3+ (a silent no-op on older Dash).

Changed

  • Upgraded Dash 3.2.0 → 4.2.0 and Dash Mantine Components 2.4.0 → 2.7.0 (Mantine 8.3.6). React 18.2.0.
  • docs/ai-integration/ai-integration.md fully rewritten for the 2.0 surface (LLMS_DOC, multi-backend, MCP bridge).
  • requirements.txt now pins dash>=4.1.0, dash-mantine-components>=2.7.0, and dash-improve-my-llms[flask]>=2.0.0, with commented [fastapi], [quart], and [all] extras plus uvicorn for ASGI deployment.
  • docs/example/example.md "Highlighting Important Elements" section rewritten around the LLMS_DOC pattern.
  • components/header.py, components/appshell.py, and components/navbar.py updated for the new backend badge and navigation (TOON Format and Handoff entries removed).
  • lib/directives/llms_copy.py / assets/llms_copy.js updated for the 2.0 /<page>/llms.txt routing.
  • APP_VERSION and package.json bumped to 1.0.0.

Removed

  • The entire TOON format systemlib/toon_generator.py (~1100 lines), the docs/toon-format/ page, the TOON Analytics Dashboard (docs/data-visualization/toon_dashboard.py), and all /llms.toon routes. dash-improve-my-llms 2.0 removed TOON from its public API (TOONConfig, toon_encode, generate_*_toon no longer exist).
  • /page.json and /<page>/page.json routes — dropped in dash-improve-my-llms 2.0; Dash 4.3 MCP exposes layouts as resources natively.
  • /architecture.txt — likewise superseded by MCP.
  • mark_important() and mark_component_hidden() — now deprecated no-ops in 2.0. Write the emphasis directly into a page's LLMS_DOC markdown.
  • LLMS_INTEGRATION.md and the docs/handoff/ doc (the FastAPI port plan that became 2.0) — superseded by the in-app AI Integration page.

Migration notes (from any 0.x)

  1. Backend: the site defaults to Flask, so no change is required. To run on FastAPI or Quart, install the matching extra (pip install "dash[fastapi]") and set DASH_BACKEND=fastapi.
  2. AI/LLM prose: give each page module an LLMS_DOC = """...""" string at module scope (or register_page_metadata(path, llms_doc=...) when the prose is computed). The startup UserWarning from 2.0 names every page still missing prose.
  3. dash-improve-my-llms extra: pick [flask], [fastapi], [quart], or [all] in requirements.txt.
  4. Removed APIs: replace any mark_important() / mark_component_hidden() calls (now no-ops) with LLMS_DOC content, and remove references to TOON, /page.json, and /architecture.txt.

0.8.0 - 2025-12-14

Added

  • TOON v3.3 Format Enhancements - Major comprehension improvements from ~75-80% to ~95%+
    • New Dataclasses:
      • CodeTip - Short instructional code snippets with context
      • BestPractice - Numbered best practices with multi-line code examples
      • Pattern - Architectural patterns with implementation code
      • Resource - External resource links with full URLs
    • New Extraction Functions:
      • extract_code_tips() - Finds short code snippets (2-15 lines) with headings
      • extract_best_practices() - Extracts numbered practices from "Best Practices" sections
      • extract_patterns() - Captures pattern implementations from "Common Patterns" sections
      • extract_resources() - Extracts markdown links with full URLs preserved
    • New TOON Sections:
      • tips[N]{context,lang,code}: - Compact code tips with one-line previews
      • bestPractices[N]: - Full multi-line code snippets for each practice
      • patterns[N]: - Pattern descriptions with implementation code blocks
      • resources[N]{name,url}: - External links without URL truncation

Changed

  • Updated TOON format version from toon/3.2 to toon/3.3
  • Enhanced summary line to include tips, best practices, patterns, and resources counts
  • Improved content deduplication - Tips exclude Best Practices and Patterns sections to avoid duplicate code

Fixed

  • Code block detection in section boundaries - Headings inside code blocks (like ## My Visualization in markdown examples) were incorrectly detected as section boundaries
    • Added code block range detection using code_block_ranges list
    • Added is_in_code_block() helper to filter out false headings
    • Applied fix to extract_code_tips(), extract_best_practices(), and extract_patterns()
  • re.escape() issue - re.escape("Best Practices") was escaping spaces incorrectly
    • Changed to custom escaping that only escapes regex special chars but preserves spaces

Technical Details

  • Updated lib/toon_generator.py (~1100 lines after updates)
  • Test results for Data Visualization page:
    • 6 tips (properly deduplicated)
    • 5 best practices (all with full multi-line code)
    • 3 patterns (all with implementation code)
    • 4 resources (with full URLs)
    • TOON size: 11,444 chars

0.7.0 - 2025-12-13

Added

  • Custom Documentation-Aware TOON Generator (lib/toon_generator.py)
    • Custom TOON route that processes raw markdown from NAME_CONTENT_MAP
    • Achieves 54.7% token reduction vs llms.txt while preserving all content
    • Full directive awareness (exec, source, kwargs, toc, llms_copy)
    • Features:
      • Section extraction with hierarchical structure (h2-h6)
      • Directive parsing with option extraction
      • Source file embedding with smart code compression
      • Table and list preservation in compact format
      • Exec component detection with callback markers
      • Deduplication of code examples and directives
    • Smart code compression (compress_code()) that:
      • Preserves imports, function/class definitions
      • Keeps callback decorators and Input/Output patterns
      • Truncates long files with line count indicator
    • TOON v3.2 format with optimized output:
      • Compact section format: [level] title
      • Grouped directives by type
      • Inline table format with pipe separators
      • Key lists extraction for substantial bullet points

Changed

  • Custom /<page>/llms.toon route in run.py
    • Overrides default dash-improve-my-llms TOON for markdown pages
    • Uses raw markdown from NAME_CONTENT_MAP instead of rendered components
    • Processes source directives to embed actual file content

Fixed

  • TOON content gap issue - Previous TOON was only capturing 15-20% of documentation content
    • Root cause: dash-improve-my-llms extracts from rendered Dash components, losing directive context
    • Solution: Custom route processes raw markdown with full directive awareness
    • Previous TOON was 185% the size of llms.txt (27,669 chars vs 14,943 chars)
    • New TOON is 45.3% the size of llms.txt (6,965 chars vs 15,369 chars)

Technical Details

  • New module: lib/toon_generator.py (698 lines)
    • generate_documentation_toon() - Main entry point
    • build_documentation_toon() - TOON string builder
    • extract_sections() - Hierarchical section parser
    • extract_directives() - Directive extractor with options
    • process_source_directive() - File content reader
    • process_exec_directive() - Component metadata extractor
    • compress_code() - Smart code compression
    • compress_section_content() - Content summarization
    • extract_tables() / extract_lists() - Structure extractors

0.6.0 - 2025-12-13

Added

  • Enhanced TOON Format v3.1 - Lossless semantic compression with 40-50% token reduction
    • Application context with related pages and multi-page awareness
    • Page purpose explanations with human-readable descriptions
    • Component breakdown with type distribution
    • Human-readable callback descriptions
    • Synthesized page summaries
    • Link categorization (internal vs external)

Changed

  • Upgraded dash-improve-my-llms from v1.0.0 to v1.1.0
    • Lossless semantic compression preserves all meaningful content
    • New content extraction: extract_markdown_content(), parse_markdown_content()
    • Smart compression: compress_code_example(), compress_section_content()
    • New helper functions: _generate_page_summary(), _format_callback_description()

New TOONConfig Options

  • preserve_code_examples=True - Include code snippets from markdown
  • preserve_headings=True - Keep section structure
  • preserve_markdown=True - Extract dcc.Markdown content
  • max_code_lines=30 - Max lines per code example
  • max_sections=20 - Max sections to include
  • max_content_items=100 - Increased from 20

Documentation

  • Updated AI/LLM Integration Guide with v1.1.0 TOON enhancements
    • Added design principle: lossless semantic compression
    • Updated token efficiency comparison table
    • Added 6 content gap examples (context, purpose, components, callbacks, summary, navigation)
    • Updated TOONConfig with new v1.1.0 options

Improved

  • Better content preservation in TOON format
  • Optimal information density vs token reduction balance
  • Enhanced developer experience with richer TOON output

0.5.0 - 2025-12-13

Added

  • TOON Format Support - Token-Oriented Object Notation for 50-60% fewer tokens
    • New /llms.toon endpoint for token-optimized LLM documentation
    • New /architecture.toon endpoint for token-optimized architecture
    • New /<page>/llms.toon per-page TOON format endpoints
    • TOON provides tabular arrays and explicit length markers for LLM validation
    • Ideal for API calls, large apps, and cost-conscious deployments

Changed

  • Upgraded dash-improve-my-llms from v0.3.0 to v1.0.0
    • Production-ready release with comprehensive test coverage (88 tests, 98% coverage)
    • New API exports: TOONConfig, toon_encode, generate_llms_toon, generate_architecture_toon
    • Zero-change migration: existing code works without modifications

Documentation

  • Updated AI/LLM Integration Guide with comprehensive TOON format documentation
    • Added TOON format section with benefits comparison table
    • Added example comparison (markdown vs TOON token usage)
    • Added TOONConfig configuration examples
    • Added programmatic TOON generation examples
    • Updated available routes table with new TOON endpoints
    • Updated key functions reference with new TOON imports

Improved

  • Better AI/LLM documentation organization
  • Enhanced developer experience with new format options
  • Cost optimization through token-efficient TOON format

0.4.0 - 2025-11-10

Added

  • LLM Copy Button Directive (.. llms_copy::)
    • New custom directive that adds a "Copy for llm 📋" button to documentation pages
    • Copies the page's /llms.txt URL to clipboard for easy AI assistant sharing
    • Users can paste the URL into ChatGPT, Claude, or other AI assistants for context-aware help
    • Features:
      • Automatic URL construction based on current page path
      • Visual feedback with "✓ Copied! ✓" confirmation
      • Fallback clipboard method for non-HTTPS contexts (HTTP development servers)
      • Works across all modern browsers
      • Tooltip: "Copy llms.txt URL for AI assistants"
    • Implementation:
      • Python directive: lib/directives/llms_copy.py
      • JavaScript handler: assets/llms_copy.js
      • Uses both modern Clipboard API and legacy execCommand fallback
      • Mutation observer for Dash-rendered content detection
    • Documentation updated in Custom Directives guide
    • Added to all 5 example documentation pages

0.3.0 - 2025-11-09

Added - Documentation System

  • Comprehensive Getting Started Guide (385+ lines)
    • Detailed directive options documentation (:code: false, :defaultExpanded, :withExpandedButton)
    • Interactive examples with best practices
    • File structure examples and patterns
  • Custom Directives Guide (476 lines)
    • Complete documentation for all 4 directives (toc, exec, source, kwargs)
    • 3 live Python examples (button, counter, form validation)
  • Data Visualization Guide (465+ lines)
    • 5 chart type examples with full implementations
    • Plotly template integration guide
    • Real-time updates and dashboard patterns
  • Interactive Components Guide (569 lines)
    • 6 callback pattern examples
    • State management, pattern matching, chained callbacks
    • Loading states demonstration
  • AI/LLM Integration Guide (577 lines)
    • Complete dash-improve-my-llms documentation
    • SEO optimization strategies
    • Bot management and privacy controls

Added - Theme System

  • DMC Figure Templates Integration
    • All Plotly charts now use dmc.add_figure_templates()
    • Theme-aware callbacks for 6 chart examples
    • Charts dynamically update with light/dark theme toggle
    • Proper background rendering in both themes
  • Code Block Theming
    • Theme-aware CSS for markdown code blocks
    • Proper syntax highlighting in light and dark modes
    • Inline code and code block styling
  • Comprehensive Theme Configuration
    • Professional typography hierarchy (h1-h6)
    • Systematic 4px-based spacing scale
    • 5-level shadow system
    • Consistent border radius system
    • Global component defaults via theme.components
    • Softer black (#1a1b1e) for better contrast

Added - UI/UX Enhancements

  • Navigation Improvements
    • Custom page ordering (Getting Started → Custom Directives → AI/LLM → Interactive → Visualization)
    • Better visual hierarchy
    • Organized documentation sections
  • Typography System
    • Inter font family across application
    • Optimized line heights (md: 1.55 for body text)
    • Proper font sizes (16px base)
    • Font smoothing and text rendering optimization
  • Layout Refinements
    • Better responsive breakpoints (md for navbar)
    • Improved spacing consistency
    • Enhanced mobile experience
    • Better heading spacing (1.5em top, 0.5em bottom)

Added - Production Features

  • SEO-Ready HTML Template
    • Comprehensive meta tags with developer guidance
    • Open Graph and Twitter Card configuration
    • Structured data (Schema.org) for Organization and SoftwareApplication
    • Analytics integration (Google Analytics ready to enable)
    • Favicon configuration with multiple formats
    • Performance optimization (preconnect hints)
    • Search engine verification placeholders
    • Enhanced noscript fallback with styled content
    • 297 lines of documentation and configuration

Improved

  • 15 Working Python Examples
    • Button interactions, counters, form validation
    • 5 chart types (bar, line, scatter, realtime, dashboard)
    • Callback patterns and state management
    • All examples theme-aware and fully functional
  • Directive System
    • Fixed kwargs directive to parse component specifications (e.g., dmc.Button)
    • Better error handling and fallbacks
    • Support for directive options
  • Code Quality
    • Fixed JSON serialization error (removed lambda from theme styles)
    • Better import statements
    • Comprehensive inline comments
    • Fixed DMC 2.4.0 compatibility issues

Changed

  • Better Performance
    • Optimized theme switching
    • Smooth transitions
    • Better font loading
  • Documentation Organization
    • Clear learning path
    • Progressive complexity
    • Better code examples

Fixed

  • Import errors in example files (missing dmc, State imports)
  • DMC 2.4.0 compatibility (removed unsupported type prop from TextInput)
  • JSON serialization error in theme configuration
  • Heading ID generation with code blocks in markdown
  • Theme persistence and switching
  • Code block rendering in dark mode

0.2.0 - 2025-11-09

Changed

  • BREAKING: Migrated from Dash 2.5.0+ to Dash 3.2.0
  • BREAKING: Migrated from dash-mantine-components 0.14.7 to 2.4.0
  • BREAKING: Updated all Mantine packages from 7.14.1 to 8.3.6
  • Updated Flask from 1.0.4+ to 3.1.2
  • Updated Plotly from 5.0.0+ to 6.4.0
  • Updated app.run_server() to app.run() (Dash 3.x standard)

Removed

  • BREAKING: Removed deprecated package imports:
    • dash-html-components (now part of main dash package)
    • dash-core-components (now part of main dash package)
    • dash_table (now part of main dash package)

Fixed

  • Replaced deprecated NotificationProvider with NotificationContainer
  • Fixed Mantine version mismatch between package.json and DMC version
  • Added node_modules to .gitignore

Added

  • Added package-lock.json for reproducible npm builds
  • Comprehensive migration documentation (8 detailed guides)
  • Project analysis and assessment documentation
  • Persistent theme preference storage using localStorage
  • Browser color scheme preference detection on first visit
  • Smooth theme transitions without page flash
  • AI/LLM & SEO Integration (dash-improve-my-llms v0.3.0)
    • Automatic llms.txt, page.json, architecture.txt generation
    • SEO-optimized sitemap.xml with intelligent priority
    • Bot management (blocks AI training, allows AI search)
    • Structured data for better search indexing
    • Privacy controls for sensitive pages

Improved

  • Better dependency management with cleaner requirements.txt
  • Improved code organization with inline comments
  • Enhanced theme management system
  • Better performance with latest Dash and DMC versions

0.1.0 - 2024-11-30

Added

  • Initial release of Dash Documentation Boilerplate
  • Markdown-driven documentation system
  • Support for light and dark themes
  • Responsive design for mobile and desktop
  • Docker deployment support
  • Interactive code examples with syntax highlighting
  • Custom markdown directives:
    • toc - Table of contents generation
    • exec - Executable Python code blocks
    • source - Source code display with syntax highlighting
    • kwargs - Component props documentation
  • AppShell layout with header, navbar, and responsive drawer
  • Search functionality for navigation
  • Theme toggle with icon indicators
  • Integration with dash-mantine-components (DMC)
  • Integration with python-frontmatter for metadata
  • Custom CSS styling system
  • Docker and docker-compose configuration

Documentation

  • README with getting started guide
  • Project structure documentation
  • Example documentation pages

Version History Summary

Version Date Dash DMC Mantine Python Features
1.0.0 2026-06-14 4.2.0 2.7.0 8.3.6 3.11+ Pluggable backends (Flask/FastAPI/Quart), dash-improve-my-llms 2.0, TOON removed
0.8.0 2025-12-14 3.2.0 2.4.0 8.3.6 3.11+ TOON v3.3, tips/best practices/patterns/resources extraction
0.7.0 2025-12-13 3.2.0 2.4.0 8.3.6 3.11+ Custom TOON generator, documentation-aware TOON v3.2
0.6.0 2025-12-13 3.2.0 2.4.0 8.3.6 3.11+ Enhanced TOON v3.1, dash-improve-my-llms v1.1.0
0.5.0 2025-12-13 3.2.0 2.4.0 8.3.6 3.11+ TOON format, dash-improve-my-llms v1.0.0
0.4.0 2025-11-10 3.2.0 2.4.0 8.3.6 3.11+ LLM Copy Button directive
0.3.0 2025-11-09 3.2.0 2.4.0 8.3.6 3.11+ Comprehensive docs, theme system, SEO
0.2.0 2025-11-09 3.2.0 2.4.0 8.3.6 3.11+ Migration to Dash 3.x, DMC 2.4.0, AI/LLM
0.1.0 2024-11-30 2.5.0+ 0.14.7 7.14.1 3.11+ Initial release

Migration Guides

Migrating to 1.0.0 from any 0.x

This is the major release that moves the boilerplate to Dash 4.x. See the Migration notes under 1.0.0 for the full checklist. In short:

  1. Backend: defaults to Flask — no change required. For FastAPI/Quart, pip install "dash[fastapi]" (or [quart]) and set DASH_BACKEND=fastapi.
  2. AI/LLM prose: add an LLMS_DOC string to each page module (or call register_page_metadata(path, llms_doc=...)); the 2.0 startup warning lists pages still missing prose.
  3. dash-improve-my-llms extra: pick [flask] / [fastapi] / [quart] / [all] in requirements.txt.
  4. Removed APIs: drop any TOON usage (TOONConfig, toon_encode, generate_*_toon), /page.json, /architecture.txt, and the now-no-op mark_important() / mark_component_hidden() calls — move emphasis into LLMS_DOC instead.

Migrating to 0.6.0 from 0.5.0

Zero changes required! The upgrade is fully backwards compatible.

Key changes:

  1. Update dash-improve-my-llms in requirements.txt to >=1.1.0
  2. TOON output now includes richer, lossless semantic content automatically

Optional new TOONConfig options:

from dash_improve_my_llms import TOONConfig

app._toon_config = TOONConfig(
    # New in v1.1.0:
    preserve_code_examples=True,   # Include code snippets
    preserve_headings=True,        # Keep section structure
    preserve_markdown=True,        # Extract dcc.Markdown content
    max_code_lines=30,             # Max lines per code example
    max_sections=20,               # Max sections to include
    max_content_items=100,         # Increased from 20
)

Migrating to 0.5.0 from 0.4.0

Zero changes required! The upgrade is fully backwards compatible.

Key changes:

  1. Update dash-improve-my-llms in requirements.txt to >=1.0.0
  2. New TOON endpoints are automatically available:
    • /llms.toon - Token-optimized LLM docs
    • /architecture.toon - Token-optimized architecture
    • /<page>/llms.toon - Per-page TOON format

Optional new features:

# Configure TOON output (optional)
from dash_improve_my_llms import TOONConfig

app._toon_config = TOONConfig(
    indent=2,
    delimiter=",",
    include_metadata=True
)

# Programmatic TOON encoding (optional)
from dash_improve_my_llms import toon_encode
toon_string = toon_encode({"key": "value"})

Migrating to 0.3.0 from 0.2.0

Minor updates, mostly additive. Key changes:

  1. Documentation content significantly expanded
  2. Chart examples now use DMC figure templates
  3. Enhanced SEO features in index.html
  4. Better theme integration across all components

Migrating to 0.2.0 from 0.1.0

Major breaking changes. See migration documentation:

  • Quick Start: MIGRATION_README.md
  • Detailed Guide: claude.md
  • Step-by-Step: MIGRATION_CHECKLIST.md
  • Code Changes: CODE_CHANGES_SUMMARY.md

Key changes to be aware of:

  1. Update all imports from dash_html_components to from dash import html
  2. Update all imports from dash_core_components to from dash import dcc
  3. Replace dmc.NotificationProvider() with dmc.NotificationContainer()
  4. Update custom components to use DMC 2.4.0 API
  5. Check CSS for any Mantine 8 specific changes

Support