Changelog
Version history of dash-pannellum. The timeline on this page is rendered from
CHANGELOG.md, reproduced below.
Unreleased
The documentation site. The dash-pannellum package is unchanged; everything here is pannellum.2plot.dev. (Site-only work has not been changelogged since 2.0.0; this section starts doing so, because the first entry below changes numbers the hub already publishes.)
Changed
- The traffic numbers move, and the new ones are the true ones.
The floor is now dash-improve-my-llms>=2.8.0 and lib/analytics_tracker.py no longer carries its own User-Agent list — it delegates to the package's classify(), the same vendor registry /robots.txt is rendered from. Consequence on adoption day: human_hits DROPS and bot_hits RISES. UA-less and library clients (httpx, Go-http-client, node-fetch, an empty User-Agent) move from the human lane to the crawler lane, and ClaudeBot is filed as training rather than search — it is Anthropic's training crawler, which this repo's own run.py comment had said for months six lines from where the list ignored it. The hub's day-over-day view will show a step. That is the number becoming true, not a regression.
- Training crawlers are allowed.
block_ai_training=False: robots.txt
no longer Disallows GPTBot, ClaudeBot or CCBot, and the package's middleware stops answering 403 on the browser document and /healthz for them. The wall decided by vendor CLASS what nobody could account for; now every corpus read is a ledger row and the tool is per-vendor policy — block or meter ONE vendor whose rows justify it, never the class. Measured on this host before the flip: both UAs got 403 on / and /healthz, 200 on /llms.txt. Every one of those 403s was this app's own middleware; there is no edge rule.
- The top bar's GitHub icon pointed at a 404.
GITHUB_URLand the
header both spelled the repo dash-pannellum; it is dash_pannellum. The dead spelling was also in the JSON-LD codeRepository, the llms-github-repo meta tag and the home page. All fixed. package.json still carries it — that is the package lane and needs a rebuild.
- The wordmark is
dash-pannellum. It still said "Dash Pannellum" —
the one surface the 2.0.0 identity pass missed.
Added
- The read table. dash-improve-my-llms 2.8.0's
on_document_read
hands the app one row per corpus document it serves — tier, verdict, bytes, vendor, verification — and AnalyticsTracker.record_read keeps it as a reads list in the same ledger file as visits, with the same buffer, lock, flush cadence and retention. reads is joined by the rollup, never summed into human_hits / bot_hits / pages. client_ip is dropped unless ANALYTICS_KEEP_CLIENT_IP=1.
- Rollup v4, additive and present only on a day that had reads:
vendors[] (one row per vendor × verification × policy, with a per-tier breakdown) and reads. Every v3 key is byte-identical.
/admin/traffic— vendor × day, vendor → tier, and top paths per
vendor for the picked day, behind the control board's exact gate and failing closed the same way. This is NOT the /analytics/traffic dashboard removed on 2026-08-02: that page was a second, staler view of the visits the hub already folds. This one shows the read table, which did not exist then, and answers a question the hub cannot — "does ClaudeBot actually get the corpus from THIS host?" Plain tables, no charts, no interval callback.
- The navigation contract. The sidebar is built from one registry, not
a hand-edited list: Home · Changelog → the app's own sections from each page's frontmatter category: + order: → API → Resources → Admin (owner-only, absent from the tree for everyone else). page_order and excluded_links are gone from components/navbar.py, which no longer holds anything this fork edits. The sections are Getting started · Panoramas · Interaction.
/changelog— this file as a Timeline, and as its ownllms.txt.- Retired from the sidebar:
pip-install-python.com(the domain is
retired), community.plotly.com, and the pre-network "Other Apps I've built" block (Plotly.pro, GeoMapIndex), which was hand-typed and had drifted. Resources is now third-party only — dmc and Pannellum, the upstream project this component wraps. The network is listed once, in the top bar's Other Apps menu, generated from lib/network_directory.
- A footer — © Pip Install Python LLC, the GitHub profile, Discord,
YouTube. Every icon labelled.
- Top bar: a
v2.0.0badge for the documented package, the Other Apps
menu, and search fed from the same registry the sidebar uses (so it can never list an admin or hidden page).
- Accessibility: the desktop Burger, the search field, the home link
and the code-block copy button all have accessible names. The audit found each of them nameless.
- Mobile fit: prop tables, code blocks, Timeline rows and long inline
code scroll or wrap inside their own box instead of dragging the whole document sideways at phone width.
/admin/trafficgains admc.DatePickerInput(bounded by the
ledger's own days, with presets) in place of a dcc.Dropdown, and a People section carrying the day's human numbers above the crawler tables — humans never enter the read ledger, and the page now says so.
releaseis the deploy branch. Render auto-deploysrelease,
and only CD's deploy job writes it: a fast-forward push of the run's own sha after the CI matrix is green. A push to main is a candidate, not a deploy, and main ahead of release means an uncertified push is pending. The Render deploy-hook step is gone from cd.yml; verify now runs only on a successful deploy and asserts /healthz build == github.sha itself, so a verify can no longer pass green against the previous build.
2.0.0 — 2026-08-02
The revival ships to PyPI — the first published release since 0.0.6. Everything from 0.1.0 through 0.4.0 below lands in this version; the number leaps to 2.0.0 to mark the break: Dash 4.2+ / React 18 required, and the 0.x line belongs to the original Dash 1/2-era component.
pannellum.2plot.dev joins the 2plot network. The component itself is unchanged since 0.4.0 (packaging metadata modernized: SPDX license expression, setuptools>=77); the rest of this entry is the documentation site reaching the network standard proven on 2plot.ai, 2plot.dev, boilerplate, leaflet, email, flexlayout and llms — deployed live at https://pannellum.2plot.dev on 2026-08-02.
The site now states one identity everywhere
dash-pannellum — 360° panoramas for Dash is the brand on every surface — <title>, the /llms.txt H1, the llms viewer's chip, og:site_name, the webmanifest, the home page H1 and the README. The package name leads because it is the string that finds the thing on PyPI; "Pip Install Python" is the byline, in the description. Previously every surface said "Dash Pannellum", a name that matches no installable package.
AI/LLM + SEO surfaces rebuilt on dash-improve-my-llms ≥ 2.3.4
The vendored 2.0.0 tarball is gone; the package installs from PyPI and run.py refuses to boot below the 2.3.4 floor. Every register_page now passes image_url= and description= — one page missing either makes Dash emit an empty og:image, which scrapers prefer over every correct tag. The social card lives on the CDN (cdn.2plot.ai/github_assets/pannellum.2plot.dev.png, 1200×630), not in assets/, so a cold container can never poison an unfurl cache. templates/index.html declares only what Dash does not emit. The cross-host network directory, the hub bulletin (NETWORK_BULLETIN_URL), and a real installable-app surface (favicon set + webmanifest) are wired.
The network's analytics contract
Requests carrying the 2plot-internal UA token are dropped at write time, before bot classification; every outbound call to another network host sends that token. The hourly signed traffic rollup POSTs to 2plot.ai when CROSS_APP_WEBHOOK_SECRET is set, under this app's one short id: pannellum (AD_APP_ID, SATELLITE_APP_KEY and the bulletin app_id all agree). /healthz answers ok: true on every backend — the exact field the fleet's batteries assert.
CI/CD is now a deploy gate
ci.yml (pull requests only; cd.yml owns main): actionlint first, a secretless pytest suite across flask/fastapi × py3.11–3.13 × dash 4.4.0 and pinned, then the real Docker image built, fingerprint-asserted from inside (dash ≥ 4.4, dimll ≥ 2.3.4, gunicorn ≥ 23), booted, and battered by the same script CD runs against production. cd.yml deploys via Render hook, waits for sustained health (120s settle + 5 consecutive 200s), and runs the battery plus smoke_live against the live domain. gunicorn moved past the request-smuggling CVEs (21.x → ≥ 23) by installing markdown2dash --no-deps; the Dockerfile gained PYTHONUNBUFFERED=1 and an explicit COPY list. render.yaml is authored for the first deploy (docker runtime, disk-backed analytics ledger, Clerk satellite env).
Housekeeping
The README was rebuilt to the network standard (badges, docs links) and made truthful again: it now lists all ten documentation pages — the old table missed scenes, arena, emotes and gyro — and no longer claims a vendored dash-improve-my-llms 2.0 (vendor/ holds only the dash-clerk-auth tarball). [project.urls] pointed at a hyphenated repository name that does not exist; it now points at dash_pannellum, with Documentation at pannellum.2plot.dev.
The local /analytics/traffic dashboard is gone: traffic accounting lives on the hub (the tracker and the hourly signed rollup to 2plot.ai are unchanged), so a per-satellite dashboard was a second, staler view of the same ledger.
tests/conftest.py removes its per-run temp state dir at exit. mkdtemp falls back to the current directory when no system temp dir is writable (sandboxed runs with TMPDIR unset), which had leaked eleven pannellum-tests-* dirs into the repo root; the pattern is gitignored as a backstop. The local legacy/ archive — an untracked 457 MB second clone of the original repo, whose history is already this repository's own history — was removed from the working tree, along with stale 0.4.0 build artifacts and a 0.4.0-era forum-post draft.
0.4.0 — 2026-06-12
Gyro look-around — the device is the camera.
Added
orientationprop (imperative, no rebuild) — request gyroscope
steering. Pannellum's native sensor pipeline does the quaternion work and runs the iOS 13+ permission prompt inside startOrientation; on iOS, set the prop from a clientside callback on a direct tap so the prompt lands inside Safari's gesture window.
orientationSupported/orientationActiveread-only props —
request vs truth, kept separate on purpose: permission can be denied, desktops have no sensors, and Pannellum pauses gyro steering while the user grabs the panorama.
- Gyro Look-Around docs page (
/components/gyro) — real sensor on
phones, plus a drag-pad tilt simulator on desktop that feeds the camera the same continuous lookAt stream a gyroscope would.
Notes
- Pannellum gates orientation on
DeviceOrientationEvent+ a **mobile
user agent + literal https:** — plain-HTTP localhost reports unsupported even though browsers would allow the sensor there. Use a TLS dev cert or a tunnel to test on a phone.
- Verified end-to-end under emulated-iPhone HTTPS: support detected,
switch engages steering, synthetic device rotation pans the camera (yaw 5° → −25°), disengage restores drag-only.
0.3.1 — 2026-06-12
Upstream feedback from the 360 Dish Lab: "the live callbackHotspots diff is a wholesale remove-and-re-add … an in-place positional update would let DOM hotspots animate too." Done.
Changed
callbackHotspotsupdates are now a per-name diff instead of a
wholesale replace: unchanged hotspots are untouched; position-only changes move the existing DOM node in place (Pannellum re-reads pitch/yaw from the live spec object every render); other changes (text/type/cssClass) recreate just that hotspot; vanished names are removed. Contacts can now drift every tick as real DOM hotspots — staying hoverable and clickable mid-flight — instead of being redrawn into the canvas as a workaround.
- In-place moves trigger one render on idle viewers via
setUpdate
re-assertion (a same-value setPitch short-circuits inside Pannellum and renders nothing — found the hard way).
Fixed
- Duplicate hotspot DOM nodes when
callbackHotspotschanged while
the panorama was still loading: a pre-load addHotSpot got a second div from Pannellum's own load pass, leaving an orphan marker. Hotspot sync now waits for isLoaded(); the on-load sync applies the latest prop value.
Added
- Drifting-hotspot demo on the Callback Hotspots docs page — a
contact orbiting on a 400 ms Interval via plain prop updates, verified: same DOM node across the whole flight, clickable mid-drift.
0.3.0 — 2026-06-11
The movement-mechanics release. 0.2.0 made the camera imperative; 0.3.0 makes the world updatable in place — and settles the design: LOOK with lookAt (free), MOVE by redrawing a canvas texture (no rebuild), rebuild only when the WORLD changes (and treat that as a real transition).
Added
- Dynamic canvas scenes — a tour scene can set
panoramaCanvasId(the
DOM id of a <canvas>) instead of a panorama URL. The canvas becomes the sphere's texture via Pannellum's dynamic mode (the same machinery as 360° video): redraw the canvas and the view updates in place — no rebuild, no camera reset, no flash.
dynamicUpdateprop — keeps the texture re-uploading every frame
while a dynamic canvas scene is live.
- Scene Configuration docs page (
/components/scenes) — a live config
playground (initial-view presets, autoRotate, zoom limits) plus the fly-to pattern: named targets + lookAt with a duration, triggered from buttons, from clicked hotspots, and from a simulated external event.
Fixed
- White flash on rebuilds — Pannellum's container defaults to
#f4f4f4; the component now forces it transparent, so any rebuild dips to the host page's background instead of flashing white.
- Joystick Arena flash while moving — the arena no longer re-outputs
tour per glide step (the 0.2.0 data-URI approach, which rebuilt the viewer every move). It binds the scene once to a live canvas and glides by redrawing pixels. Verified: the viewer DOM node survives whole glide sessions and arena switches; frame brightness stays flat (no white frames). Arena switches get a deliberate 140 ms dip-to-dark fade.
- Hold-to-glide —
DashRCJoystickonly emits on change, so a pinned
stick went silent after one step; the arena now runs its own glide cadence while the stick is held at the rim.
0.2.0 — 2026-06-11
The real-time release — shaped by the 360 Dish Lab prototype's wishlist (its turn-based design existed purely to work around 0.1.0's lack of an imperative API).
Added
lookAtprop — imperative camera writes:{pitch, yaw, hfov, animated}
pans/zooms the live viewer with no rebuild. Smooth enough for joystick-driven steering via window.dash_clientside.set_props.
hfovread-only prop — the zoom now reports back alongside
pitch/yaw, so it can be baked across rebuilds.
loadSceneprop — imperative tour scene switch, no rebuild; unknown
IDs and the active scene are ignored.
callbackHotspotsis live — prop changes diff through Pannellum's
addHotSpot/removeHotSpot instead of requiring a tour re-init, so markers can move in real time (360 Dish Lab wishlist #1).
preloadScenesprop (defaultTrue) — other scenes' panoramas are
prefetched once the viewer is up: tour jumps no longer show a "Loading..." flash.
hideLoadingSpinnerprop — suppresses Pannellum's load box per
viewer, for preloaded tours and data-URI panoramas where the spinner is the only visible part of an instant transition.
- Joystick Arena docs page (
/components/arena) — DashRCJoystick
(dash-gauge) drives a 360° petri-dish arena composed client-side from vendored {z}/{x}/{y} floor tilesets (3×3 @ z15, 5×5 @ z9, 7×7 @ z3) into data-URI panoramas: tilt to look (lookAt), pin the stick to glide (re-compose + bake gaze). The server is never in the locomotion loop.
Changed
- Callback hotspots are now applied imperatively on viewer load rather
than baked into the scene config (no behavioral change to clicks; verified against the hotspots docs page).
- The Virtual Tours example demonstrates flash-free scene jumps
(preloadScenes + hideLoadingSpinner).
Still on the wishlist
Hotspots + viewer-config passthrough in multiRes mode, and a bundled (non-CDN) Pannellum runtime.
0.1.0 — 2026-06-11
The revival release. The component was rebuilt for the modern Dash stack and the repository was restructured around a live documentation site.
Breaking
- Dash 4.2+ required. The package now depends on
dash>=4.2.0and is built against React 18. customControlsis coherent across modes:Truehides the built-in zoom/fullscreen controls in every mode. Previously it was ignored for image panoramas and inverted for video.- Video no longer autoplays; users press play (browsers block autoplay-with-audio anyway).
pitch/yawupdates are throttled to 4/s and change-detected (previously 10/s unconditionally). Idle viewers no longer fire callbacks.
Added
- Configuration props are live: changing
tour,multiRes,video,autoLoad,compass,northOffsetoruseHttpStreamingfrom a callback tears down and re-initializes the viewer, so panoramas can be swapped dynamically (in 0.0.6 a config change destroyed the viewer without rebuilding it). loadedis now a declared prop (it was set but undeclared in 0.0.6).- Callback hotspots use real Pannellum
clickHandlerFunchandlers, solastClickedHotspotfires reliably. - CDN script loading is deduplicated — multiple viewers on one page share one Pannellum/video.js load.
- Live documentation site (
run.py) on the dash-documentation-boilerplate architecture: Dash Mantine Components appshell, markdown-driven pages with live examples, pluggable Flask/FastAPI backends, llms.txt/sitemap/robots via dash-improve-my-llms 2.0.
Changed
- Packaging moved from
setup.pytopyproject.toml; JS toolchain updated (webpack 5.97, babel 7.26, React 18.3). - Repository restructured: component source at
src/lib/, generated package atdash_pannellum/, docs app at the top level. The original 0.0.6 repo is archived untouched inlegacy/.
0.0.6 — 2024
Last release of the original component (Dash 1/2 era).
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /changelog/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt