Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
430
scripts/build-manual.py
Normal file
430
scripts/build-manual.py
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
#!/usr/bin/env python3
|
||||
"""SuperPoster User Manual PDF — with embedded screenshots from the running app."""
|
||||
import os
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import inch, mm
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, PageBreak, Image, Table, TableStyle,
|
||||
HRFlowable, KeepTogether, NextPageTemplate, PageTemplate, Frame,
|
||||
)
|
||||
from reportlab.lib.utils import ImageReader
|
||||
|
||||
SHOTS = "apps/web/runs/manual/shots"
|
||||
OUT = "docs/SuperPoster-User-Manual.pdf"
|
||||
|
||||
# Colors (match the app's dark-first identity)
|
||||
BG = HexColor("#0a0a0b")
|
||||
SURFACE = HexColor("#1a1a1e")
|
||||
ACCENT = HexColor("#6c7cff")
|
||||
TEXT = HexColor("#e0e0e6")
|
||||
MUTED = HexColor("#8b8b96")
|
||||
WHITE = HexColor("#ffffff")
|
||||
DARK_TEXT = HexColor("#1a1a2e")
|
||||
DARK_MUTED = HexColor("#555566")
|
||||
BRAND_INDIGO = HexColor("#5b6cff")
|
||||
BRAND_DARK = HexColor("#111118")
|
||||
LIGHT_BG = HexColor("#f5f5fa")
|
||||
|
||||
# Styles
|
||||
title_s = ParagraphStyle("title", fontName="Helvetica-Bold", fontSize=32, textColor=BRAND_INDIGO, spaceAfter=6, leading=38)
|
||||
subtitle_s = ParagraphStyle("subtitle", fontName="Helvetica", fontSize=14, textColor=DARK_MUTED, spaceAfter=20)
|
||||
h1_s = ParagraphStyle("h1", fontName="Helvetica-Bold", fontSize=22, textColor=BRAND_INDIGO, spaceBefore=30, spaceAfter=10, leading=26)
|
||||
h2_s = ParagraphStyle("h2", fontName="Helvetica-Bold", fontSize=16, textColor=DARK_TEXT, spaceBefore=20, spaceAfter=8)
|
||||
body_s = ParagraphStyle("body", fontName="Helvetica", fontSize=10.5, textColor=DARK_TEXT, leading=16, spaceAfter=8)
|
||||
step_s = ParagraphStyle("step", fontName="Helvetica-Bold", fontSize=11, textColor=BRAND_INDIGO, spaceBefore=14, spaceAfter=6)
|
||||
note_s = ParagraphStyle("note", fontName="Helvetica-Oblique", fontSize=9.5, textColor=DARK_MUTED, leading=14, spaceAfter=8)
|
||||
caption_s = ParagraphStyle("caption", fontName="Helvetica-Oblique", fontSize=9, textColor=DARK_MUTED, spaceBefore=4, spaceAfter=12)
|
||||
kbd_s = ParagraphStyle("kbd", fontName="Courier-Bold", fontSize=10, textColor=BRAND_INDIGO, backColor=LIGHT_BG, borderPadding=4, spaceAfter=8)
|
||||
toc_s = ParagraphStyle("toc", fontName="Helvetica", fontSize=11, textColor=DARK_TEXT, leading=22)
|
||||
|
||||
def img(name, width=6.5*inch):
|
||||
path = os.path.join(SHOTS, name)
|
||||
if not os.path.exists(path):
|
||||
return Paragraph(f"[screenshot: {name}]", note_s)
|
||||
img = ImageReader(path)
|
||||
iw, ih = img.getSize()
|
||||
h = width * ih / iw
|
||||
return Image(path, width=width, height=h)
|
||||
|
||||
def screenshot(name, caption):
|
||||
return KeepTogether([img(name), Paragraph(caption, caption_s)])
|
||||
|
||||
doc = SimpleDocTemplate(OUT, pagesize=A4,
|
||||
leftMargin=25*mm, rightMargin=25*mm, topMargin=20*mm, bottomMargin=20*mm,
|
||||
title="SuperPoster User Manual", author="SuperPoster")
|
||||
|
||||
story = []
|
||||
|
||||
# ══════════════════════════════════════ COVER ══════════════════════════════════════
|
||||
story.append(Spacer(1, 80))
|
||||
story.append(Paragraph("SuperPoster", title_s))
|
||||
story.append(Paragraph("User Manual", subtitle_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=2, spaceAfter=20))
|
||||
story.append(Paragraph("""
|
||||
Version 1.0 · October 2026<br/><br/>
|
||||
This guide covers everything you need to get started: creating your workspace,
|
||||
inviting your team, connecting social accounts, monitoring streams, composing
|
||||
and scheduling posts, and using the audit trail.<br/><br/>
|
||||
<b>Design identity reference:</b> Canva Visual Kit · Canva Post Kit · Canva Graphic Resources
|
||||
""", body_s))
|
||||
story.append(Spacer(1, 40))
|
||||
story.append(screenshot("streams-01-overview.png", "SuperPoster — Streams view, the monitoring hub"))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ CONTENTS ══════════════════════════════════════
|
||||
story.append(Paragraph("Contents", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
toc_items = [
|
||||
("1.", "Getting Started — Onboarding"),
|
||||
("2.", "Understanding Streams"),
|
||||
("3.", "Composing and Scheduling Posts"),
|
||||
("4.", "Managing Connected Accounts"),
|
||||
("5.", "Team, Groups and Permissions"),
|
||||
("6.", "Inbox — Direct Messages"),
|
||||
("7.", "Audit Trail — Who Did What"),
|
||||
("8.", "Keyboard Shortcuts"),
|
||||
("9.", "Troubleshooting"),
|
||||
]
|
||||
for num, title in toc_items:
|
||||
story.append(Paragraph(f"{num} {title}", toc_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 1. GETTING STARTED ══════════════════════════════════════
|
||||
story.append(Paragraph("1. Getting Started", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(Paragraph("Creating Your Workspace", h2_s))
|
||||
story.append(Paragraph("""
|
||||
The first time you use SuperPoster, you'll see the onboarding wizard. A workspace
|
||||
is your team's tenant boundary — everything (accounts, drafts, posts, audit)
|
||||
belongs to exactly one workspace and is isolated from all others.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Step 1 — Name your workspace", step_s))
|
||||
story.append(screenshot("onboard-01-start.png", "The onboarding wizard starts here"))
|
||||
story.append(Paragraph("Type a name that identifies your team or organisation.", body_s))
|
||||
story.append(screenshot("onboard-02-named.png", "Enter a descriptive name — you can change it later in Settings"))
|
||||
|
||||
story.append(Paragraph("Step 2 — Invite your team", step_s))
|
||||
story.append(screenshot("onboard-03-invite.png", "Enter email addresses separated by commas"))
|
||||
story.append(Paragraph("""
|
||||
Add teammates by email. They'll receive an invitation and can join with the role
|
||||
you assign them (or the default role). You can always change roles later from
|
||||
the Team screen.
|
||||
""", body_s))
|
||||
story.append(screenshot("onboard-04-emails-filled.png", "Multiple invitations entered"))
|
||||
|
||||
story.append(Paragraph("Step 3 — Connect your first accounts", step_s))
|
||||
story.append(screenshot("onboard-05-connect.png", "Choose platforms to connect — OAuth only, no passwords stored"))
|
||||
story.append(Paragraph("""
|
||||
Click <b>Connect</b> on any platform. You'll be redirected to that platform's own
|
||||
OAuth page where you log in securely. SuperPoster never sees or stores your
|
||||
platform password — only an encrypted access token.
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
<b>Important:</b> SuperPoster stores OAuth tokens only. Your account password never
|
||||
transits or rests in this system. You can revoke access at any time from either
|
||||
SuperPoster or the platform itself.
|
||||
""", note_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 2. STREAMS ══════════════════════════════════════
|
||||
story.append(Paragraph("2. Understanding Streams", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(Paragraph("Your Monitoring Hub", h2_s))
|
||||
story.append(screenshot("streams-01-overview.png", "Streams — three columns, each tracking a different feed"))
|
||||
story.append(Paragraph("""
|
||||
The Streams screen is your command centre. Each column tracks a feed from a
|
||||
connected platform. Cards show the author, age, text, and engagement. Hover over
|
||||
any card to reveal its amplify bar.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Selecting Items", h2_s))
|
||||
story.append(screenshot("streams-02-item-selected.png", "An item is selected (highlighted in blue) — its amplify bar is now visible"))
|
||||
story.append(Paragraph("""
|
||||
Press <b>j</b> to move to the next item or <b>k</b> to go back. You can also click any
|
||||
card. The selected card shows a blue highlight and reveals its amplify buttons.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Amplifying — Boost, Quote and Share", h2_s))
|
||||
story.append(screenshot("streams-03-repost-toast.png", "Pressing r boosts the selected item — a toast confirms the action"))
|
||||
story.append(Paragraph("""
|
||||
Each platform's amplify bar shows <b>only what that API truly supports</b>:
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
<b>Mastodon:</b> Boost (native reblog) and Quote (native quote-post)<br/>
|
||||
<b>Telegram:</b> Forward<br/>
|
||||
<b>Bluesky:</b> Repost and Quote<br/>
|
||||
<b>Instagram:</b> No native reshare — but Quote… with attribution always works<br/>
|
||||
<b>Facebook:</b> Share link (approximation)<br/>
|
||||
<b>Reddit:</b> Crosspost
|
||||
""", body_s))
|
||||
|
||||
story.append(screenshot("streams-04-quote-popover.png", "Pressing q opens the quote composer"))
|
||||
story.append(Paragraph("""
|
||||
Type your commentary. The character limit matches the platform you're quoting
|
||||
on. The original post is shown above for reference.
|
||||
""", body_s))
|
||||
story.append(screenshot("streams-05-quote-typed.png", "Your commentary appears above the quoted content"))
|
||||
|
||||
story.append(Paragraph("Quote with Attribution", h2_s))
|
||||
story.append(Paragraph("""
|
||||
On platforms without a native quote API (like Instagram or Facebook), SuperPoster
|
||||
offers <b>Quote… with attribution</b> — this composes a new original post that
|
||||
references the source with proper attribution. It's indicated by a dashed-border
|
||||
button. This isn't a native API call; it's your own post with attribution text,
|
||||
which every platform allows.
|
||||
""", body_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 3. COMPOSING ══════════════════════════════════════
|
||||
story.append(Paragraph("3. Composing and Scheduling Posts", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(Paragraph("Writing a Post", h2_s))
|
||||
story.append(screenshot("compose-01-blank.png", "The Compose screen — a rich editor with markdown support"))
|
||||
story.append(Paragraph("""
|
||||
Type your post in the rich editor. Markdown formatting is supported (bold with
|
||||
**text**, headings with #, etc.). As you type, the character counter updates.
|
||||
""", body_s))
|
||||
|
||||
story.append(screenshot("compose-02-typed.png", "A composed post — note the character counter and platform previews"))
|
||||
|
||||
story.append(Paragraph("Targeting Platforms", h2_s))
|
||||
story.append(Paragraph("""
|
||||
Click platform chips (Mastodon, Telegram, Facebook, Instagram) to toggle targets.
|
||||
Each target shows its own character limit. The overall counter reflects the
|
||||
<b>tightest</b> limit among your selected targets.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("The Approval Flow", h2_s))
|
||||
story.append(Paragraph("""
|
||||
If your workspace uses approvals, the flow is:<br/><br/>
|
||||
<b>Draft</b> → Submit for Review → <b>In Review</b> → Approve/Reject → <b>Approved</b> → Schedule/Publish
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
Every transition is permission-checked and recorded in the audit trail with a
|
||||
full diff of what changed.
|
||||
""", note_s))
|
||||
|
||||
story.append(Paragraph("Scheduling", h2_s))
|
||||
story.append(Paragraph("""
|
||||
Set a date and time in the Schedule field. Your post enters the Temporal-backed
|
||||
scheduling pipeline — durable, idempotent, and survives restarts. If Temporal
|
||||
is briefly unavailable, your scheduled post sits safely as "pending" until the
|
||||
worker picks it up.
|
||||
""", body_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 4. CONNECTIONS ══════════════════════════════════════
|
||||
story.append(Paragraph("4. Managing Connected Accounts", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(screenshot("connections-01-overview.png", "The Channels screen — each card shows platform, handle, capabilities and health"))
|
||||
story.append(Paragraph("""
|
||||
Every connected account appears as a card showing:
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
<b>Platform and handle</b> — which account this is<br/>
|
||||
<b>Capability chips</b> — what this platform's API actually supports<br/>
|
||||
<b>Health badge</b> — healthy (green) or token expiring (amber)<br/>
|
||||
<b>Capacity chip</b> — how many posts you have left in your rolling 24h window
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Post Capacity", h2_s))
|
||||
story.append(Paragraph("""
|
||||
Click the capacity chip (e.g., <b>17/25 posts left</b>) to see your usage bar,
|
||||
when the next slot frees, and any tier notes. Provider caps are rolling 24-hour
|
||||
windows — slots free up continuously as older posts age out, not at midnight.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Connecting a New Account", h2_s))
|
||||
story.append(Paragraph("""
|
||||
1. Click <b>Connect account</b> (or the Connect button on a disconnected card)<br/>
|
||||
2. You'll be redirected to the platform's OAuth page<br/>
|
||||
3. Log in on the platform's own page and authorize SuperPoster<br/>
|
||||
4. You're redirected back — the connection appears with its capabilities
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
<b>Security guarantee:</b> OAuth tokens are envelope-encrypted in the vault.
|
||||
Nobody — not even workspace Owners — can read raw tokens through any interface.
|
||||
""", note_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 5. TEAM ══════════════════════════════════════
|
||||
story.append(Paragraph("5. Team, Groups and Permissions", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(screenshot("team-01-overview.png", "The Team screen — members, roles and the shared asset library"))
|
||||
|
||||
story.append(Paragraph("Roles", h2_s))
|
||||
story.append(Paragraph("""
|
||||
SuperPoster ships with 10 seeded roles. Each role is a bundle of permissions —
|
||||
the system checks permissions, never role names, so you can customise roles
|
||||
without breaking anything:
|
||||
""", body_s))
|
||||
|
||||
role_table = Table([
|
||||
["Role", "Can Do"],
|
||||
["Owner", "Everything, including credential management"],
|
||||
["Admin", "Everything except credential management"],
|
||||
["Publisher", "Compose, schedule, publish, manage media"],
|
||||
["Editor", "Compose and edit drafts, manage media"],
|
||||
["Approver", "Review and approve/reject submitted drafts"],
|
||||
["Analyst", "View analytics and audit trail"],
|
||||
["Moderator", "Manage inbox, live chat moderation"],
|
||||
["Video Publisher", "Compose, schedule and publish video content"],
|
||||
["Stream Operator", "Manage livestreams"],
|
||||
["Broadcast Operator", "Virtual channel control, director actions, live chat"],
|
||||
], colWidths=[1.6*inch, 4.9*inch])
|
||||
role_table.setStyle(TableStyle([
|
||||
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0,0), (-1,-1), 9),
|
||||
("TEXTCOLOR", (0,0), (0,-1), BRAND_INDIGO),
|
||||
("TEXTCOLOR", (1,0), (1,-1), DARK_TEXT),
|
||||
("VALIGN", (0,0), (-1,-1), "TOP"),
|
||||
("BOTTOMPADDING", (0,0), (-1,-1), 6),
|
||||
("TOPPADDING", (0,0), (-1,-1), 6),
|
||||
("LINEBELOW", (0,0), (-1,-2), 0.5, HexColor("#e0e0e8")),
|
||||
("BACKGROUND", (0,0), (-1,0), LIGHT_BG),
|
||||
]))
|
||||
story.append(role_table)
|
||||
|
||||
story.append(Paragraph("Changing Roles and Removing Members", h2_s))
|
||||
story.append(Paragraph("""
|
||||
Use the role dropdown on any member row. Removing a member takes effect
|
||||
immediately — their active sessions fail closed on their next request thanks
|
||||
to the authorization epoch system. They are logged out instantly; no stale
|
||||
access lingers.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Groups (Coming Soon)", h2_s))
|
||||
story.append(Paragraph("""
|
||||
Groups let you scope which connected accounts and media assets each sub-team
|
||||
can use. For example: the "Breaking News" group gets access to the main
|
||||
Mastodon and Bluesky accounts, while the "Lifestyle" group only sees the
|
||||
Instagram and Facebook pages. A member's publish targets filter automatically
|
||||
to show only the accounts their groups permit.
|
||||
""", body_s))
|
||||
|
||||
story.append(Paragraph("Asset Library", h2_s))
|
||||
story.append(Paragraph("""
|
||||
The shared asset library holds your team's images, videos and documents.
|
||||
Assets can be scoped to groups (shown with a lock icon) — members outside the
|
||||
group see the lock but cannot access the file.
|
||||
""", body_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 6. INBOX ══════════════════════════════════════
|
||||
story.append(Paragraph("6. Inbox — Direct Messages", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(screenshot("inbox-01-overview.png", "The Inbox — unified conversations across all connected accounts"))
|
||||
story.append(Paragraph("""
|
||||
All direct-message conversations from your connected accounts appear in one list.
|
||||
Unread badges show new messages; assignee initials show who's handling each thread.
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
<b>Reply windows:</b> Messaging APIs allow replies within each platform's window
|
||||
(typically 24 hours for Meta platforms). Outside the window, only pre-approved
|
||||
template messages are allowed. SuperPoster shows you exactly what's possible
|
||||
rather than failing silently.
|
||||
""", note_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 7. AUDIT ══════════════════════════════════════
|
||||
story.append(Paragraph("7. Audit Trail — Who Did What", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
story.append(screenshot("audit-01-overview.png", "The Audit Trail — filterable, hash-chained, tamper-evident"))
|
||||
story.append(Paragraph("""
|
||||
Every meaningful action is recorded: posts published, roles changed, credentials
|
||||
rotated, connections added. The chain is <b>hash-linked</b> — each entry's hash
|
||||
incorporates the previous entry's hash, so any tampering is mathematically
|
||||
detectable.
|
||||
""", body_s))
|
||||
|
||||
story.append(screenshot("audit-02-diff-open.png", "Click any row to see the exact field-by-field diff"))
|
||||
story.append(Paragraph("""
|
||||
The diff viewer shows <b>before → after</b> values with red/green highlighting.
|
||||
Secret values always appear as <b>[REDACTED]</b> — they are stripped before the
|
||||
audit entry is created, not hidden by the UI. This is enforced at the database
|
||||
function level.
|
||||
""", body_s))
|
||||
story.append(Paragraph("""
|
||||
Use the filter bar to narrow by actor or action. Export to CSV for compliance
|
||||
reporting.
|
||||
""", body_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 8. SHORTCUTS ══════════════════════════════════════
|
||||
story.append(Paragraph("8. Keyboard Shortcuts", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
keys = Table([
|
||||
["Shortcut", "Action", "Where"],
|
||||
["j / ↓", "Next item", "Streams"],
|
||||
["k / ↑", "Previous item", "Streams"],
|
||||
["r", "Repost/boost selected item", "Streams"],
|
||||
["q", "Quote selected item", "Streams"],
|
||||
["Esc", "Close popover/dialog", "Everywhere"],
|
||||
["⌘K / Ctrl+K", "Command palette", "Everywhere"],
|
||||
], colWidths=[1.4*inch, 3.3*inch, 1.8*inch])
|
||||
keys.setStyle(TableStyle([
|
||||
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0,0), (-1,-1), 10),
|
||||
("FONTNAME", (0,1), (0,-1), "Courier-Bold"),
|
||||
("TEXTCOLOR", (0,1), (0,-1), BRAND_INDIGO),
|
||||
("TEXTCOLOR", (1,0), (1,-1), DARK_TEXT),
|
||||
("VALIGN", (0,0), (-1,-1), "TOP"),
|
||||
("BOTTOMPADDING", (0,0), (-1,-1), 8),
|
||||
("TOPPADDING", (0,0), (-1,-1), 8),
|
||||
("LINEBELOW", (0,0), (-1,-2), 0.5, HexColor("#e0e0e8")),
|
||||
("BACKGROUND", (0,0), (-1,0), LIGHT_BG),
|
||||
]))
|
||||
story.append(keys)
|
||||
story.append(Spacer(1, 20))
|
||||
story.append(Paragraph("""
|
||||
The command palette (⌘K) lets you jump to any screen, compose a new post, or
|
||||
trigger actions without touching the mouse.
|
||||
""", body_s))
|
||||
story.append(PageBreak())
|
||||
|
||||
# ══════════════════════════════════════ 9. TROUBLESHOOTING ══════════════════════════════════════
|
||||
story.append(Paragraph("9. Troubleshooting", h1_s))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=14))
|
||||
|
||||
faq = Table([
|
||||
["Question", "Answer"],
|
||||
["Why can't I boost on some columns?", "That platform's API doesn't expose resharing. The button is simply not there — we never fake capabilities. Use Quote… with attribution instead."],
|
||||
["Why did everyone get logged out?", "A role or membership change bumped the workspace's authorization epoch. Stale sessions fail closed immediately. One re-login fixes it."],
|
||||
["Where are my passwords?", "Nowhere. SuperPoster stores OAuth tokens only, envelope-encrypted in the vault. Revoke access from the platform or in SuperPoster at any time."],
|
||||
["Who can see my stream keys?", "Nobody through any interface. Keys are redacted from every screen, log, API response, and audit entry by database-level enforcement."],
|
||||
["Why is my capacity at 0?", "You've hit your API tier's rolling 24h cap. The capacity chip shows when the next slot frees — as your oldest post exits the window."],
|
||||
["Posts stuck as 'pending'?", "The scheduler worker picks them up automatically. Check that the Temporal service is running (your admin can verify via /readyz)."],
|
||||
], colWidths=[1.8*inch, 4.7*inch])
|
||||
faq.setStyle(TableStyle([
|
||||
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0,0), (-1,-1), 9.5),
|
||||
("TEXTCOLOR", (0,1), (0,-1), BRAND_INDIGO),
|
||||
("TEXTCOLOR", (1,1), (1,-1), DARK_TEXT),
|
||||
("VALIGN", (0,0), (-1,-1), "TOP"),
|
||||
("BOTTOMPADDING", (0,0), (-1,-1), 10),
|
||||
("TOPPADDING", (0,0), (-1,-1), 10),
|
||||
("LINEBELOW", (0,0), (-1,-2), 0.5, HexColor("#e0e0e8")),
|
||||
("BACKGROUND", (0,0), (-1,0), LIGHT_BG),
|
||||
]))
|
||||
story.append(faq)
|
||||
|
||||
story.append(Spacer(1, 30))
|
||||
story.append(HRFlowable(width="100%", color=BRAND_INDIGO, thickness=1, spaceAfter=10))
|
||||
story.append(Paragraph("SuperPoster · Version 1.0 · October 2026", ParagraphStyle(
|
||||
"footer", fontName="Helvetica", fontSize=9, textColor=DARK_MUTED, alignment=TA_CENTER
|
||||
)))
|
||||
|
||||
doc.build(story)
|
||||
print(f"PDF: {OUT} ({os.path.getsize(OUT):,} bytes)")
|
||||
Loading…
Add table
Add a link
Reference in a new issue