# CODING CONVENTIONS — TikTok Manager

Every developer and AI agent working on this project must follow these conventions. They are mandatory and enforced by review.

---

## 1. Project Structure

```
TikTokManager/
├── Dockerfile                  # Container image definition (web + scheduler share it)
├── docker-compose.yml          # tiktok-manager + tiktok-scheduler + optional postgres profile
├── entrypoint.sh               # DB wait + migrations + gunicorn (web container only)
├── healthcheck.py              # Scheme-aware container health probe
├── requirements.txt            # Python dependencies (pinned)
├── requirements-dev.txt        # pytest, pip-audit
├── pytest.ini
├── .env.example                # Environment template
├── .gitignore
├── AGENTS.md                   # Entry point for AI agents
├── .githooks/                  # pre-commit + commit-msg (git config core.hooksPath .githooks)
├── DOC/
│   └── API.md                  # Complete API documentation
├── RULEFOLDER/
│   ├── README.md               # Map and reading order
│   ├── ARCHITECTURE.md         # Modules, data flow, pattern catalog, layering
│   ├── FEATURE-REGISTRY.md     # Feature → GUI route, API endpoint, service, doc, status
│   ├── CODING-CONVENTIONS.md   # This file
│   ├── SECURITY.md             # Mandatory security rules + open findings (TT-S-*)
│   ├── API-PARITY.md           # GUI ↔ API rule
│   ├── CONTRIBUTING.md         # Workflow, definition of done, review, release
│   ├── CHANGELOG.md            # GENERATED from changes/ — never hand-edited
│   ├── TEAM.md                 # Developer codes for change IDs
│   ├── changes/                # One file per change: CHG-YYYYMMDD-DEV-NN-<slug>.md
│   ├── decisions/              # ADR-001..005
│   └── tools/                  # new_change.py, build_changelog.py, check_rules.py
├── app/
│   ├── __init__.py             # Flask app factory + SQLAlchemy init
│   ├── main.py                 # Gunicorn entry point (web)
│   ├── scheduler.py            # Scheduler loop entry point (python -m app.scheduler, ADR-005)
│   ├── config.py               # Config from environment variables
│   ├── models.py               # Account + ScheduledPost models (Fernet-encrypted tokens)
│   ├── auth.py                 # TikTok Login, token refresh, decorators
│   ├── tiktok_api.py           # Facade: Login Kit + Display API + Content Posting API
│   ├── services/               # Use cases shared by GUI, API and the scheduler (ADR-002)
│   │   ├── errors.py           # ValidationError, NotFound, Forbidden, UpstreamError
│   │   ├── uploads.py          # store_upload, remove_quietly
│   │   ├── posts.py            # publish-now validation + flow
│   │   └── scheduled.py        # scheduled-post use cases + run_due_posts (ADR-005)
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── auth_routes.py      # Login/callback/logout
│   │   ├── account_routes.py   # Dashboard, accounts (GUI adapters)
│   │   ├── post_routes.py      # Posts + scheduled posts (GUI adapters)
│   │   ├── insights_routes.py  # Insights (GUI adapter)
│   │   └── api_routes.py       # REST API adapters (/api/v1/*)
│   ├── templates/              # Jinja2 HTML templates
│   └── static/
│       └── css/style.css
├── migrations/                 # Alembic (Flask-Migrate); one migration per schema change (ADR-004)
├── tests/                      # pytest; mirrors app/ (tests/services/, tests/routes/)
├── data/                       # SQLite DB (volume mount)
├── uploads/                    # Temp video files for immediate posts (volume mount)
├── media/                      # Persistent video files for scheduled posts (volume mount)
└── dev-certs/                  # mkcert certificates for local HTTPS (git-ignored)
```

### Rules

- **One blueprint per file.** Each route file defines exactly one `Blueprint` object.
- **Routes are adapters, not logic.** A route parses the transport (form/JSON), picks the
  account, calls **one** service or facade function, renders (HTML or envelope). No domain
  `if`/`else`, no field-by-field mapping, no TikTok API calls outside the facade. Both the GUI
  route and the API route for a feature call the same function (ADR-002).
