Skip to content

Commit 57f6642

Browse files
Production release for boilerplate.2plot.dev: vendor 2.3.2/0.9.1, live hub contract, auth + access, CI
The accumulated unreleased work, shipped as the tree the Render Blueprint deploys (rollout step 4 of the four-app plan): - Vendor dash-improve-my-llms 2.3.2 (was 2.3.0 pre-fix: robots.txt now allows OAI-SearchBot — the Allow line is the live artifact fingerprint, asserted in tests and scripts/smoke_live.py) and dash-clerk-auth 0.9.1 (data-clerk-domain on the script tag; fixes the dead satellite avatar). - Align lib/hub_client.py with the hub's live contract: current_key() sends the Clerk session token, never caller-asserted identity; hub_tiers() implements the signed /api/page-tiers feed, TTL-cached, failing safe to the local tier. verify() unchanged. - Clerk satellite auth + llms.txt access tiers (lib/auth.py, access.py, page_tiers.py), inert by default: no Clerk env means no-op, all-public pages mean access control stays off. - Dash ~=4.4.1 across backends (4.3.0 FastAPI 500s on non-root URLs), network directory + wordmark, traffic rollups, health endpoint, markdown heading/TOC fixes, docs (authentication, networks), MIT LICENSE, render.yaml, CI matrix (3 backends x Python x Dash), tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cc60db8 commit 57f6642

56 files changed

Lines changed: 6684 additions & 3354 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/claude.md‎

Lines changed: 0 additions & 594 deletions
This file was deleted.

‎.claude/dash_improve_my_llms/SKILLS.md‎

Lines changed: 0 additions & 429 deletions
This file was deleted.
-38.6 KB
Binary file not shown.

‎.claude/design.md‎

Lines changed: 0 additions & 1301 deletions
This file was deleted.

