Member invites, gallery resync, content and operator docs
- Invite flow: admin issue/revoke on join requests, public single-use claim links (hash-only tokens, 7-day expiry, no state reveal), claim creates the Member account and marks the request accepted - Gallery: admin status page plus resync endpoint; sync refreshes thumbnails whose content changed; tools/gallery contract and checker - Docs: public content page, admin-only operator runbook, api.md invite/gallery sections, all routes in the live API docs, docs reachability gate test - Screenshots cover the new pages; version 1.0.18
This commit is contained in:
+32
-3
@@ -34,14 +34,18 @@ PUBLIC_SHOTS = [
|
||||
("terms", "/terms", "Terms"),
|
||||
("privacy", "/privacy", "Privacy"),
|
||||
("login", "/auth/login", "Log in"),
|
||||
("invite-invalid", "/invite/gallery-sample-expired", "Invite expired"),
|
||||
]
|
||||
|
||||
DOCS_SHOTS = [
|
||||
("docs", "/docs", "Docs"),
|
||||
("docs-welcome", "/docs/welcome", "Docs welcome"),
|
||||
("docs-writing", "/docs/writing", "Docs writing"),
|
||||
("docs-join", "/docs/join", "Docs join"),
|
||||
("docs-legal", "/docs/legal", "Docs legal"),
|
||||
("docs-content", "/docs/content", "Docs content"),
|
||||
("docs-api", "/docs/api", "Docs API index"),
|
||||
("docs-api-content", "/docs/api/content", "Docs API content"),
|
||||
("docs-component-code", "/docs/component-code", "Docs component code"),
|
||||
]
|
||||
|
||||
ADMIN_SHOTS = [
|
||||
@@ -57,6 +61,10 @@ ADMIN_SHOTS = [
|
||||
("admin-audit", "/admin/audit", "Admin audit"),
|
||||
("admin-stats", "/admin/stats", "Admin stats"),
|
||||
("admin-trash", "/admin/trash", "Admin trash"),
|
||||
("admin-gallery", "/admin/gallery", "Admin gallery"),
|
||||
("docs-admin", "/docs/admin", "Docs admin"),
|
||||
("docs-operator", "/docs/operator", "Docs operator"),
|
||||
("docs-backups", "/docs/backups", "Docs backups"),
|
||||
("notifications", "/notifications", "Notifications"),
|
||||
("profile-api-key", "/profile/api-key", "Profile API key"),
|
||||
("docs-api-admin", "/docs/api/admin", "Docs API admin"),
|
||||
@@ -178,6 +186,22 @@ def seed_join(base):
|
||||
raise RuntimeError(f"seed join failed: {response.status_code}")
|
||||
|
||||
|
||||
def seed_invite(base):
|
||||
import httpx
|
||||
|
||||
with httpx.Client(base_url=base, timeout=15.0) as client:
|
||||
login = client.post("/auth/login", data={"username": ADMIN_USERNAME, "password": BOOT_ADMIN_PASSWORD})
|
||||
if login.status_code not in (302, 303):
|
||||
raise RuntimeError(f"seed invite login failed: {login.status_code}")
|
||||
rows = client.get("/admin/joins", headers={"Accept": "application/json"}).json()["requests"]
|
||||
uid = next(row["uid"] for row in rows if row["name"] == "Gallery Writer")
|
||||
issued = client.post(f"/admin/joins/{uid}/invite", headers={"Accept": "application/json"})
|
||||
if issued.status_code != 200:
|
||||
raise RuntimeError(f"seed invite issue failed: {issued.status_code}")
|
||||
claim_url = issued.json()["data"]["claim_url"]
|
||||
return "/invite/" + claim_url.rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def first_post_slug(base):
|
||||
import httpx
|
||||
|
||||
@@ -202,7 +226,7 @@ def check_overflow(page, path, viewport):
|
||||
return ""
|
||||
|
||||
|
||||
def capture_full(browser, out_dir, base, password, records, counter):
|
||||
def capture_full(browser, out_dir, base, password, records, counter, invite_path=None):
|
||||
anon = browser.new_context(viewport=FULL_DESKTOP)
|
||||
page = anon.new_page()
|
||||
page.set_default_timeout(60000)
|
||||
@@ -212,6 +236,9 @@ def capture_full(browser, out_dir, base, password, records, counter):
|
||||
slug = first_post_slug(base)
|
||||
records.append(capture(page, out_dir, base, counter, "post", f"/posts/{slug}", "Post", "Public", "full", True))
|
||||
counter += 1
|
||||
if invite_path:
|
||||
records.append(capture(page, out_dir, base, counter, "invite-claim", invite_path, "Invite claim", "Public", "full", True))
|
||||
counter += 1
|
||||
for name, path, title in DOCS_SHOTS:
|
||||
records.append(capture(page, out_dir, base, counter, name, path, title, "Docs", "full", True))
|
||||
counter += 1
|
||||
@@ -441,6 +468,7 @@ def main():
|
||||
log = None
|
||||
records = []
|
||||
failures = []
|
||||
invite_path = None
|
||||
try:
|
||||
if boot:
|
||||
if REPO_ROOT / "data" not in SHOTS_DATA_DIR.parents:
|
||||
@@ -449,6 +477,7 @@ def main():
|
||||
SHOTS_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
process, log = start_server(SHOTS_DATA_DIR)
|
||||
seed_join(base)
|
||||
invite_path = seed_invite(base)
|
||||
else:
|
||||
wait_live(base)
|
||||
headless = os.environ.get("PLAYWRIGHT_HEADLESS", "1") == "1"
|
||||
@@ -456,7 +485,7 @@ def main():
|
||||
clear_output(out_dir)
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(headless=headless, slow_mo=slow_mo)
|
||||
counter = capture_full(browser, out_dir, base, password, records, 1)
|
||||
counter = capture_full(browser, out_dir, base, password, records, 1, invite_path)
|
||||
capture_responsive(browser, out_dir, base, records, failures, counter)
|
||||
browser.close()
|
||||
checked = len(RESPONSIVE_PAGES) * len(RESPONSIVE_VIEWPORTS)
|
||||
|
||||
Reference in New Issue
Block a user