- **Services are Flask-free.** `app/services/*` never import `request`, `session`, `flash`,
  `jsonify` or templates. They take plain values plus the injected account/token, validate,
  call the facade, return plain dicts, raise typed errors from `services/errors.py`.
- **The scheduler calls services, never routes.** `app/scheduler.py` is a plain loop over
  `services.scheduled.run_due_posts()` — the same use case the adapters call (ADR-005).
- **The facade is DB-free and Flask-free.** `tiktok_api.py` takes tokens as plain strings,
  never touches models. DB access stays in `models.py`, `auth.py` and services.
- **Dependencies point down only:** routes → services → facade/models. Never upward, never
  sideways between two route modules.
- **Templates extend `base.html`.** No standalone HTML pages.
- **All new env vars go in `.env.example`** with a comment and a safe default (no working
  default for secrets, see `SECURITY.md` §2.1).

---

## 2. Naming Conventions

| Element | Convention | Example |
|----------|-----------|---------|
| Python files | `snake_case.py` | `tiktok_api.py` |
| Python functions | `snake_case()` | `list_videos()` |
| Python classes | `PascalCase` | `ScheduledPost` |
| Flask blueprints | `name_bp` | `post_bp` |
| Route functions | `blueprint_action()` | `api_create_post()` |
| Template files | `snake_case.html` | `scheduled_posts.html` |
| CSS classes | `kebab-case` | `post-card` |
| API URL paths | `/kebab-case` | `/api/v1/insights/videos` |
| Env vars | `UPPER_SNAKE` | `TIKTOK_CLIENT_KEY` |
| Change IDs | `CHG-YYYYMMDD-DEV-NN` | `CHG-20261013-RG-01` |
| Branches | `chg/<slug>` | `chg/reels-support` |
| Service functions | `verb_noun(account, ...)` in `services/<domain>.py` | `services.scheduled.run_due_posts()` |

---

## 3. Design Patterns Used

### 3.1 Factory Pattern — `create_app()` (test isolation, config flexibility)
### 3.2 Blueprint Pattern — one `Blueprint` per route module
### 3.3 Decorator Pattern — `@login_required` (session-only), `@api_key_required` (constant-time)
### 3.4 Facade Pattern — `tiktok_api.py` wraps the three TikTok APIs; failures become `TikTokAPIError` with safe messages
### 3.5 Adapter Pattern — `TikTokAPIError` → envelope codes (`tiktok_error_response()`) or GUI flash messages
### 3.6 DTO Pattern — the response envelope; `Account.to_dict()` / `ScheduledPost.to_dict()` (never the tokens)
### 3.7 Active Record Pattern — the two models carry encryption helpers and serialization
### 3.8 Template Method Pattern — `base.html` skeleton
### 3.9 Service Layer (ADR-002) — `services/posts.py`, `services/scheduled.py`: one validation for GUI, API and scheduler
### 3.10 Worker loop (ADR-005) — `app/scheduler.py`: one process, one writer, idempotent ticks; no scheduler library

---

## 4. API Design Rules

### 4.1 Response Envelope

Every `/api/v1/*` endpoint returns:

```json
{ "success": true, "data": { ... }, "error": null }
```

On error:
```json
{ "success": false, "data": null, "error": { "code": "ERROR_CODE", "message": "Human-readable" } }
```

Use `api_success(data)` and `api_error(message, code, status)` helpers. Never return raw dicts.

### 4.2 URL Structure

- Base prefix: `/api/v1`
- Resource nesting: `/api/v1/posts/{publish_id}/status`, `/api/v1/scheduled/{id}/retry`
- Query params for filtering: `?account_id=1&max_count=20&cursor=0`

### 4.3 Account Resolution

