Changelog
Version history for the Pip Install Python documentation site (2plot.dev) — the docs framework, the component catalogue, and the site's own infrastructure.
The timeline on this page is rendered from CHANGELOG.md, reproduced below.
Changelog
All notable changes to Pip Install Python Documentation will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Fixed
- **The /pip card's
llms.txtlink now routes to the component's dedicated
site** (https://<sub>.2plot.dev/llms.txt, new tab) whenever the network directory says that satellite is serving — the owner's requested behavior: the satellite's index twin is the complete reference, where this site's /pip/<component>/llms.txt is only the quick-start twin. Components with no dedicated site keep the local twin link, and the "This page as Markdown" header link (/pip/llms.txt) is unchanged. Verified by building every catalogue card through the real _card callee: 10 satellite-routed, 9 local.
- Sign-in works on the satellite again. Clicking "Sign in" or "Create free
account" on 2plot.dev produced Clerk's "This operation is not allowed on a satellite domain" error — both the auth-gate card and the header button opened Clerk's modal, which satellites reject. They now navigate to https://2plot.ai/onboarding?returnTo=<the page you were on>; the hub signs you in and sends you straight back (the sign-up button carries &mode=signup so the hub opens the create-account flow directly). Ships via dash-clerk-auth 0.9.2 (vendored): the header button is satellite-aware in the package, and the new satellite_sign_in_redirect / CLERK_SATELLITE_SIGN_IN_REDIRECT setting defaults to the hub's onboarding page. Satellite env vars are now documented in .env.example, and the subdomain blueprint STANDARD raises its floor to dash-clerk-auth ≥ 0.9.2 with the modal-on-satellite pitfall spelled out for the other *.2plot.dev hosts.
requirements.txtpointed at two vendored wheels that no longer exist.
./vendor/dash_clerk_auth-0.9.2-py3-none-any.whl and ./vendor/dash_leaflet2-0.1.0-py3-none-any.whl were both removed from vendor/, so pip install -r requirements.txt — and therefore the Docker build, which COPY vendor/ ./vendor/ before pip runs — could not resolve. Repinned to the artifacts below. Nothing warned about this locally because the site's .venv already had both packages installed from the old wheels.
- The home page's brand H1 had picked up a leading blank line, so
pages/home.md no longer opened with # 2plot.dev — …. That line is the string /llms.txt publishes as the site's H1 and tests/test_site_identity.py pins verbatim; restored.
- The home page's YouTube badge rendered as invalid. The cause was not the
URL: shields.io's youtube/channel/subscribers/… endpoint is failing service-wide — it returns invalid for every channel, including well-known IDs that certainly exist, so its YouTube API credentials are dead upstream. Replaced with a static badge/YouTube-@2plotai shield (verified rendering) linked to the channel's ?sub_confirmation=1 URL. The live subscriber count is the deliberate trade for a badge that draws.
- The footer's YouTube icon (
components/appshell.py) still pointed at the
retired @pipinstallpython channel while every other surface — README, templates/index.html, the email templates — had moved to @2plotai. Now https://www.youtube.com/@2plotai?sub_confirmation=1.
Fixed
- pip-install-python.com showed a black page with a raw Clerk error. The
retired domain is a second custom domain on this same Render service, so it served the real app — and then ClerkJS booted, saw a host that is not the registered satellite domain (CLERK_SATELLITE_DOMAIN=2plot.dev), and the primary rejected the handshake with does not match one of the allowed values for parameter redirect_url. Every visitor arriving from an older post or video got that JSON body on a black screen. lib/canonical_host.py (ported from dash-mui-scheduler, same contract — keep them in sync) now 301s every non-canonical host to 2plot.dev, which also covers the *.onrender.com URL that never stops answering once a custom domain is attached.
- Applied as the outermost WSGI wrapper in
run.py, deliberately: a
Flask before_request is not enough, because dash-improve-my-llms answers crawler-classified requests with prerendered HTML and short-circuits the remaining handlers — so a crawler on the retired domain would have been served a duplicate of the page instead of the redirect that consolidates it. Firing at the WSGI layer also means the 301 lands before a byte of HTML is parsed, so ClerkJS never runs on the wrong host at all.
- Path- and query-preserving, which is the entire point: the deep links
in old posts and videos land on the page they promised, and a 301 is what passes the old domain's accumulated ranking to the new one. Exempt: /healthz (a 3xx there fails the Render deploy), /api/ and /webhooks (Discord interactions and the Clerk webhook verify a signature over the body), /assets and /_dash*, and localhost/test clients. GET and HEAD only — most clients downgrade a redirected POST to GET and drop the body.
- Defaults on here, where the rest of the fleet defaults off. The fleet's
default guards against redirecting to a domain whose DNS has not resolved yet; here that risk is inverted and already realised. CANONICAL_HOST_REDIRECT=0 disables it. Covered by tests/test_canonical_host.py (11 cases).
- **The network directory advertised the retired domain, and would have become
self-referential.** AFFILIATED listed https://pip-install-python.com with Machine-readable: https://pip-install-python.com/llms.txt, published in this host's own /llms.txt. Once the 301 above shipped, an agent following that line would have been sent straight back to this host's /llms.txt. A host that redirects to you is an alias, not a peer — entry removed. Verified: zero references remain in the published index, and the other affiliated entries are untouched.
Changed
- dash-leaflet2 installs from PyPI. 0.2.2 was published 2026-08-05, so
requirements.txt drops ./vendor/dash_leaflet2-0.2.2-py3-none-any.whl for dash-leaflet2==0.2.2 and the wheel leaves vendor/, which now holds only the clerk and excalidraw artifacts. Note the PyPI wheel is a different build of the same version than the one that was vendored (152,503 vs 152,509 bytes — zip metadata), which is why the swap was verified by installing from PyPI and booting rather than assumed. Same move as flexlayout 2.0.0 and muicharts 1.4.0. It also gains its missing home-page catalogue row — dash-leaflet2 was the one maintained package absent from that table, since a package with no PyPI release has no pepy badge to show. Badge verified live (372 downloads).
- Vendored dependencies refreshed:
dash-clerk-auth0.9.2 → 1.0.0
and dash-leaflet2 0.1.0 → 0.2.2, both as wheels (the repo's vendor convention — an sdist would make the Docker build compile at install time). Verified 2026-08-04 that neither is on PyPI yet, so vendor/ remains the install path and .claude/tasks/pypi-publish-checklist.md is updated with the new versions and dash-leaflet2's real source path (2plot_leaflet/, not dash-leaflet2/).
- dash-clerk-auth 1.0.0 requires no application change: diffing the
0.9.2 and 1.0.0 wheels, the only differences are __version__ and one corrected docstring (clear_header → clear_cookie_header). 1.0.0 declares the 0.9.x surface stable, which is exactly what run.py already calls — all ten register_clerk_auth keyword arguments and all four imported symbols (register_clerk_auth, configure_app, create_clerk_menu, current_user) verified present. Booted against the real env: registers clean, backend autodetected as Flask. Note its requires-python floor rose to ≥3.10 (3.9 was never actually installable); this site runs 3.11, so no impact.
- dash-leaflet2 0.2.2 over the 0.2.0 that was staged: 0.2.1/0.2.2 were
documentation-site and network releases with no dl2.* component change, but 0.2.2's regenerated component stubs import dash.types.NumberType (falling back for dash<=4.1) rather than defining it locally — the better fit for the Dash 4.4.1 pinned here.
- Navigation labels shortened for the four longest sidebar entries —
Dash Documentation Boilerplate → Boilerplate, Dash Improve My LLMs → LLMs, Dash Email → Email, Dash Flows → Flows — via each page's frontmatter name:. Endpoints, package names and descriptions are untouched, so no URL or catalogue entry moves.
- The navbar's "Docs Boilerplate" link now points at
https://boilerplate.2plot.dev rather than its onrender.com origin, matching the subdomain promoted to live in the network directory.
Added
- Hero banner on the home page — the 2plot wordmark above the fold,
served from cdn.2plot.ai/github_assets/. Both the light and dark variants ship in the layout and CSS picks one off data-mantine-color-scheme (assets/main.css); a callback on the theme store would leave the wrong banner on screen for a round trip on every page load, and a dark-mode wordmark on the light theme is exactly the mismatch worth avoiding. Light is the default and dark the override, so a missing color-scheme attribute shows one banner rather than none. It is a real component in pages/home.py, not markup in home.md — that page renders through dcc.Markdown with dangerously_allow_html off, where an <img> tag would come out as literal text.
[2.2.2] - 2026-08-02
Changed
- Network directory:
muischeduler,flowsandemojimartpromoted
shipping → live, and all three added to VERIFIED_APP_IDS (source-verified: muischeduler's traffic_report.app_key(), flows' satellite_reporter.app_key(), emojimart's constants.APP_KEY; each battery-verified live on its subdomain 2026-08-02). Their /pip entries now render the CDN social card, and the live /llms.txt Network section gains all three subdomains. Note for the traffic ledger: flows reported under the key email for a few hours on 2026-08-02 (a dash-email env block pasted onto its Render service during an unrelated fix) — corrected the same day; a small same-day blip on email's series is expected.
Added
- Catalogue presence for the two network-infrastructure projects that had
none here: summary/funnel docs pages at /pip/dash_improve_my_llms and /pip/dash_documentation_boilerplate (public, concise overview + install / use-the-template quickstart, funneling to the full docs at llms.2plot.dev and boilerplate.2plot.dev), rows in the home page's Currently Maintained table, and dash-improve-my-llms in the PyPI download counter. The boilerplate gets a GitHub-stars badge instead of a pepy badge and stays out of the counter — it is a use-this-template repo, not a PyPI package (verified: pypi.org 404s for dash-documentation-boilerplate).
Changed
- Network directory: the documentation boilerplate promoted from
OTHER_PEERS to a full SATELLITES entry (key boilerplate, status live, package: None since it is not on PyPI), and the llms entry's docs_path now points at its new summary page — so both render their CDN-verified social cards on /pip and get the subdomain banner. The boilerplate's canonical/legacy app ids and VERIFIED_APP_IDS entry are unchanged, and it is listed exactly once in /llms.txt's Network section (via satellite_peers() now, instead of OTHER_PEERS).
/pipcomponent index: entries whose satellite is WIRED —status: live
with a subdomain in lib/network_directory.SATELLITES — now show the satellite's 1200×630 social card (from cdn.2plot.ai/github_assets/) in place of the icon+name header, with the name kept as a caption and alt text. Rollout state is now visible at a glance: a card means that subdomain's docs are verified wired; unwired entries keep the plain icon+name row. Derived via the new card_url_for() — status is the trust signal (the hub promotes to live only after the card is verified on the CDN), so the page never probes the CDN at request time, and a shipping entry forced onto its subdomain by SATELLITE_SUBDOMAINS_LIVE still shows no card until promoted.
[2.2.1] - 2026-08-01
Changed
- Network directory:
flexlayoutandemailentries promoted tolive—
both satellites measured on the full network standard 2026-08-01 (brand H1, OAI robots, zero empty og tags, 1200×630 CDN card). flexlayout now appears in /llms.txt's Network section and the per-page banners.
VERIFIED_APP_IDSgrewleaflet,emailandflexlayout— each id read
in the satellite's own source, per the "verified, never guessed" rule.
Added
.claude/support_files/subdomain_blueprint/(local ops docs, untracked) —
the unified satellite standard (README / STANDARD / LESSONS / QUEUE / KICKOFF), reconciling the boilerplate's network-standard doc, the .subdomains package bundle and the rollout kickoff into one point of direction for the remaining migrations. The kickoff task file slims to an ops status ledger pointing at it.
Known issues
- Upstream (dash-improve-my-llms): the crawler prerender carries no
og:image, so Slackbot/Twitterbot unfurls are text-only network-wide while Facebook and browsers get the full card. Targeted at dimll 2.4; repro in .claude/knowledge/llms-package-notes.md.
[2.2.0] - 2026-08-01
Fixed — the share card was blank (port of boilerplate 1.2.1–1.2.3)
Every OG/Twitter tag was declared twice: once statically in templates/index.html, and once per page by Dash — which, with no image_url= passed to any register_page, emitted og:image/og:description as content="". The empty tags came later in document order, scrapers honoured them, and every unfurl of 2plot.dev rendered a blank card.
- Every
register_pagenow passesimage_url=lib.constants.OG_IMAGE_URL
(all ~27 docs pages via pages/markdown.py, plus every static page); the home page finally has a description= too.
templates/index.htmldeclares ONLY what Dash omits:og:site_nameand
the image auxiliaries (secure_url, type, width, height, alt, twitter:image:alt). The doubled og:*/twitter:* set is deleted.
- The card itself:
assets/plotly-pro-og.gif(5.05 MB, 58-frame animated,
910×480 declared as 1200×630 — over Twitter's 5 MB ceiling and a lying size) is deleted; scripts/make_social_card.py (from the boilerplate) renders a true 1200×630 PNG published at https://cdn.2plot.ai/github_assets/2plot.dev.png — CDN-hosted so a scraper's fetch never races a cold container.
og:titleno longer says "Dash Pip Components":PAGE_TITLE_PREFIXis
now derived from SITE_SHORT_NAME ("2plot.dev | "), and the changelog and analytics pages join the prefix convention.
- Guard rails:
tests/test_social_card.py(ported; empty/duplicate/SVG
cards, manifest installability, untracked-asset sweep), tests/test_site_identity.py grows the fork-brand sweep, and the smoke battery gains social_card_real_pixels — it reads the REAL CDN file's IHDR pixels against the declared size after every deploy.
Changed — one short app id per app, on every hub surface
Four id namespaces disagreed (directory long ids, the ad board's hardcoded list, the bulletin's verified ids, and whatever each satellite sent). The canonical id is now the directory key — boilerplate, leaflet, email, flexlayout — never the package name:
lib/network_directory.py: every satellite'sapp_idIS itskey; the
PyPI name moved to a package field; old spellings live in legacy_ids; new canonical_app_id() folds any legacy id into the canonical one.
- Applied at every ingest point — ad serve/click, bulletin
?app=,
X-Satellite-App, page-tiers — so a satellite still sending ?app=dash-leaflet2 accrues under leaflet and history stays one series.
- The ad board's Target-apps suggestions derive from the directory (the
hardcoded long-name list is gone) and canonicalize on save; the network board's preview reads the package field instead of guessing from a dash- prefix.
- Timing note: stored data carried only
2plot.dev/null app ids, so the
rename lands with zero history to migrate.
tests/test_app_ids.pypins the fold, the ingest points, and the ad
board's vocabulary.
[2.1.0] - 2026-07-31
Changed — one site identity, everywhere (review finding F2)
2plot.ai's directory advertises this host as "the developer wing of the network"; this host's own /llms.txt opened "# Pip Install Python Documentation … Welcome to my notebook". Explicit identity now beats fallback — the same principle dash-improve-my-llms 2.3.4's resolve_site_title enforces per page:
- One brand constant in
run.py— **"2plot.dev — the 2plot network's
developer wing"** — feeds Dash(title=), the home page's register_page_metadata (which 2.3.4 promotes to the /llms.txt H1, og:title and schema.org name), and the opening of pages/home.md.
- "Pip Install Python" stays as the maintainer byline in the prose, not the
site's name.
tests/test_site_identity.pyandscripts/network_smoke.pypin every
surface to the constant, so no surface can drift back.
Added — the network's internal-traffic convention
Adopted the analytics point of truth (https://2plot.ai/docs/satellite-analytics, "Internal traffic"): network machinery talking to itself is counted nowhere.
lib/constants.pymirrors the network-wideINTERNAL_UA/
INTERNAL_UA_TOKEN ("2plot-internal").
- Both trackers drop token-carrying requests at write time, BEFORE bot
classification: lib/traffic_reporter.record_hit (the ledger behind the signed hourly rollups to 2plot.ai — internal hits can no longer inflate human_hits/bot_hits) and lib/analytics_tracker.track_visit (the visitor file behind /analytics). /healthz probes are never a visit either.
- The hourly rollup POST itself now sends the internal UA, so the hub's
4×-daily heartbeat and this host's own reporting stay invisible to every ledger in the network.
Added — CI raised to the network's security standard
The Docker-gate core (build Render's exact image, boot it secretless, prove the surfaces) is the template the standard grew from; the rest now matches 2plot.ai's reference pipeline:
- In-process pytest suite (
tests/, zero secrets, state in tmp): site
identity, hidden/admin documents 404, robots OAI-SearchBot fingerprint, sitemap host + no admin leaks, /healthz, the network bulletin payload shape, doc-key mint→verify round trip plus tamper rejection, hub-auth API failing closed (503) without its secret, and the internal-traffic exclusions.
scripts/network_smoke.py— the same named-check battery in CI and
against production (stdlib-only, case-insensitive response headers), so the two failure modes read identically.
- Workflow hardening:
permissions: contents: read, per-ref concurrency
with cancel-in-progress, timeout-minutes on every job, buildx type=gha layer caching.
- Fingerprints inside the image: dash 4.4.x, dash-improve-my-llms
≥ 2.3.4 (was ≥ 2.3.2), and gunicorn ≥ 23 — the image resolves 26.0.0 today; the assert exists because markdown2dash installs --no-deps to dodge its spurious gunicorn<22 pin, and this is what proves the dodge keeps holding against the request-smuggling advisories fixed in 22+.
.github/dependabot.yml(weekly pip with adash-networkgroup,
monthly actions) and an advisory pip-audit job.
- CD lessons ported back from the boilerplate rollout (the template
shipped first and surfaced the pitfalls): an actionlint job — a double quote inside a ${{ }} expression is a lex error that invalidates the whole workflow file, and such runs die with zero jobs, so there is nothing to click and the failure is silent; the image fingerprint step now imports markdown2dash, proving --no-deps skipped only its dependency graph and not the package; and the deploy-hook job is replaced by verify-live — on a push to main (the branch Render auto-deploys), wait for sustained health (five consecutive 200s — Render swaps instances, so a single 200 is the old build talking), then run the same smoke battery against https://2plot.dev, retried across the build window.
- First pip-audit pass actioned: requests 2.32.5 → 2.33.0
(PYSEC-2026-2275; still compatible with the urllib3 1.26 line). The urllib3<2 findings are documented and deliberately deferred — the pin exists because urllib3 2.x broke HTTPS-proxy SSL CONNECT on the QuotaGuard Discord path; lifting it requires a verified proxied discord.com call on urllib3 ≥ 2.7, tracked as a follow-up.
Added — this host is now the network's auth hub
Satellites run Clerk locally, so they resolve a signed-in browser without asking anyone. What they cannot do is validate an agent fetch: it arrives with no cookie carrying ?key=, and only this host holds the secret that key derives from. Three endpoints (lib/network_auth.py), shaped like lib/ad_network.py because that is this host's established satellite-facing pattern:
POST /api/agent-key/verify {key, path, tier, app} -> {verdict, ttl}
POST /api/agent-key/current {token, app} -> {key, scope}
POST /api/page-tiers {app} -> {tiers, ttl}
Satellites therefore hold no key material at all. Sharing SESSION_SECRET would let them verify locally, but anything that can verify can also mint — including scope=admin — and twenty deployments holding a network-wide admin-minting secret is the failure this avoids.
- Callers are authenticated by signature: HMAC-SHA256 over
"{timestamp}." + body with CROSS_APP_WEBHOOK_SECRET, the scheme lib/traffic_reporter.py already uses. Plus a ±300s window (nothing else makes a signed body single-use, so without it a captured request replays forever), constant-time comparison, and 503 when the secret is unset rather than degrading open. Read through _satellite_secret(app_id) so per-satellite secrets later are a registry lookup, not a refactor.
/currentproves the user rather than believing the caller. A shared
secret cannot substantiate "user X is signed in on my site", so a satellite forwards the browser's Clerk session token and this host verifies it against Clerk's JWKS (verify_user_sync, JWKS-verified, cached min(5 min, JWT exp)). A caller-asserted user_id is refused with a message saying why. Without this, a satellite could name any user and be handed their key — attributable to a person who never asked for it, alive up to 30 days.
- Minting through a satellite is capped at
scope=auth, never admin, even
when that user is an admin here. Verification is the lock; the cap is what still holds if the lock is ever misconfigured.
- The hub sets the ceiling. Verdicts resolve
most_restrictive(tier_the_satellite_claims, hub_ceiling(app, path)), applied hub-side as well as in the feed, so a satellite that never reads the feed still cannot loosen anything. An unrecognised tier is "no opinion", never public.
- The tier feed is POST and authenticated, not a public GET: a
hidden
page's path in it discloses a URL meant to be undiscoverable.
- TTLs returned explicitly —
allow 900,gated 60,deny 60. A brief hub
outage must not gate readers who were fine a minute ago; the stated cost is that a revoked key keeps working on a satellite for up to 15 minutes.
2plot.dev/llms.txtstays public, with a regression test. It is the map
every satellite's navigation block points at, and the package deliberately never appends a key to a cross-host link — so gating it would wall off every agent on its first hop outward.
/admin/network-board → Page tiers publishes those ceilings per satellite: pick an app, set auth / admin / hidden on a path, clear to hand the page back. public is deliberately not offered — it would restrict nothing while reading like a decision. Each satellite's row shows when it last pulled the feed, which is the only way to tell a published ceiling has actually landed.
Deployment note: CROSS_APP_WEBHOOK_SECRET is currently set on neither this host nor the satellites. Until it is, every verify answers 503, the client reads that as an outage, and agent access on every satellite stays gated. That is configuration, not code.
Security
- The control board's tiers now gate every surface, not just the page.
A page set to auth was serving its full prose to anyone: 90 KB through /<page>/llms.txt, again in the crawler HTML, again in the prerendered block, and listed in the sitemap. Only the interactive Dash layout was gated, because that was the only surface this app controlled. lib/page_visibility.llms_verdict now answers per request and dash-improve-my-llms consults it on all six surfaces, via the new configure_access hook (2.3.0).
What each setting means is now stated on the control board itself, in a legend, because nothing on that screen previously said the tier stopped at the page.
| tier | llms_public | anonymous | with an agent link | signed in |
|---|---|---|---|---|
public | on | prose | prose | prose |
public | off | 404 | 404 | 404 |
auth | on | prose (unchanged) | prose | prose |
auth | off | gate document | prose | prose |
admin | either | 404 | prose (admin key) | prose (admins) |
hidden | either | 404 | 404 | 404 |
public + off denies everyone including key holders — a public page has no notion of entitlement, so the toggle means "no machine-readable document exists". auth + off stays in the sitemap: the URL is public, the content is not, and de-listing would defeat the sign-in funnel the tier exists for. hidden is absolute; no key opens it.
- Agent links: entitlement that survives the paste. A cookie cannot gate
these documents — the whole point is to hand the URL to Claude or ChatGPT, which fetch it with no session. So a gated document is reachable with ?key=…, and lib/llms_access.py binds that key to the Clerk session that created it:
- created automatically on any authenticated request — there is no mint
button, and the "Copy for llm" button already produces the right URL;
- rotates on re-login — the HMAC covers Clerk's
session_id; - dies on logout —
POST /api/clerk/webhookrevokes onsession.ended
(CLERK_WEBHOOK_SECRET was in .env but nothing consumed it until now);
- capped at 30 days regardless, so an abandoned tab can't keep a link
alive;
- per session, not per user, so a second device doesn't invalidate the
first.
The key is derived (HMAC(SESSION_SECRET, user:session:scope:version)), never stored, so the new llms_access_keys table holds no secret material and a dump of it yields nothing usable — while still supporting revocation and showing last-used as a leak signal. Keyed responses carry Cache-Control: private, no-store and X-Robots-Tag: noindex, and the key reaches no canonical tag, sitemap or published index. Links inside a keyed document carry the key, or an agent handed one authorised URL would hit gate documents one hop later.
- The llms.txt viewer names its reader when signed in — email, plan, and
"session since". HTML variant only: the Markdown that agents and crawlers get from the same URL never contains it. Identity-bearing responses are private, no-store too; the package previously set no Cache-Control at all, so a CDN could have served one visitor's banner to the next.
The timestamp is first-seen-per-session rather than Clerk's iat, which refreshes about every 60 seconds and would have read as a clock resetting every minute.
Fixed
- Every page canonicalised to the homepage.
templates/index.htmlhardcoded
<link rel="canonical" href="https://2plot.dev"> with no path, on all 27 pages — so every documentation page declared itself a duplicate of /, and the only URL asking to be indexed was the homepage. The package emits a correct per-page canonical, so the hardcoded one was also about to become a second, conflicting tag, which search engines resolve by ignoring both. Verified after: exactly one canonical per indexable page, each pointing at its own URL.
- Same bug for
<title>. The template carried a hardcoded title element
before {%title%}, and the first one wins, so all 27 pages shipped the identical title "2plot.dev - Open Source Dash Packages & Documentation". Removed; one title element remains and the package rewrites it per page. Bare og:url / twitter:url went too — Dash Pages and the package both emit per-page versions.
Note for anyone editing that template: the package's title rewrite is a DOTALL regex that does not know what an HTML comment is, so mentioning a title tag in a comment there deletes everything up to the real one. It cost 10,739 characters of head on the first attempt at this fix. Both tags are now described in words, with a warning at the top of the file.
- A custom route was shadowing the package's
/<page>/llms.txt. It was
registered before add_llms_routes and won, so neither of 2.2.0's headline features reached this host: no navigation block (the three lines that stop a page's llms.txt being a dead end for an agent) and no rendered viewer for people. It existed for a real reason — it re-checked llms_accessible() per request so control-board toggles applied immediately, which static mark_hidden() cannot do. That behaviour now lives in lib/page_visibility.py (register_llms_doc / apply_llms_state), which pushes each verdict into the package's own registry instead of intercepting the route. A toggle now moves the 404, the root index and the sitemap entry on the next request, which is more than the old route managed.
warn_missing_llms_doc=Falsewas hiding two stub pages. Turned on;
/changelog now builds its LLMS_DOC from CHANGELOG.md (it renders as a DMC Timeline, which a crawler cannot read) and /404 is mark_hidden — a not-found page has no business in an index. Zero pages now serve the crawler stub, and the startup warning is silent for the right reason.
The call had to move below the prose registrations: it evaluates the warning immediately, and this app registers prose in run.py rather than per page module, so from its old position it named 26 of 27 pages and meant nothing.
- Home page prose, which is now the lead of the network's most-read
document: "their is a chat tab" → "there is", lowercase "ai" → "AI", a dangling "Welcome to my notebook," and a sentence fragment, plus stale install instructions (dash>=3.0, dash-mantine-components>=2.4 — this app needs 4.2 and 2.8) and a "Visit" link to the boilerplate's old Render URL.
Added
- dash-improve-my-llms, vendored rather than from PyPI, landing at 2.3.2
(started as 2.2.0, then 2.3.0 for configure_access, then the 2.3.2 rebuild — two artifacts both claimed 2.3.0 before and after the OAI-SearchBot robots fix, and the bump makes "which build is this host running" answerable from pip metadata; remotely, the fingerprint is robots.txt showing User-agent: OAI-SearchBot → Allow: /). Nothing publishes to PyPI until all four Phase-1 apps are verified in production; Phase 5 reverts requirements.txt to a PyPI pin. Dockerfile already copies vendor/ before pip runs, so no build change was needed.
- Cross-host network directory (
lib/network_directory.py, applied in
run.py before add_llms_routes). Emits <link rel="related"> tags, a ## Network section in /llms.txt, and followed links in the prerendered body — a sitemap cannot cross origins, and an agent fetches one URL rather than crawling from it. Deliberately the same shape as the satellites' copy so the two can be diffed; what differs here is that 2plot.dev is a section hub, so hub_url points one level up at https://2plot.ai while the *.2plot.dev satellites point here. Carries the morse wordmark (2 + morse plot + ai), gated on the installed signature so the same file cannot break a satellite still on 2.0.
Every external / affiliated entry carries an explicit llms_txt, because the package otherwise synthesizes {url}/llms.txt for entries that have no such document — https://dash.plotly.com/llms.txt 404s, and publishing that in a directory whose value is that its links resolve is worse than omitting the entry. Plotly's real one is at https://plotly.com/llms.txt.
GET /api/network/bulletin(lib/network_bulletin.py) — the endpoint
every other satellite in the network reads, so the network can say "here is what changed" once instead of in twenty repositories. Mirrors lib/ad_network.py deliberately: server-to-server, ?app=<id> attribution, _clean() caps on every field, permissive CORS, short Cache-Control, and per-app fetch counters. Caps mirror the client's exactly, so the board's preview is what satellites render and an oversized entry cannot push the response past the client's 64 KB ceiling (at which point it discards the whole document, not just the long field). Storage via the shared Postgres site_settings table with the usual JSON-file fallback; network_bulletin.json is a tracked seed like advertising_config.json.
/admin/network-board— the admin surface that curates it: network
identity, Tips for getting started, and What's new, each entry with an active toggle and optional per-app targeting (empty = every satellite, same convention as a campaign's apps). A copyable NETWORK_BULLETIN_URL plus the configure_bulletin snippet for satellites, a per-satellite fetch table (the only way to tell a satellite is configured rather than merely deployed), and a standing warning that saved text reaches every satellite within one TTL. Same ADMIN_EMAILS gate as the other boards, re-checked server-side in every callback. Lives in the navbar's Admin section (tabler:broadcast).
What's new entries take their date from a DatePickerInput defaulting to today, published as YYYY-MM-DD. The field is free-form in the contract, but every use of it is "when did this land", and a picker is how twenty sites end up agreeing on one format.
The Preview tab renders the real thing, per satellite: pick an app id and the card is drawn by the package's own render_llms_viewer with the exact payload that app would receive, in a locked-down iframe (sandbox=""; the viewer HTML is self-contained, so nothing is lost). That makes per-app targeting visible — an entry aimed at one satellite is demonstrably absent from every other one's card — and it cannot drift from the endpoint, because it is the endpoint's output through the client's own renderer. The JSON payload is shown underneath.
Bulletin app ids live in lib/network_directory.py, split into verified and expected, because they are not the ad network's ids: the boilerplate calls itself boilerplate for the bulletin and dash-documentation-boilerplate for ads. The board badges an id as verified-in-code, expected-but-unconfirmed, or unrecognised, and treats "has actually fetched" as the only ground truth.
/pip— the component catalogue as one page, for people and as one
document an agent can read the whole catalogue from. Generated from the docs' own frontmatter, so a new docs/<component>/<component>.md appears automatically. Its LLMS_DOC is written as absolute Markdown links (2.2.0 renders those as real anchors, so the prose is the link graph), and it re-renders on any visibility change via a new on_visibility_change hook — an index that keeps advertising a page the admin just unpublished is worse than no index. Navbar icon tabler:apps.
- Optional self-rendering of the bulletin, gated on
NETWORK_BULLETIN_URL
(the same variable the satellites use) so the hub can show the same banner it publishes. Off by default: _base_url is the production host, so an unconditional call would have a dev machine reading production's bulletin.
- Every component page now links its own documentation site, in a call-out
under the description and as the first line of the page's llms.txt. Those sites carry the complete API reference and examples with more depth and testing than the quick-start examples rendered here, and saying so is more useful than letting a reader assume this page is everything.
Both come from lib/network_directory.py, not per-page markdown. Four pages previously carried a hand-written *.onrender.com blockquote — four URLs to re-edit at migration, and no link at all for the components that never got one. Those are removed.
- **The directory is built around the
*.onrender.com→*.2plot.dev
migration.** Each satellite carries its subdomain, its current Render URL and a status of live / shipping / planned; one function resolves to whichever answers today, and nothing without a resolvable URL is published anywhere — peer list, rel="related", /pip, or page banners. So pannellum / posprinter / modelviewer are known to the plumbing and advertised nowhere.
On rollout day, set SATELLITE_SUBDOMAINS_LIVE=1 and every shipping entry switches to its subdomain across all four surfaces at once — no code change, no redeploy. Verified in both states. As of today only leaflet.2plot.dev is live, so that is the only subdomain advertised.
- The bulletin board's targeting suggestions come from the directory instead
of a hand-kept list, which had drifted: it offered leaflet where the real id is dash-leaflet2, and omitted llms, dash-emoji-mart and flexlayout-dash. An entry targeted at an id no satellite sends is never delivered and never reports an error — it just silently reaches nobody.
Notes
- Upstream defects found while wiring this, with local workarounds and no
changes to the package: mark_hidden() has no inverse; the prerender's title rewrite is comment-blind; directory entries advertise a synthesized llms.txt. Written up in .claude/knowledge/llms-package-notes.md (local only — .claude/ is not tracked in this repo).
- For the record, since the point of the rollout is the delta: **pages serving
the crawler stub went 2 → 0**, and both were genuinely undocumented pages fixed by hand here. This app never had 2.2.0's prose-erasure bug — nothing loops over dash.page_registry refreshing names after llms_doc is set. Its gains are the canonical, the title, the nav block, the viewer, the root index and the network directory.
- Control Board settings now survive redeploys — page visibility tiers
and llms.txt toggles persist to Postgres (new generic site_settings JSONB key-value table in lib/ad_storage.py, key page_visibility) instead of only the container's ephemeral page_visibility.json. On boot the stored overrides are pulled once the background DB init finishes (reads never block); with no database the JSON file keeps working as before, and existing JSON overrides seed Postgres on first contact. No more re-doing the board after every release.
- 2plot.dev is now installable as an app on phones — the template linked
/assets/site.webmanifest, but the file never existed (404), so browsers never offered "Install app" / Add to Home Screen. Added a real assets/favicon/ bundle (192/512 android-chrome icons, 96/32/16 favicons, apple-touch-icon, site.webmanifest with display: standalone) and pointed the template at it, matching the setup 2plot.media and HoneyComb already had.
Removed
- Old
/analytics/trafficvisitor dashboard (pages/analytics.py) and
its orphaned lib/ad_analytics.py helper — superseded by /admin/ad-analytics. The navbar Analytics section keeps Api Cost; visitor tracking itself is untouched. All admin boards (email, ad, ad-analytics) are now also excluded from the sitemap/llms surface like the control board already was.
Added
/admin/ad-analytics— ad network performance dashboard (admin-gated,
in the admin nav section): KPI cards with sparklines (impressions, clicks, CTR, apps reporting), daily network traffic line chart, impressions stacked by app, impression/click share donuts, per-campaign impressions+clicks and CTR bars, a radial CTR-by-app chart, a campaign-activity timeline (dash-mui-scheduler EventTimeline — the served history day by day), and a top-pages table. 7/30/90-day ranges. Backed by a new get_timeseries aggregation (lib/advertising.py) with a Postgres daily GROUP BY (stats_timeseries in lib/ad_storage.py) so the history is persistent in production, and the JSON fallback in dev; the page footer shows which source is live. Built with dash-mui-charts + dash-mui-scheduler.
- Ad board image uploads to the network CDN — the
/admin/ad-board
campaign form now has a drop zone under the Image link field: drop or pick an image (PNG/JPG/GIF/WebP/AVIF, up to 10 MB) and it uploads to the shared 2plot R2 bucket and comes back as a permanent https://cdn.2plot.ai/ads/… URL, filled into the form and previewed immediately. Ads finally survive redeploys (no more ephemeral /assets/ images) and the same URL works across every app in the ad network. Pasting an external URL still works; deployments without the R2_* env vars keep the paste-a-link flow and the upload zone explains itself instead of failing. (lib/r2_images.py; adds boto3.)
Added — Email system (dash-email + Resend)
- dash-email 0.0.1 docs page (
/pip/dash_email) — quick start, rows/columns,
and button/CTA live examples; added to the home catalogue table and download counter
/admin/email-board— admin control board for transactional and promotional
email: template picker (welcome / announcement / newsletter / plain) with live dash_email preview and generated-HTML view; single, scheduled ("in 1 hour" / ISO), and per-recipient batch sends (100/chunk); recipients as a searchable MultiSelect populated from the Clerk user base (lib/clerk_users.py) with select-all and custom-address entry; durable outbox history (email_outbox.json)
lib/email_provider.py— Resend REST API viarequests(no SDK) +
dash_email component→HTML renderer; verified with a live send from dashboard@2plot.dev
Added — Advertising platform
/admin/ad-board— campaign CRUD from an image link + destination link, with
rotation weights, live preview, active toggles, and per-campaign impressions / clicks / CTR
- Live ad serving — ads are now resolved per page view by a mount callback
(serve_ad) instead of being baked into layouts at startup: board changes apply in production without a restart, and impressions count real views
- Cross-app ad network — satellites on their own domains fetch ads from
GET /api/ad-network/serve (server-to-server, per page view) and beacon clicks to POST /api/ad-network/click (CORS, preflight-free text/plain) with app + page attribution; per-campaign app targeting and a board Performance tab (per-app and per-page CTR); satellite kit lib/ad_client.py (2s timeout, 60s failure circuit breaker) wired into dash-documentation-boilerplate, dash-email, dash-mui-scheduler, dash-flows, and dash-mui-charts
- Postgres persistence (
lib/ad_storage.py) — campaigns + analytics in the
shared 2plot production Render Postgres (AD_DATABASE_URL/DATABASE_URL), auto-DDL, one-time JSON migration, JSON-file fallback in dev
Added — Navigation & branding
- Admin nav section — Control Board / Email Board / Ad Board moved out of
Components into an admin-only section, rendered per request against the ADMIN_EMAILS allowlist (links never sent to non-admins)
- README.md — project README in the catalogue-standard format
Changed
- 2plot.dev rebrand of
templates/index.html(titles, og:site_name, noscript);
YouTube references now @2plotai
- PyPI cleanup — dash-email 0.0.1, dash-nle-timeline 0.0.1, and
dash-mui-scheduler 0.1.0 now install from PyPI (vendor archives removed); dash-mui-charts / flexlayout-dash / dash-excalidraw stay vendored while PyPI lags the deployed versions
- Docs cross-links — dash-email, dash-mui-scheduler, dash-mui-charts,
dash-flows, and dash-nle-timeline pages now open with a link to their dedicated deep-dive docs site plus GitHub/PyPI links
- Header — "Other Apps" menu hidden below the
mdbreakpoint so the header
fits on small screens
.claude/removed from the repository (now gitignored)
Fixed
- Corrupt
advertising_analytics.json(truncated write) that had been failing
every impression/click log — repaired (190 impressions / 4 clicks salvaged) and the loader now auto-recovers with a .corrupt backup
- dash-email docs examples in dark mode — all example text now carries explicit
email-safe colors, plus a [data-email-component] CSS safety net so email previews always render as light artifacts
[2.0.0] - 2026-07-07
Changed — Core Framework Upgrade
- Dash 3.3.0 → 4.4.0 — removed stale
_dash_rendererimports (DMC 2.7+ no longer
needs _set_react_version); health_endpoint, app.setup_apis(), and Dash hooks all carried forward unchanged
- dash-mantine-components 2.4.0 → 2.8.0 (Mantine 8.3.x) — no breaking API changes
- dash-improve-my-llms 0.3.0 → 2.0.0 (
[flask]extra now required) — adopted the
LLMS_DOC pattern: every docs page's processed markdown is registered via register_page_metadata(llms_doc=…), powering bot-detection prerendering now and dash.mcp resources when we reach Dash 4.3+ MCP; the package's /page.json, /architecture.txt, and TOON endpoints were removed upstream (our own /<page>/llms.txt and /<page>/page.json routes are unaffected)
- flexlayout-dash 1.0.0 → 1.1.0 — BREAKING import rename
dash_flex_layout→
flexlayout_dash; docs folder renamed to docs/flexlayout_dash/ (endpoint URL /pip/dash_flex_layout unchanged)
- dash-pannellum → 0.4.0 — docs now cover
lookAtcamera control, per-name
callbackHotspots diffing, and the 0.4.0 gyroscope look-around (orientation / orientationSupported / orientationActive)
- dash-excalidraw → 0.1.0 — docs rewritten for the command/lastExport imperative
API and the JSON-safe prop surface (38-prop table regenerated from docstrings)
- dash-mui-charts → 1.3.0, dash-flows → 1.3.0, dash-ag-grid → 35.x
Added — New Component Documentation
- dash-mui-scheduler 0.1.0 (
/pip/dash_mui_scheduler) — MUI X Scheduler event
calendar/timeline; replaces dash-fullcalendar
- dash-leaflet2 0.1.0 (
/pip/dash_leaflet2) — Leaflet 2-native map components
(admin-only visibility by default)
- dash-nle-timeline 0.0.1 (
/pip/dash_nle_timeline) — non-linear-editor timeline +
scene compositor, including the "Python owns clips / apply lastEdit" pattern
Added — Authentication & Access Control (Clerk)
- dash-clerk-auth 0.9.0 integration:
register_clerk_auth()beforeDash()
(headless mode), configure_app() after; account menu embedded in the header
- Four-tier page visibility (
lib/page_visibility.py):public/auth/
admin (ADMIN_EMAILS/ADMIN_USER_IDS allowlist) / hidden, defaulting from docs frontmatter (visibility:, llms_public:) and resolved per request — component docs default to auth so the sign-in card drives account creation
- Admin Control Board (
/admin/control-board) — live toggles for every docs page's
visibility tier and llms.txt exposure, persisted to page_visibility.json, applied without restart
- llms.txt / page.json routes respect visibility: hidden pages 404, admin-tier content
is never served to anonymous or crawler traffic, llms_public is toggleable per page
- Graceful degradation: without Clerk keys the site runs fully open (dev mode) with a
startup warning
Removed
- dash-fullcalendar — docs removed, package moved to the Archived catalogue
(superseded by dash-mui-scheduler)
Infrastructure
vendor/wheels for not-yet-published packages (flexlayout-dash 1.1.0,
dash-mui-charts 1.3.0, dash-mui-scheduler 0.1.0, dash-excalidraw 0.1.0, dash-leaflet2 0.1.0, dash-nle-timeline 0.0.1, dash-clerk-auth 0.9.0) — see .claude/tasks/pypi-publish-checklist.md
- "Other Apps" header menu → piratesbargain.com, ai-agent.buzz, 2plot.ai, 2plot.media,
2plot.xyz
- Home catalogue: 17 maintained / 8 archived packages; download counter tracks the new
packages
[1.2.0] - 2026-04-08
Added
Discord Integration — dash-widgetbot v0.4.1
- Global Discord Crate widget embedded on every page via
add_discord_crate()inrun.py - Custom floating action button replacing the default Crate button (
assets/widgetbot_fix.js) - Slash command bridge:
/ask,/ai,/gen,/status,/navigatecommands registered and dispatched throughcallbacks/discord_handlers.py - Crate text bridge — intercepts free-text
/cmdmessages from WidgetBot and routes to bot handlers - Bot bridge HTTP endpoint (
/api/bot-bridge-prompt) serving AI-assisted responses with agent avatar - Discord interactions endpoint (
/api/discord/interactions) with guild command registration lib/bot_bridge.py— streaming Gemini AI integration for Discord bot responses- New docs page at
/pip/dash_widgetbot— comprehensive guide covering Crate setup, slash commands, bridge callbacks, and functions-as-props event handling
dash-flows v1.2.0 Feature Documentation
- Sub-flows (Collapsible Groups) —
docs/dash_flows/subflows.pywith working double-click collapse viatoggleCollapseNode/collapsedGroupscallback - Smart Handle Positioning —
docs/dash_flows/smart_handles.py— edges auto-route to the closest node side - Floating Edges —
docs/dash_flows/floating_edges.py— edges that connect to node perimeters - Helper Lines —
docs/dash_flows/helper_lines.py— alignment guides when dragging nodes - Undo / Redo —
docs/dash_flows/undo_redo.py— history tracking withenableUndoRedo,undoRedoAction,undoRedoState - Add Node on Edge Drop —
docs/dash_flows/add_node_drop.py - Animated Layout Transitions —
docs/dash_flows/animated_layout.py - Computing Flows —
docs/dash_flows/computing_flows.py— topological sort and data propagation - Resize Constraints —
docs/dash_flows/resize_constraints.py— aspect ratio lock, min/max dimensions - Accessibility (ARIA) —
docs/dash_flows/accessibility.py— screen reader labels and keyboard navigation - Viewport Portal —
docs/dash_flows/viewport_portal.py— floating annotations at flow coordinates - Custom Icons —
docs/dash_flows/custom_icons.py— DashIconify integration with stacked/horizontal layouts
dash-mui-charts v1.1.0 Upgrade
- LiveTradingChart — new component: real-time OHLCV candlestick streaming with volume histogram, forecast line with uncertainty bands, and swing-point alert labels
- Tick Configuration & Date Formatting — new
dateFormat/dateTickFormatprops on time-scale axes; no JavaScript required for formatted date labels - Functions-as-props pattern —
alertFormatterandalertFilterprops accepting{function, options}dicts resolved fromwindow.dashMuiChartsFunctions assets/muiChartsFunctions.js— JS registry providingpriceAlertFormatter(▲/▼ price labels) andswingAlertFilter(configurable lookback-based swing detection)- New docs section Tick Configuration & Date Formatting in
docs/dash_mui_charts/tick_hover.py— time-scale axis, angled labels, reference lines, click-event capture - New docs section Live Trading Chart in
docs/dash_mui_charts/live_trading.py— full interactive demo with Start/Stop/Reset, Speed/Volatility/Drift/Window sliders, stats readout, and alert history log
New Layout Manager Documentation
docs/dash_dock_view/— comprehensive docs fordash-dock-viewdrag-and-drop panel systemdocs/dash_flex_layout/— comprehensive docs forflexlayout-dashmulti-panel tabbed layoutdocs/dash_flows/— full v1.2.0 documentation suite (12 example files)
Changed
dash-mui-chartsalert tuning — reduced defaultalertProbabilityto0.03and raisedalertThresholdPctto3.0in the docs example to keep labels sparse and meaningful- Navbar — Changelog link moved out of the Components accordion; now appears as a top-level link between Index and Docs Boilerplate in both desktop and mobile layouts
requirements.txt— upgradeddash-mui-charts>=1.0.0→dash-mui-charts==1.1.0
Fixed
dash-flowssub-flows collapse —docs/dash_flows/subflows.pywas missing the callback connectingdoubleClickedNode→toggleCollapseNode; double-clicking a group node now correctly collapses and expands itdash-flowsReact Flow height error — component renders correctly inside dock tabs; error was cosmetic (fired before container had visible dimensions) and resolves on scroll
[1.1.0] - 2026-01-05
Added
- Liquid Glass Theme Showcases
- Created isolated liquid glass design example for
dash_dock_view/themes_example.pywith Apple WWDC 2025 inspired glassmorphism - Created liquid glass theme example for
dash_flex_layout/theming_example.pywith 3-panel layout and theme switching - Inline style definitions for backdrop blur, saturation, translucency effects
- Dynamic theme switching between light and dark modes with helper functions
- 5 themed panels in dock example, 3 themed panels in flex layout example
- Professional gradient overlays and smooth transitions
Changed
- Component Migration to DMC
- Replaced all
dash.htmlcomponents withdash_mantine_components(DMC) in dash_flex_layout examples - Migrated
ide_layout.py,callbacks_example.py, andtheming_example.pyto use DMC components - Updated HTML elements:
html.Div→dmc.Box,html.H4→dmc.Title,html.Pre→dmc.Code,html.P→dmc.Text - Improved component theming consistency across light and dark modes
Fixed
- Theme-Aware Component Styling
- Fixed dmc.Paper components staying white in dark mode in dash_flows examples
- Removed hardcoded
bg="gray.0"props frombasic_nodes_edges.py(2 instances) andnode_interactions.py(3 instances) - Papers now properly respect Mantine's automatic light/dark mode theming
- Fixed SegmentedControl labels to use emoji strings instead of DashIconify components (prevents JavaScript errors)
- Removed invalid
heightprop usage on DashFlexLayout components
- Layout and Sizing Issues
- Added
style={"height": "100%", "width": "100%"}to all DashFlexLayout instances - Fixed components not filling their containers in 5 files:
introduction.py,two_panel_layout.py,ide_layout.py,callbacks_example.py,theming_example.py - Replaced
html.Divwrappers withdmc.Boxfor proper sizing behavior - Ensured all examples properly utilize 500px/450px/400px container heights
Documentation
- Code Quality Improvements
- Established rule: Never use
html.Style(doesn't exist in Dash) - Standardized on inline Python style dictionaries for isolated component styling
- Added comprehensive comments to liquid glass style definitions
- Improved code examples with theme-aware patterns
[1.0.0] - 2025-11-16
Initial Production Release
Production-ready documentation platform for Pip Install Python's custom Dash components and Python packages, now live at https://pip-install-python.com
🎯 Core Features
📚 Documentation System
- Markdown-Driven Content
- Write documentation in Markdown with Python integration
- Frontmatter metadata for page configuration
- Custom directive system for interactive examples
- Automatic page generation and routing
- Table of contents generation
- Custom Markdown Directives
.. toc::- Automatic table of contents from headings.. exec::module.path- Render executable Python components.. source::file/path.py- Display source code with syntax highlighting.. sourcetabs::module.path- Tabbed source code display.. kwargs::ComponentName- Component props documentation tables.. llms_copy::- Copy llms.txt URL for AI assistant sharing
- 18 Custom Component Libraries Documented
- Currently maintained: dash-summernote, dash-insta-stories, dash-image-gallery, dash-fullcalendar, dash-gauge, dash-emoji-mart, dash-dock, dash-pannellum, dash-planet, dash-model-viewer, dash-excalidraw
- Archived: dash-credit-cards, dash-charty, dash-nivo, dash-discord, dash-dynamic-grid-layout, dash-swiper, dash-fullcalendar
- Each with comprehensive guides, live examples, and download statistics
🎨 Modern UI/UX
- Responsive Design
- Mobile, tablet, and desktop optimized layouts
- Adaptive navigation with hamburger menu on mobile
- Responsive AppShell with collapsible sidebar
- Custom breakpoints for optimal viewing
- Theme System
- Light and dark mode with automatic persistence
- Browser preference detection on first visit
- Smooth theme transitions without page flash
- Theme-aware Plotly charts with DMC figure templates
- Professional typography with Inter font family
- Systematic 4px-based spacing scale
- 5-level shadow system for depth
- Code block theming with proper syntax highlighting
- Navigation & Search
- Custom page ordering and organization
- Searchable component navigation
- Icon-based visual hierarchy
- "Other Apps" menu for quick access to related projects
- Breadcrumb-style organization
📊 Analytics & Tracking
- Visitor Analytics System
- Session-based tracking using MD5 hash of IP + user agent
- Device type detection (desktop, mobile, tablet, bot)
- Bot type classification (training, search, traditional)
- Geolocation tracking with ip-api.com integration
- Unique visitor counting (sessions, not page views)
- First-visit-only location data to avoid duplicates
- Privacy-conscious tracking with session IDs
- Race condition handling for concurrent file access
- JSON data persistence with error recovery
- Analytics Dashboard (
/analytics/traffic) - Real-time visitor statistics (unique visitors by device type)
- Interactive geolocation bubble map with Plotly
- Device breakdown visualization (desktop/mobile/tablet/bot)
- Traffic by page with bar charts
- Hourly traffic patterns with line charts
- Browser detection and analytics
- Comprehensive visitor metrics and insights
- PyPI Download Statistics
- Automatic aggregation across all 18 packages
- Real-time download counters from pypistats.org API
- Visual presentation on home page
- Smart error handling and caching
- Historical data tracking
🤖 AI Integration & Chat
- Page-Level AI Chat
- Context-aware AI assistant on every documentation page
- Streaming SSE responses for real-time interaction
- Page context injection (markdown content, code examples, metadata)
- Session-based conversation tracking
- Multiple response formats (markdown, code)
- Chat history and analytics tracking
- Cost and token usage monitoring
- Error handling and graceful degradation
- AI Analytics Dashboard (
/analytics) - Cost breakdown by model (Claude Opus, Sonnet, Haiku)
- Total API costs and token usage tracking
- Questions per page visualization
- Recent user questions with interactive modal details
- Chat log viewing with formatted Q&A display
- Time-based filtering and analysis
- Cost efficiency metrics
- AI Chat Modal System
- Row selection in dash-ag-grid tables
- Modal display of complete chat interactions
- Formatted question and response sections
- Metadata badges (timestamp, page, model, cost, tokens)
- Markdown rendering for responses
- Clean, professional UI with DMC Paper components
🔍 SEO & LLM Integration
- dash-improve-my-llms Integration (v0.3.0)
- Automatic llms.txt generation for AI understanding
- page.json with technical architecture details
- architecture.txt with ASCII art overview
- robots.txt with intelligent bot management
- sitemap.xml with smart priority inference
- Schema.org structured data for search engines
- Custom page-specific llms.txt routes
- Source directive processing for complete context
- Bot Management
- Blocks AI training bots (GPTBot, CCBot, anthropic-ai)
- Allows AI search bots (ChatGPT-User, ClaudeBot, PerplexityBot)
- Allows traditional search engines (Googlebot, Bingbot)
- Configurable crawl delays and disallowed paths
- Privacy controls with mark_hidden() for sensitive pages
- SEO Optimization
- Google Analytics integration (G-6WYY9JHMP2)
- Minimal, optimized HTML template
- Fast page load times
- Mobile-friendly design
- Proper meta tag configuration
- Structured data for rich search results
🐋 Production Infrastructure
- Technology Stack
- Dash 3.2.0 - Modern Plotly Dash framework
- Dash Mantine Components 2.4.0 - Beautiful React UI
- Mantine 8.3.6 - Latest Mantine design system
- React 18.2.0 - Modern React features
- Python 3.11+ - Latest Python capabilities
- Flask 3.1.2 - Production web server
- Plotly 6.4.0 - Interactive visualizations
- Deployment Ready
- Docker and docker-compose support
- Gunicorn production server configuration
- Environment-based configuration
- Error handling and logging
- Graceful degradation for missing data
- Production URL configuration (pip-install-python.com)
- Performance Optimizations
- Fast file pattern matching with Glob
- Efficient JSON data persistence
- LRU caching for geolocation lookups
- Optimized font loading (Inter, JetBrains Mono)
- CSS optimization and minification
- Chart template caching
- Smart skip paths for analytics tracking
🎯 Developer Experience
- Code Quality
- PEP 8 compliant Python code
- Comprehensive inline comments
- Type hints where appropriate
- Modular, reusable components
- Clean separation of concerns
- Error handling throughout
- Documentation Quality
- 5 comprehensive guides with 15+ examples
- Live interactive code demonstrations
- Best practices and patterns
- Troubleshooting guides
- Migration documentation
- API references
- Development Tools
- Hot reload during development
- Debug mode support
- Comprehensive error messages
- Development server on port 8502
- Environment variable support
📁 Project Structure
pip-docs/
├── assets/ # Static assets
│ ├── main.css # Custom styles (theme-aware)
│ ├── m2d.css # Markdown styling
│ ├── chat.js # AI chat client
│ ├── llms_copy.js # LLM copy button handler
│ ├── model_viewer_*.js # 3D model viewer scripts
│ └── [images, icons, etc.]
│
├── callbacks/ # Dash callbacks
│ └── chat_callbacks.py # AI chat functionality
│
├── components/ # UI components
│ ├── appshell.py # Main layout with MantineProvider
│ ├── header.py # Header with search and theme toggle
│ └── navbar.py # Navigation sidebar (custom ordering)
│
├── docs/ # Documentation content
│ ├── dash_*/ # Component-specific docs (18 packages)
│ └── [markdown files + Python examples]
│
├── lib/ # Utility libraries
│ ├── analytics_tracker.py # Visitor analytics system
│ ├── constants.py # App-wide constants
│ ├── directives/ # Custom markdown directives
│ └── page_chat/ # AI chat context gathering
│
├── pages/ # Dash pages
│ ├── home.py # Home page with download stats
│ ├── markdown.py # Dynamic markdown loader
│ ├── analytics.py # Visitor analytics dashboard
│ ├── api_analytics.py # AI chat analytics dashboard
│ └── not_found_404.py # Custom 404 page
│
├── templates/
│ └── index.html # Production HTML template
│
├── CHANGELOG.md # This file
├── README.md # Project documentation
├── CLAUDE.md # AI development notes
├── requirements.txt # Python dependencies
├── package.json # Node.js dependencies
├── run.py # Application entry point
└── visitor_analytics.json # Analytics data store
🔧 Configuration
Environment Variables
DASH_DEBUG- Enable debug mode (default: False)DASH_HOST- Server host (default: 0.0.0.0)DASH_PORT- Server port (default: 8502)ANTHROPIC_API_KEY- API key for AI chat (required for chat features)
Key Configuration Points
run.py:44- Base URL for SEO (set to https://2plot.dev)run.py:47-53- Bot management policieslib/constants.py- App-wide constants and themingtemplates/index.html- Google Analytics and meta tagscomponents/appshell.py- Theme configuration and MantineProvider settings
📊 Analytics Features Breakdown
Session-Based Tracking
- Unique session ID generated from IP address + user agent (MD5 hash)
- Prevents duplicate counting of same visitor across pages
- First-visit-only geolocation to reduce API calls
- Consistent localhost location assignment for development testing
Visitor Analytics
- Device Detection: Desktop, mobile, tablet, bot classification
- Bot Classification: Training bots, search bots, traditional crawlers
- Geolocation: City, region, country with coordinates
- Privacy: No PII stored, only hashed session IDs
- Performance: Skip tracking for assets (.css, .js, images, Dash internals)
AI Chat Analytics
- Cost Tracking: Per-model cost breakdown (Opus $15/$75, Sonnet $3/$15, Haiku $0.25/$1.25)
- Token Usage: Input and output token counting
- Question Analysis: Track questions per page
- Response Logging: Complete chat history with timestamps
- Modal Details: Interactive row selection for full chat logs
🚀 Getting Started
Installation
# Clone repository
git clone https://github.com/pip-install-python/pip-docs.git
cd pip-docs
# Install Python dependencies
pip install -r requirements.txt
# Install Node dependencies
npm install
# Run development server
python run.py
Access Points
- Main Application: http://localhost:8502
- Visitor Analytics: http://localhost:8502/analytics/traffic
- AI Chat Analytics: http://localhost:8502/analytics
- 404 Page: http://localhost:8502/404
Production Deployment
# Build Docker image
docker build -t pip-docs .
# Run with Docker Compose
docker-compose up -d
# Access at port 8550
🎯 Future Enhancements
Planned Features
- Extended analytics dashboard with more visualizations
- User authentication and personalized experiences
- Advanced search with full-text indexing
- Version switcher for documentation
- Code playground/sandbox for testing components
- Automated screenshot generation for examples
- More chart types (heatmaps, 3D plots, geographic maps)
- Export analytics to CSV/JSON
- Real-time visitor tracking with WebSocket
- A/B testing framework for documentation
Under Consideration
- Multi-language support (i18n)
- Community contributions and voting system
- Interactive tutorials and walkthroughs
- Video tutorials integration
- RSS feed for updates
- Newsletter integration
- Component comparison tools
- Performance benchmarking suite
🙏 Acknowledgments
Built With
- Plotly Dash - Web framework
- Dash Mantine Components - UI components
- Mantine - React component library
- dash-improve-my-llms - AI/SEO integration
- dash-ag-grid - Advanced data grids
- Plotly - Interactive visualizations
- Anthropic Claude - AI chat capabilities
Special Thanks
- Plotly Dash team for the amazing framework
- Snehil Vijay for Dash Mantine Components & Ann Marie W for maintaining DMC
- The many open-source community members for continuous support
- All contributors to the custom Dash components
📝 License
This project is licensed under the MIT License - see the LICENSE file for details.
📞 Support & Community
Get Help
- GitHub Issues: Report bugs or request features
- GitHub Discussions: Ask questions and share ideas
- Dash Community: Plotly Community Forum
- Discord: Join the Dash Discord
Stay Connected
- Website: https://2plot.dev
- GitHub: @pip-install-python
- YouTube: Pip Install Python
- 2plot.ai: https://plotly.pro
- ai-agent.buzz: https://ai-agent.buzz
Made with ❤️ by Pip Install Python LLC
Star this repo if you find it useful! ⭐
[1.1.0]: https://github.com/pip-install-python/pip-docs/releases/tag/v1.1.0 [1.0.0]: https://github.com/pip-install-python/pip-docs/releases/tag/v1.0.0