Skip to content

go2impact/deck-manager

 
 

Repository files navigation

Deck Manager

Owner-only admin dashboard for hosting and sharing HTML decks through unique URLs. Built with Next.js (App Router), Prisma, and SQLite.

Make a deck anywhere, host it on your own machine or server, and share it from your own domain.

This is not a slide builder. The intended workflow is:

  • Generate a deck anywhere you want, including with Claude, Codex, or your own tools.
  • Reuse your own HTML slide template if you want consistent layouts, navigation, and interactions across decks.
  • Export or write the result as standalone HTML, including clickable links, buttons, charts, and custom styling.
  • Paste that HTML into Deck Manager and publish it behind a share link and optional password.

If Claude makes you a clickable HTML deck, you can drop it in here and it stays a clickable deck. You do not need to rebuild it as PowerPoint or convert it into a different slide format first.

If you already have a good HTML slide template, this works especially well: tell Claude or Codex to generate the deck inside that template, paste the result here, and share it with an optional password plus any additional resources link you want to attach.

For teams already using AI agents, Deck Manager can be the publish target in an automated loop: generate deck HTML, push it to your site, grab the share URL and password, and send it to the client without a manual formatting pass.

Features

  • 🔐 Owner login via Google OAuth and/or shared password
  • 🗂️ Rich admin UI to create, edit, preview, and deactivate decks with inline HTML editor
  • 🔁 One-click share link regeneration with immediate revocation of old URLs
  • 🤖 Owner-authenticated admin API for listing decks and creating/updating them from other agents or scripts
  • 🧩 Viewer route renders decks inside a sandboxed iframe; raw HTML is never injected into the admin UI
  • 🔗 Optional additional-resources link on each deck for handouts, docs, forms, or follow-up material
  • 🖨️ On-demand PDF downloads with automatic slide pagination and caching
  • 📦 SQLite persistence via Prisma with size limits, validation, and automatic migrations

Tech Stack

  • Next.js 15 (App Router, Server Actions, Turbopack)
  • TypeScript & Tailwind CSS 4 preview
  • Prisma ORM targeting SQLite
  • Sonner for toast notifications

Prerequisites