Every endpoint that touches TikTok must call `resolve_account()` which:
1. Checks `account_id` query param (database id **or** TikTok open_id)
2. Checks `X-Account-Id` header
3. Read endpoints fall back to the first connected account
4. Mutating endpoints pass `require_explicit=True` and return `400 EXPLICIT_ACCOUNT_REQUIRED`
   without an explicit account — always check the error first

### 4.4 Tokens

TikTok access tokens live 24 hours and the refresh token **rotates on every refresh**. Never
call the facade with a stored token directly — go through `auth.ensure_fresh_token(account)`,
which refreshes when needed and persists the rotated tokens. The scheduler does the same
before every publish.

### 4.5 Timestamps

`scheduled_at` and `expires_at` are stored as **naive UTC** (engine-neutral: SQLite and
PostgreSQL behave identically). Parse user input with `services.posts.parse_scheduled_time`
(accepts ISO-8601, returns aware UTC) and strip the tzinfo at the storage boundary
(`astimezone(timezone.utc).replace(tzinfo=None)`). Never compare naive columns with aware
datetimes.

---

## 5. Template Rules

- Always `{% extends "base.html" %}`; blocks: `title`, `content`, `scripts`
- Flash messages are rendered in `base.html` — use `flash(message, category)` in routes
- Every `POST` form carries `<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">`
- Bootstrap 5 dark theme (`bg-dark`, `border-secondary`, `text-secondary`); brand accents are
  the TikTok pink `--tt-pink` and cyan `--tt-cyan` CSS variables from `style.css`
- Bootstrap Icons (`bi bi-*`)
- External JS via CDN only (Bootstrap)

---

## 6. Error Handling Rules

### In Routes
- Catch `TikTokAPIError` and typed service errors; `flash()` the safe `message` (GUI) or map to
  the envelope (API)
- Never expose raw exception strings to end users

### In API
- `tiktok_error_response()` maps TikTok statuses to `NOT_FOUND` / `FORBIDDEN` / `RATE_LIMITED` /
  `UPSTREAM_ERROR` with the fixed safe message
- `ValidationError` → `400 INVALID_INPUT`; missing account → `404 NO_ACCOUNT`

### In tiktok_api.py / services / scheduler
- The facade raises `TikTokAPIError` with a client-safe message; adapters handle it
- The scheduler records **only** the safe message in `scheduled_posts.error` — the same string
  the UI shows; details go to the log with `exc_info=True`

---

## 7. Dependency Rules

- All packages pinned in `requirements.txt` with exact versions
- Add new dependencies only when necessary, in a dedicated change with the `pip-audit` result
  in `## Verification` (ADR-005 deliberately added **none** — the scheduler is a plain loop)
- Never use `from module import *`
- Group imports: stdlib → third-party → app modules

---

## 8. Database Rules (ADR-004)

- **Two supported engines, one switch.** `DB_ENGINE=sqlite` (default) or `DB_ENGINE=postgres`
  (PostgreSQL **17 or newer**). The choice is made in `.env` and nowhere else.
- **PostgreSQL is a sibling container** (`db`), enabled by the `postgres` compose profile. Data
  lives in the named volume `pgdata`. Its port is not published.
- **`config.py` is the only place a connection URL is built.**
- **Schema changes are Alembic migrations.** One migration per change, named after the change ID.
  `entrypoint.sh` runs `flask db upgrade` before starting workers. `db.create_all()` is not used.
- **Engine-neutral models and queries only.** No raw SQL, no `JSONB`/`ARRAY`, no SQLite pragmas.
  Counters are integers; scopes are comma-separated text.
- **OAuth tokens are encrypted by the model.** Write-only properties Fernet-encrypt on
  assignment; `access_token_plain()` / `refresh_token_plain()` are the only decrypt points;
  `TIKTOK_CLIENT_SECRET` is config, never a column.
