# YouTube Private Upload Dashboard MVP

Use this reference when building a creator dashboard flow that can upload prepared videos to YouTube while preserving explicit approval, auditability, and no automatic public publishing.

## Core guardrails

- Sprint/MVP scope should be **private upload only**: reject `public` and `unlisted` until a later explicitly-approved release.
- Never silently upload in the background. The UI must show readiness and require explicit creator confirmation.
- Never automatically push a website companion/update after upload. Store link/context internally, but keep public website link hidden until `link_allowed`/approval is explicit.
- Do not add TikTok or other platforms unless the sprint specifically asks for them.
- Use the minimal YouTube OAuth scope first: `https://www.googleapis.com/auth/youtube.upload`.
- `notifySubscribers` must be `false` for private-upload MVP.
- Do not expose tokens/secrets in frontend, logs, audit payloads, docs, or commits.

## Backend shape

1. Add settings with safe defaults/placeholders:
   - `google_client_id`
   - `google_client_secret`
   - `google_redirect_uri`
   - `youtube_oauth_scope=https://www.googleapis.com/auth/youtube.upload`
   - `youtube_default_privacy=private`
   - `youtube_upload_notify_subscribers=false`
   - `youtube_default_category_id=22`
   - `youtube_contains_synthetic_media_default=true`
   - `youtube_upload_dry_run=true|false`
2. OAuth endpoints:
   - `GET /api/accounts/youtube/connect/start`
   - `GET /api/accounts/youtube/connect/callback`
   - `POST /api/accounts/youtube/disconnect/{account_id}`
   - `POST /api/accounts/youtube/reconnect/{account_id}`
   - `GET /api/accounts/youtube/status`
3. Callback requirements:
   - validate OAuth `state`
   - store account/channel identity
   - encrypt access/refresh tokens server-side
   - store expiry/scopes/status
   - if Google config/test-user blocks auth, show a clear blocker; never fake a real connection
4. Upload endpoint:
   - `POST /api/publish/youtube/private-upload/{post_draft_id}`
   - require confirmations for private upload, metadata reviewed, disclosure reviewed
   - validate video asset and file path exists
   - validate YouTube draft/title/description/tags/disclosure
   - require connected account for real upload
   - create a PublishJob rather than doing opaque synchronous work
5. YouTube metadata:
   - `snippet.title`
   - `snippet.description`
   - `snippet.tags[]`
   - `snippet.categoryId` default `22`
   - `status.privacyStatus=private`
   - `status.selfDeclaredMadeForKids=false`
   - `status.containsSyntheticMedia` from disclosure/video metadata
   - `notifySubscribers=false`
6. Prefer `google-api-python-client` with `MediaFileUpload(..., resumable=True)` for the MVP if the project is Python/FastAPI.

## Audit and context updates

Create or extend an audit record like `YouTubeUploadAudit` with:

- video/post/account/job IDs
- YouTube video ID and Shorts URL
- privacy, title, description hash, tags, category
- synthetic-media and made-for-kids flags
- notifySubscribers
- upload status
- endpoints/scopes used
- `website_link_allowed=false` initially
- redacted request/response JSON
- error code/message
- timestamps

On success or dry-run success, update:

- `PostDraft.external_post_id`
- `VideoAsset.external_youtube_id` if available
- `ExternalPost` for future analytics CSV matching by YouTube ID
- Activity events: queued, started, private succeeded/failed
- Website companion status to `uploaded_private` or `waiting_for_public_video`; keep public link hidden until explicitly allowed

## Dry-run mode

Build dry-run early:

- Env/config flag: `YOUTUBE_UPLOAD_DRY_RUN=true` or per-request `dry_run=true`.
- Do not call YouTube API.
- Generate `dryrun_<uuid>` ID.
- Exercise the same DB/audit/context updates as real upload.
- UI must clearly show: “Dry run, not uploaded to YouTube.”
- If no real OAuth account exists, allow a clearly-labelled dry-run account (`account_type=dry_run`, `status=dry_run`) rather than pretending it is connected.

## Frontend UX

Accounts page:

- Show YouTube connection status, account/channel name, scopes, expiry, last API call, last error.
- Disable Connect when OAuth config is missing and explain what is missing.
- Show secrets only as present/missing.
- Keep TikTok marked as planned if not implemented; no fake connect.

Review page:

- Show upload readiness checklist:
  - video file exists
  - title ready
  - description ready
  - tags ready
  - disclosure reviewed
  - account selected
  - privacy = private
- Confirmation modal must state:
  - uploads as PRIVATE
  - not public
  - does not update website automatically
  - stores video ID and audit log
- After upload/dry-run show YouTube ID/URL, website link status, and link to audit/logs.

Settings page:

- Show OAuth configured yes/no, client ID/secret present/missing, redirect URI, scope, default privacy, notify subscribers, category ID, synthetic-media default, dry-run setting.
- Never display secret values.

Logs page:

- Add YouTube uploads filter/tab with time, title/package, account, status, YouTube ID, privacy, endpoints, scopes, error, and audit details.
- Redact tokens, upload URLs with sensitive params, and secrets.

## Verification checklist

Backend:

- OAuth start requires config.
- OAuth callback validates state.
- Tokens are encrypted and redacted.
- Upload rejects missing confirmations.
- Upload rejects non-private privacy.
- Upload rejects missing real account for real upload.
- Dry-run succeeds without YouTube API.
- Successful mocked/dry-run upload stores YouTube ID and audit.
- PostDraft/VideoAsset/ExternalPost/ActivityEvent/WebsiteCompanion update correctly.
- Logs contain no token material.

Frontend/manual:

- Accounts page shows OAuth config blocker or connected account.
- Review page disables real upload until readiness/confirmations are complete.
- Dry-run button works without credentials and is visibly labelled.
- Upload audit appears in logs.
- Website repo status remains pending/unpushed.
- Analytics mapping can later link by YouTube ID.

## Pitfalls

- Relative media paths may be relative to the backend root rather than the monorepo root; resolve against the correct runtime base before declaring “file missing.”
- A dry-run account must not look like a real connected OAuth account. Use explicit `dry_run` account type/status.
- A private/dry-run YouTube URL can be stored internally, but public website output must depend on a separate `website_link_allowed` or equivalent approval gate.
- Do not turn missing OAuth credentials into a skill-level “YouTube upload does not work” conclusion; the durable rule is to show config blockers and keep dry-run available.
