# Changelog

> Version history for the Pip Install Python documentation site (2plot.dev) — the docs framework, the component catalogue, and the site's own infrastructure.

**Site index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.ai/llms.txt](https://2plot.ai/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 11 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://2plot.dev/sitemap.xml  


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](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [Unreleased]

### Fixed
- 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.txt` pointed 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-auth` 0.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`, `flows` and `emojimart` promoted
  `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](https://llms.2plot.dev) and
  [boilerplate.2plot.dev](https://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`).
- `/pip` component 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: `flexlayout` and `email` entries promoted to `live` —
  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_IDS` grew `leaflet`, `email` and `flexlayout` — 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_page` now passes `image_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.html` declares ONLY what Dash omits: `og:site_name` and
  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:title` no longer says "Dash Pip Components": `PAGE_TITLE_PREFIX` is
  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's `app_id` IS its `key`; 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.py` pins 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.py` and `scripts/network_smoke.py` pin 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.py` mirrors the network-wide `INTERNAL_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 a `dash-network` group,
  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.
- **`/current` proves 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.txt` stays 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/webhook` revokes on `session.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.html` hardcoded
  `<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=False` was 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/traffic` visitor 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 via `requests` (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 `md` breakpoint 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_renderer` imports (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 `lookAt` camera 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()` before `Dash()`
  (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()` in `run.py`
- Custom floating action button replacing the default Crate button (`assets/widgetbot_fix.js`)
- Slash command bridge: `/ask`, `/ai`, `/gen`, `/status`, `/navigate` commands registered and dispatched through `callbacks/discord_handlers.py`
- Crate text bridge — intercepts free-text `/cmd` messages 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.py` with working double-click collapse via `toggleCollapseNode` / `collapsedGroups` callback
- **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 with `enableUndoRedo`, `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` / `dateTickFormat` props on time-scale axes; no JavaScript required for formatted date labels
- **Functions-as-props pattern** — `alertFormatter` and `alertFilter` props accepting `{function, options}` dicts resolved from `window.dashMuiChartsFunctions`
- `assets/muiChartsFunctions.js` — JS registry providing `priceAlertFormatter` (▲/▼ price labels) and `swingAlertFilter` (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 for `dash-dock-view` drag-and-drop panel system
- `docs/dash_flex_layout/` — comprehensive docs for `flexlayout-dash` multi-panel tabbed layout
- `docs/dash_flows/` — full v1.2.0 documentation suite (12 example files)

### Changed

- **`dash-mui-charts` alert tuning** — reduced default `alertProbability` to `0.03` and raised `alertThresholdPct` to `3.0` in 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`** — upgraded `dash-mui-charts>=1.0.0` → `dash-mui-charts==1.1.0`

### Fixed

- **`dash-flows` sub-flows collapse** — `docs/dash_flows/subflows.py` was missing the callback connecting `doubleClickedNode` → `toggleCollapseNode`; double-clicking a group node now correctly collapses and expands it
- **`dash-flows` React 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.py` with Apple WWDC 2025 inspired glassmorphism
  - Created liquid glass theme example for `dash_flex_layout/theming_example.py` with 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.html` components with `dash_mantine_components` (DMC) in dash_flex_layout examples
  - Migrated `ide_layout.py`, `callbacks_example.py`, and `theming_example.py` to 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 from `basic_nodes_edges.py` (2 instances) and `node_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 `height` prop 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.Div` wrappers with `dmc.Box` for 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 policies
- `lib/constants.py` - App-wide constants and theming
- `templates/index.html` - Google Analytics and meta tags
- `components/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
```bash
# 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
```bash
# 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](https://dash.plotly.com/) - Web framework
- [Dash Mantine Components](https://dash-mantine-components.com/) - UI components
- [Mantine](https://mantine.dev/) - React component library
- [dash-improve-my-llms](https://pypi.org/project/dash-improve-my-llms/) - AI/SEO integration
- [dash-ag-grid](https://dash.plotly.com/dash-ag-grid) - Advanced data grids
- [Plotly](https://plotly.com/) - Interactive visualizations
- [Anthropic Claude](https://anthropic.com/) - 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](https://github.com/pip-install-python/pip-docs/issues)
- **GitHub Discussions**: [Ask questions and share ideas](https://github.com/pip-install-python/pip-docs/discussions)
- **Dash Community**: [Plotly Community Forum](https://community.plotly.com/)
- **Discord**: [Join the Dash Discord](https://discord.gg/uwQ2f3KCad)

### Stay Connected
- **Website**: https://2plot.dev
- **GitHub**: [@pip-install-python](https://github.com/pip-install-python)
- **YouTube**: [Pip Install Python](https://youtube.com/@PipInstallPython)
- **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