- **Naive-UTC datetimes** for `expires_at` / `scheduled_at` (see §4.5).
- **The app waits for the database.** `entrypoint.sh` retries up to `DB_CONNECT_TIMEOUT_SECONDS`.
- **Two containers write the same DB** (web + scheduler): at SQLite scale this is safe
  (sequential scheduler ticks, short transactions); multi-writer deployments should use
  PostgreSQL (SECURITY.md TT-S-09).

---

## 9. Docker Rules

- `Dockerfile` must build without cache: `docker compose build --no-cache`
- Two always-on services from one image: `tiktok-manager` (entrypoint.sh → db wait → migrate →
  gunicorn) and `tiktok-scheduler` (`python -m app.scheduler`, user `1000:1000`, no ports, no
  gunicorn — ADR-005). Never run two scheduler containers against one database.
- Volumes: `./data` (DB), `./uploads` (temp), `./media` (scheduled videos), `./dev-certs`
  (read-only TLS certs), `pgdata` (named volume).
- Image tags pinned to an exact version (`postgres:17.6-alpine`, `python:3.12-slim`), never
  `latest`; bumps are `infra` changes.
- Env vars via `.env` file, never hardcoded.
- gunicorn: 2 workers, 300s timeout (video uploads); optional `--certfile/--keyfile` from
  `SSL_CRT_FILE`/`SSL_KEY_FILE`.
- Web container runs as the non-root `app` user (gosu drop in entrypoint); `HEALTHCHECK` runs
  `healthcheck.py` (scheme-aware).

---

## 10. Design Patterns: when to name one

A pattern name in a fragment or in `ARCHITECTURE.md` is a claim that the code is deliberately
shaped that way and that a reader can rely on it. So:

- Name a pattern only when the code has the structure the pattern describes **and** the choice
  is explained in the fragment's `## How` (problem, why this pattern, what was rejected).
- Do not decorate ordinary code with pattern names. Write `patterns: []` instead.
- Prefer the simplest structure that works. A function beats a class; a plain loop beats a
  scheduler library (ADR-005); a hierarchy needs an ADR.

---

## 11. Testing

- Framework: `pytest`; dev dependencies in `requirements-dev.txt`.
- Layout mirrors `app/`: `tests/services/test_scheduled.py`, `tests/routes/test_api_posts.py`.
- **Every service function has a unit test** using a fake facade (`monkeypatch`) so no test
  ever reaches TikTok.
- **Every API endpoint has a test** through the Flask test client covering: missing/invalid key
  (401), happy path (envelope `success: true`), one validation error (`success: false`, code),
  and for mutations the missing-explicit-account case (`EXPLICIT_ACCOUNT_REQUIRED`).
- **Every GUI mutation route has a test** that a missing CSRF token is rejected (the
  `WTF_CSRF_ENABLED=True` app fixture in `tests/routes/test_gui_csrf.py`).
- **The scheduler use case is tested** (`run_due_posts`): due posts publish, future posts wait,
  failures record safe errors, stored files are cleaned up.
- No test, no merge. "Tested manually in the browser" goes in `## Verification` **in addition**,
  not instead.

---

## 12. Logging and errors

- `import logging; log = logging.getLogger(__name__)` at module top. Never `print`.
- One line per event: `log.info("scheduled.publish id=%s status=%s", post.id, post.status)`.
  Never log tokens, keys, request bodies, file contents or personal data.
- Services raise typed errors; adapters map them: API → envelope codes, GUI → flash categories.
  Exception text never reaches a client (`SECURITY.md` §2.8); TikTok payloads are logged
  server-side with `exc_info=True` only.

---

## 13. Type hints and docstrings

- Type hints on every service and facade signature.
- One-line docstring on every service function stating the use case and the errors it raises.
- No other comments unless the *why* is non-obvious; then a short comment with the change ID.

---

## 14. Change Records

**Every change, no exceptions, has a change fragment in the same commit.**

Create it with `python RULEFOLDER/tools/new_change.py …`, follow `CONTRIBUTING.md`, never edit
`CHANGELOG.md` directly. The pre-commit hook rejects code without a fragment.