macOS instructions for a clean machine:

  1. Install Homebrew (skip if already installed):
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
    Then follow the on-screen instructions to add Homebrew to your shell profile (usually echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile && eval "$(/opt/homebrew/bin/brew shellenv)").
  2. Install Node.js 20 (includes npm):
    brew install node@20
    If prompted, add Node to your PATH (e.g. echo 'export PATH="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc).
  3. Verify the tooling:
    node -v
    npm -v
    Both commands should print a version string (for example v20.x.x).

Local Setup

  1. Clone the repository and change into the directory.
  2. Copy the example environment file and update the values:
    cp .env.example .env
    • DATABASE_URL stays as file:./decks.db even when you deploy to Turso/LibSQL (the runtime overrides connection details via LIBSQL_URL).
    • LIBSQL_URL / LIBSQL_AUTH_TOKEN can stay unset locally; production deployments should provide them if you use a remote database.
    • AUTH_SECRET must be a long random string (e.g. openssl rand -base64 32).
    • ADMIN_EMAIL is the single Google account allowed into the admin.
    • ADMIN_PASSWORD is optional and enables password sign-in for that same owner account.
    • ADMIN_API_KEY is optional and enables bearer-token access to the automation API for agents and scripts.
    • GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are optional; set both if you want Google OAuth in addition to password login. See “Set up Google OAuth credentials”.
    • NEXTAUTH_URL should point to the base URL that serves the app (e.g. http://localhost:3000 in development, https://your-domain.com in production). Update it per environment.

Set up Google OAuth credentials

  1. Visit https://console.cloud.google.com/ and create (or select) the Google Cloud project that will own your OAuth client.
  2. In the left navigation, choose Google Auth Platform (or APIs & Services on older projects), open OAuth consent screen, configure the app details, and publish the consent screen so external users can authenticate.
  3. Under Google Auth Platform → Credentials (or APIs & Services → Credentials), click Create credentials → OAuth client ID.
  4. Choose Web application, then configure the authorised origins and redirect URIs:
    • Authorised JavaScript origins: add each base URL that will host the app. For example: http://localhost:3000 (local dev), https://preview.your-domain.com, https://your-domain.com.
    • Authorised redirect URIs: add /api/auth/callback/google for every environment. For example: http://localhost:3000/api/auth/callback/google, https://preview.your-domain.com/api/auth/callback/google, https://your-domain.com/api/auth/callback/google.
  5. After saving, copy the generated Client ID and Client secret into .env as GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
  6. Repeat the authorised origins/redirect URIs and environment variables for each environment (staging, preview, production) where the app will run.
  7. Install dependencies and apply the Prisma migrations:
    npm install
    npx prisma migrate dev
  8. (Optional) Seed sample deck templates manually:
    npm run db:seed
    Each new Google account that signs in receives a private copy of these templates the first time they reach the dashboard.
  9. Start the dev server:
    npm run dev
    The app is now accessible at http://localhost:3000.

Manual testing checklist

  1. Visit http://localhost:3000/admin/login and sign in with the configured owner Google account or the admin password.
  2. Confirm that the dashboard lists the seeded sample decks copied into your account. Use the Copy link and Copy password buttons to grab both values.
  3. Open the copied URL in a new tab (or private window), paste the password, and verify the iframe renders the seeded deck.
  4. From the deck detail screen:
    • Update the HTML textarea (e.g., modify the heading), click Save changes, then refresh the share link tab to confirm the content updates instantly.
    • Click Regenerate link and ensure the old URL now shows “Deck unavailable” while the new link works.
    • Toggle Deactivate and confirm the share link returns the “Deck unavailable” message until reactivated.
    • Click Generate password then Copy password, reload the share page, and confirm only the new password unlocks the deck.
    • Use Delete deck and confirm you are redirected to the dashboard and the share link no longer works.
    • Add an Additional resources link, save, and confirm the share page renders the “Additional resources” button that opens the URL in a new tab.
    • Unlock the share page, click Download deck, and verify the PDF puts each slide on its own page. Repeat the download to confirm the cached copy returns instantly.

Repeat npm run db:seed at any time to update the reusable sample templates. Existing users keep their copies; new logins receive the latest template content.

PDF Downloads

How it works

  • Downloads run headless Chromium in a serverless-friendly mode. Local development uses the full puppeteer package, while production (e.g., Vercel) relies on @sparticuz/chromium + puppeteer-core to keep the binary slim.
  • The generator scans for common slide wrappers such as .slide, .page, [data-slide], [data-page], .slides > *, or plain section elements. If nothing obvious matches the script falls back to printing the top-level layout so every deck still renders.
  • Each successful render stores the PDF bytes, selected selector, slide count, and render time in DeckPdfCache. Subsequent downloads return the cached copy until the deck HTML changes or the deck is deleted.
  • Expect the first render to take 1–5 seconds depending on slide complexity. Cached downloads typically finish in under half a second.

Chromium setup

  • The helper honours optional environment overrides:
    • CHROMIUM_EXECUTABLE_PATH – point to a custom Chromium build.
    • CHROMIUM_ARGS – append extra launch flags (space-delimited).
  • When deploying to Vercel, ensure the function has enough memory (≥1024 MB recommended) and that system packages for fonts/NSS are available. @sparticuz/chromium exposes sensible defaults; no additional config is required for local development beyond npm install.

Testing & guardrails

  • Use FORCE_PDF_FAIL_ONCE=1 npm run dev (or invoke the API route with the env set) to simulate a transient Chromium failure. The route retries once before surfacing a 500, mirroring the viewer’s two-attempt client logic.
  • Because cached PDFs serve immediately, repeated download attempts are inexpensive. We intentionally rely on caching + the single retry as the guardrail instead of adding per-token rate limiting.

Troubleshooting selectors

  • If your exported HTML uses custom slide wrappers, ensure they map to one of the selectors above or wrap each slide in a dedicated element (e.g., <section data-slide>). The admin form includes helper copy with these guidelines.
  • You can confirm which selector was used by checking server logs ([pdf] generated deck=... selector=...).

Admin Workflow

  1. Navigate to /admin/login and sign in with the configured owner Google account or the admin password.
  2. Create a new deck by pasting exported HTML from Claude, Codex, your own app, or any other HTML-producing slide workflow.
  3. If you use a house HTML slide template, keep reusing it and tell your AI tool to generate each new deck inside that template.
  4. (Optional) Provide an Additional resources URL to expose a helper button on the share page.
  5. Use Generate password (or provide your own), then copy both the share link (/share/<token>) and password for your clients.
  6. Use the saved-deck preview inside admin to check the published version without leaving the editor.
  7. Edit decks at any time—existing links immediately serve the updated HTML.
  8. Use Regenerate link to invalidate a previously shared URL, or Deactivate to temporarily block access (viewers will see a “Deck unavailable” message).

Recommended AI Workflow

  1. Build one good HTML deck template with the structure, styling, and interactions you want.
  2. Ask Claude, Codex, or another agent to generate a new deck inside that template.
  3. Paste the resulting HTML into Deck Manager.
  4. Add a password if needed and attach an Additional resources link for docs, files, forms, or handoffs.
  5. Share the published URL.

Example Use Cases

  • Post-client follow-ups with a polished clickable deck, a password-protected share link, and attached resources.
  • AI-generated presentations that go through an edit loop, publish to your website, and then get sent out automatically.
  • Internal proposals, sales follow-ups, handoff docs, and async presentations that need more interaction than a static PDF.

Automation API

If you want another agent to publish decks without clicking through the UI, set ADMIN_API_KEY and use the admin API with a bearer token:

Authorization: Bearer <ADMIN_API_KEY>

The most useful endpoint for automation is the slug-based upsert route. It lets an agent keep updating the same deck while preserving the existing share link and password by default.

Upsert a deck by slug

curl -X PUT "http://localhost:3000/api/admin/decks/by-slug/acme-follow-up" \
  -H "Authorization: Bearer $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Acme Follow-Up",
    "html": "<!DOCTYPE html><html><body><section class=\"slide\">Hello</section></body></html>",
    "description": "Post-meeting recap deck",
    "generatePassword": true,
    "additionalResourceUrl": "https://example.com/resources"
  }'

Response fields include:

  • deck.shareUrl for the client-facing link
  • deck.password for the viewer password
  • deck.adminUrl for jumping back into the editor
  • operation set to created or updated

Common automation routes

  • GET /api/admin/decks lists workspace decks.
  • GET /api/admin/decks/:id returns one deck with its HTML.
  • PATCH /api/admin/decks/:id updates an existing deck by ID.
  • DELETE /api/admin/decks/:id deletes a deck by ID.
  • PUT /api/admin/decks/by-slug/:slug creates or updates a deck using a stable automation slug.

Automation behavior

  • Use a stable slug when you want repeated AI runs to keep updating the same deck.
  • Omit password on slug-based updates to keep the current password unchanged.
  • Set generatePassword: true when you want the server to mint a new password for you.
  • Set regenerateShareLink: true on updates only when you want to rotate the public URL.

Deployment

Step-by-step instructions (including provisioning Turso/LibSQL and configuring Vercel) live in docs/deployment.md.

Useful Scripts

  • npm run dev – local dev server with Turbopack
  • npm run lint – run ESLint across the project
  • npm run build – production build
  • npx prisma studio – inspect the SQLite database contents

Prisma Studio lifecycle

  1. Start the UI in your terminal (launches at http://localhost:5555):
    npx prisma studio
  2. Stop it with Ctrl+C in the same terminal. If the shell session is gone, find the lingering process and terminate it:
    lsof -i :5555
    kill <PID>
    Replace <PID> with the value from the node row in the lsof output.

Troubleshooting

  • Unable to sign in: Confirm AUTH_SECRET, ADMIN_EMAIL, and at least one login method are configured. If using Google, ensure GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set and the redirect URI matches /api/auth/callback/google. If using password login, confirm ADMIN_PASSWORD matches what you entered.
  • Deck link 404: Ensure the deck is marked “Active” in the admin UI and that you copied the latest share link after regeneration.
  • Download fails or is slow: Make sure the deck unlocks first, then retry. Check server logs for Chromium launch errors and confirm external assets allow headless access. The second attempt should use the cached PDF.
  • Need to simulate a flaky render: Set FORCE_PDF_FAIL_ONCE=1 (in non-production environments) before calling the PDF route. The first attempt will throw, the built-in retry will immediately follow, and logs capture both attempts.

Why we ship our own renderer

  • Deck HTML is arbitrary: users paste fully dynamic pages (custom CSS, JS, animations, Chart.js, etc.), so we need a real browser runtime. Puppeteer with headless Chromium gives us the exact layout/JS execution a deck expects.
  • Slide pagination is bespoke: the generator inspects the DOM to find slide-like nodes, clones them, enforces page breaks, and normalises styles—behaviour generic HTML→PDF tools don’t provide.
  • We inject print overrides: animations are disabled, gradients preserved, and canvases are snapshotted before printing to keep charts/images intact; owning the pipeline lets us wire those hooks in.
  • Caching/retries and metrics are first-class: the renderer plugs directly into DeckPdfCache, emits structured logs, and obeys our retry semantics—important for consistent behaviour in share downloads.
  • Deployment parity: running Chromium ourselves keeps local dev, Vercel, and future serverless targets aligned without relying on third-party PDF services or external network calls.
  • Large HTML uploads: Decks larger than 2 MB are rejected. Compress assets within the exporting tool if needed.

Happy sharing!

About

Private HTML deck hosting for AI-generated presentations with share links, passwords, preview, and a small automation API.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 95.7%
  • Shell 2.3%
  • JavaScript 1.7%
  • CSS 0.3%