‎.flake8‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[flake8]
2+
# Line length is not policed: this repo's comments carry a lot of explanation
3+
# and reflowing them to 79 columns would make them harder to read, not easier.
4+
max-line-length = 120
5+
extend-ignore = E203, W503, E501
6+
exclude =
7+
.git,
8+
.venv,
9+
__pycache__,
10+
node_modules,
11+
vendor,
12+
.idea,
13+
docs/*/,
14+
per-file-ignores =
15+
# run.py's satellite reporter import sits after the app is fully wired,
16+
# because starting it earlier would report against a half-built registry.
17+
run.py: E402

‎.github/workflows/cd.yml‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: CD
2+
3+
# Deploys boilerplate.2plot.dev, then checks the live site.
4+
#
5+
# The deploy step POSTs to a Render deploy hook held in the
6+
# RENDER_DEPLOY_HOOK_URL secret. Without that secret the step is skipped and
7+
# the workflow goes straight to verification — useful when Render is already
8+
# auto-deploying from GitHub on its own, and it means a fork of this template
9+
# doesn't fail CD on day one for a secret it was never given.
10+
on:
11+
push:
12+
branches: [main]
13+
workflow_dispatch:
14+
inputs:
15+
target_url:
16+
description: Site to verify (skips the deploy when set to another host)
17+
required: false
18+
type: string
19+
20+
concurrency:
21+
group: cd-production
22+
cancel-in-progress: false
23+
24+
env:
25+
PIP_DISABLE_PIP_VERSION_CHECK: "1"
26+
SITE_URL: ${{ inputs.target_url || 'https://boilerplate.2plot.dev' }}
27+
28+
jobs:
29+
test:
30+
name: ci
31+
uses: ./.github/workflows/ci.yml
32+
33+
deploy:
34+
name: deploy to render
35+
needs: [test]
36+
runs-on: ubuntu-latest
37+
environment:
38+
name: production
39+
url: https://boilerplate.2plot.dev
40+
outputs:
41+
deployed: ${{ steps.hook.outputs.deployed }}
42+
steps:
43+
- name: Trigger the Render deploy hook
44+
id: hook
45+
env:
46+
HOOK: ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
47+
run: |
48+
if [ -z "$HOOK" ]; then
49+
echo "::notice::RENDER_DEPLOY_HOOK_URL is not set. Skipping the deploy trigger and verifying whatever is currently live."
50+
echo "deployed=false" >> "$GITHUB_OUTPUT"
51+
exit 0
52+
fi
53+
curl -fsS -X POST "$HOOK" > /dev/null
54+
echo "deployed=true" >> "$GITHUB_OUTPUT"
55+
56+
- name: Wait for the new build to serve traffic
57+
if: steps.hook.outputs.deployed == 'true'
58+
run: |
59+
# Render swaps instances rather than restarting in place, so the old
60+
# build answers /healthz throughout. Waiting for a 200 proves
61+
# nothing; give the build time, then require sustained health.
62+
sleep 120
63+
ok=0
64+
for _ in $(seq 1 40); do
65+
if curl -fsS "$SITE_URL/healthz" > /dev/null; then
66+
ok=$((ok + 1))
67+
[ "$ok" -ge 5 ] && break
68+
else
69+
ok=0
70+
fi
71+
sleep 15
72+
done
73+
if [ "$ok" -lt 5 ]; then
74+
echo "::error::$SITE_URL never became reliably healthy"
75+
exit 1
76+
fi
77+
78+
verify:
79+
name: verify the live site
80+
needs: [deploy]
81+
if: always() && needs.deploy.result != 'cancelled'
82+
runs-on: ubuntu-latest
83+
steps:
84+
- uses: actions/checkout@v4
85+
- uses: actions/setup-python@v5
86+
with:
87+
python-version: "3.12"
88+
89+
- name: Smoke-test the deployment
90+
run: python scripts/smoke_live.py "$SITE_URL"
91+
92+
- name: Report
93+
if: failure()
94+
run: |
95+
echo "::error::Live verification failed for $SITE_URL. All four failures it checks for are silent in production: a canonical on the wrong host, a page serving the JavaScript stub, a missing network directory, and dead peer llms.txt links."

‎.github/workflows/ci.yml‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
# Called by cd.yml so a deploy can never ship something the matrix rejected.
9+
workflow_call:
10+
11+
concurrency:
12+
group: ci-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
env:
16+
PIP_DISABLE_PIP_VERSION_CHECK: "1"
17+
FORCE_COLOR: "1"
18+
# Never let a CI run inherit production behaviour: the base-URL guard keys
19+
# off RENDER / APP_ENV, and the satellite reporter keys off the webhook
20+
# secret. Both must stay inert here.
21+
APP_ENV: ci
22+
23+
jobs:
24+
lint:
25+
name: lint
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
- uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.12"
32+
cache: pip
33+
- run: pip install flake8
34+
- name: flake8
35+
run: flake8 lib components pages tests run.py
36+
37+
test:
38+
name: py${{ matrix.python }} · ${{ matrix.backend }} · dash${{ matrix.dash && format(" {0}", matrix.dash) || " (pinned)" }}
39+
runs-on: ubuntu-latest
40+
strategy:
41+
fail-fast: false
42+
matrix:
43+
# Every backend gets a run on the current Python and the pinned Dash...
44+
python: ["3.12"]
45+
backend: [flask, fastapi, quart]
46+
dash: [""]
47+
include:
48+
# ...the supported Python range gets a run on the default backend...
49+
- python: "3.11"
50+
backend: flask
51+
dash: ""
52+
- python: "3.13"
53+
backend: flask
54+
dash: ""
55+
# ...and the bottom of the `~=4.4.1` range is pinned explicitly on
56+
# both backends that matter, so a 4.4.0-only regression cannot hide
57+
# behind pip resolving to 4.4.1. 4.3.0 is deliberately absent: its
58+
# FastAPI backend 500s on every non-root URL (upstream, fixed in
59+
# 4.4.0), which is why requirements.txt floors at 4.4.
60+
- python: "3.12"
61+
backend: fastapi
62+
dash: "4.4.0"
63+
- python: "3.12"
64+
backend: flask
65+
dash: "4.4.0"
66+
steps:
67+
- uses: actions/checkout@v4
68+
69+
- uses: actions/setup-python@v5
70+
with:
71+
python-version: ${{ matrix.python }}
72+
cache: pip
73+
74+
- name: Install the app
75+
run: |
76+
pip install -r requirements.txt
77+
# Dash's own extras are required for the ASGI backends: a bare
78+
# `fastapi` install is not enough for dash.backends._fastapi to
79+
# import. httpx backs starlette's TestClient.
80+
if [ "${{ matrix.backend }}" != "flask" ]; then
81+
pip install "dash[${{ matrix.backend }}]" httpx
82+
# Globbed, not hardcoded: a version bump in requirements.txt used
83+
# to leave this line pointing at a tarball that no longer exists,
84+
# which fails only on the two ASGI legs.
85+
sdist=$(ls ./vendor/dash_improve_my_llms-*.tar.gz)
86+
pip install "${sdist}[${{ matrix.backend }}]"
87+
fi
88+
pip install pytest pytest-cov
89+
# An explicit matrix pin overrides requirements.txt, to prove the
90+
# bottom of the supported range still works.
91+
if [ -n "${{ matrix.dash }}" ]; then
92+
pip install "dash[${{ matrix.backend }}]==${{ matrix.dash }}" \
93+
|| pip install "dash==${{ matrix.dash }}"
94+
fi
95+
96+
- name: Confirm the pinned dependency versions
97+
run: |
98+
python - <<'PY'
99+
import dash, dash_improve_my_llms as pkg
100+
# Dash 4.3.0's FastAPI backend 500s on every non-root URL. The floor
101+
# in requirements.txt is 4.4.0 for that reason; assert it held.
102+
def parts(v):
103+
return tuple(int(x) for x in v.split(".")[:3] if x.isdigit())
104+
# 4.4 floor: 4.3.0's FastAPI backend 500s on every non-root URL.
105+
assert parts(dash.__version__)[:2] >= (4, 4), dash.__version__
106+
assert parts(pkg.__version__) >= (2, 3, 0), pkg.__version__
107+
print(f"dash {dash.__version__}, dash-improve-my-llms {pkg.__version__}")
108+
PY
109+
110+
- name: Test suite (${{ matrix.backend }})
111+
env:
112+
DASH_BACKEND: ${{ matrix.backend }}
113+
run: pytest tests -q
114+
115+
- name: Boot under a production server
116+
if: matrix.backend == 'flask'
117+
run: |
118+
pip install gunicorn
119+
gunicorn run:server -b 127.0.0.1:8550 --daemon --access-logfile - --error-logfile -
120+
for _ in $(seq 1 30); do
121+
curl -sf http://127.0.0.1:8550/healthz && break
122+
sleep 1
123+
done
124+
# A page that renders under the test client can still fail under a
125+
# real WSGI worker — different import path, different working
126+
# directory, no test-client conveniences.
127+
curl -sf http://127.0.0.1:8550/ > /dev/null
128+
curl -sf http://127.0.0.1:8550/networks > /dev/null
129+
curl -sf http://127.0.0.1:8550/llms.txt | grep -q "## Network"
130+
if curl -sf -A "Googlebot/2.1" http://127.0.0.1:8550/ \
131+
| grep -q "requires JavaScript"; then
132+
echo "::error::the home page served the JavaScript stub to a crawler"
133+
exit 1
134+
fi
135+
136+
docker:
137+
name: docker image
138+
runs-on: ubuntu-latest
139+
needs: [test]
140+
steps:
141+
- uses: actions/checkout@v4
142+
143+
- name: Build
144+
run: docker build -t dash-docs-boilerplate:ci .
145+
146+
- name: Boot the image and probe it
147+
run: |
148+
docker run -d --name docs -p 8550:8550 dash-docs-boilerplate:ci
149+
for _ in $(seq 1 60); do
150+
curl -sf http://127.0.0.1:8550/healthz && break
151+
sleep 2
152+
done
153+
curl -sf http://127.0.0.1:8550/getting-started > /dev/null
154+
curl -sf http://127.0.0.1:8550/sitemap.xml | grep -q "<urlset"
155+
docker logs docs
156+
docker rm -f docs

‎.gitignore‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ db.sqlite3-journal
6464
instance/
6565
.webassets-cache
6666

67+
# Server-side session store. NEVER commit this — Flask-Session writes pickled
68+
# session payloads here, which in apps built on this boilerplate carry Clerk
69+
# identity data and live session ids. Committing it leaks credentials.
70+
flask_session/
71+
6772
# Scrapy stuff:
6873
.scrapy
6974

@@ -130,5 +135,18 @@ dmypy.json
130135
.DS_Store
131136
.idea
132137

138+
# Runtime analytics store written by lib/analytics_tracker.py on app start.
139+
# Generated state, not seed data — committing it means every local run shows up
140+
# as a diff. Unanchored so it matches whatever directory the app is run from.
141+
visitor_analytics.json
142+
visitor_analytics.json.lock
143+
visitor_analytics.json.tmp
144+
# Reporter lease — which worker POSTed the last hourly rollup to 2plot.ai.
145+
.satellite_report.lease
146+
133147
# Node
134-
node_modules/
148+
node_modules/
149+
# Local Claude Code workspace: session notes, design scratch, and the vendored
150+
# package copy used while iterating. Useful locally, noise in a template other
151+
# people fork.
152+
.claude/

0 commit comments

Comments
 (0)