Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f508df75 | ||
|
|
26d662ed7c | ||
|
|
e35d908672 | ||
|
|
c0db8f6165 | ||
|
|
fd4e69a27c | ||
|
|
1ccb031dba | ||
|
|
7be9ffd3ed | ||
|
|
74b57bfee4 | ||
|
|
638a59018a |
@@ -12,3 +12,11 @@ screenshot-*.png
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Local AI coding hosts (procoder init); .procoder/ stays trackable.
|
||||||
|
/skills/procoder/
|
||||||
|
|
||||||
|
# procoder state (regenerable)
|
||||||
|
.procoder/state/
|
||||||
|
.procoder/index/
|
||||||
|
.lycheecache
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[git]
|
||||||
|
default_branch_policy = "report" # rulebook allows docs/evidence commits to main
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Context — anwarpy.ai
|
||||||
|
|
||||||
|
## effective access
|
||||||
|
|
||||||
|
The single server-side decision (launch.ts policy + doc.access + doc.status) consumed identically by the article page, public JSON route, download gate, and llms-full.
|
||||||
|
|
||||||
|
## ghost-read
|
||||||
|
|
||||||
|
Fetching an intended-VIP post logged out to assert what a stranger sees; the acceptance check for any access-logic change.
|
||||||
|
|
||||||
|
## honest 503
|
||||||
|
|
||||||
|
A dependency-down response with a fixed Arabic sentence (e.g. billing disabled, mail transport down) instead of a fake success; reference implementation: /api/notify.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"skills"
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"synced_at": "2026-10-13T02:31:48Z",
|
||||||
|
"lockfiles": {
|
||||||
|
"web/package-lock.json": "b0168d824c755b061aeb7dc8bddb7faa05b7a236ccc82b196843d5752e3632d3"
|
||||||
|
},
|
||||||
|
"migrations": {
|
||||||
|
"web/src/migrations": {
|
||||||
|
"count": 14,
|
||||||
|
"entries": [
|
||||||
|
"20261002_130505_init.json",
|
||||||
|
"20261002_130505_init.ts",
|
||||||
|
"20261002_170000_add_full_name.ts",
|
||||||
|
"20261003_111451.json",
|
||||||
|
"20261003_111451.ts",
|
||||||
|
"20261003_133758.json",
|
||||||
|
"20261003_133758.ts",
|
||||||
|
"20261003_201454.json",
|
||||||
|
"20261003_201454.ts",
|
||||||
|
"20261009_130817.json",
|
||||||
|
"20261009_130817.ts",
|
||||||
|
"20261010_142214.json",
|
||||||
|
"20261010_142214.ts",
|
||||||
|
"index.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"env_keys": {}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<!-- gstack digest, source: https://github.com/garrytan/gstack (agents-digest/gstack-AGENTS.md) -->
|
<!-- gstack digest, source: https://github.com/garrytan/gstack (agents-digest/gstack-AGENTS.md) -->
|
||||||
|
|
||||||
# gstack digest v1.91.12.0 — regenerate/re-copy after upgrading gstack
|
# gstack digest v1.91.12.0 — regenerate/re-copy after upgrading gstack
|
||||||
|
|
||||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||||
@@ -15,6 +16,7 @@ reviews, and evals on top of these rules.
|
|||||||
## The reuse ladder
|
## The reuse ladder
|
||||||
|
|
||||||
Before writing new code, stop at the first rung that holds:
|
Before writing new code, stop at the first rung that holds:
|
||||||
|
|
||||||
1. A helper, util, or pattern already in this repo.
|
1. A helper, util, or pattern already in this repo.
|
||||||
2. The standard library.
|
2. The standard library.
|
||||||
3. A native platform feature (CSS over JS, DB constraint over app code).
|
3. A native platform feature (CSS over JS, DB constraint over app code).
|
||||||
@@ -34,3 +36,55 @@ corporate tone, no AI vocabulary.
|
|||||||
Clone https://github.com/garrytan/gstack and run `./setup` for the full
|
Clone https://github.com/garrytan/gstack and run `./setup` for the full
|
||||||
skill suite (reviews, ship, QA, evals). This digest is generated — edit
|
skill suite (reviews, ship, QA, evals). This digest is generated — edit
|
||||||
scripts/gen-agents-digest.ts, not this file.
|
scripts/gen-agents-digest.ts, not this file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Anwarpy.ai Operational Rulebook (agents)
|
||||||
|
|
||||||
|
Appended 2026-10-11 by owner request. The gstack digest above is untouched and authoritative for behavior; this section is project-specific operations. The specs live beside it: `PRD.md`, `ARCHITECTURE.md`, `DESIGN_SYSTEM.md`, `DATABASE.md`, `SECURITY.md`, `CODE_STYLE.md`, `TESTING.md` — read the relevant one before touching that layer.
|
||||||
|
|
||||||
|
## Always do
|
||||||
|
|
||||||
|
1. **Work in `web/`** for app code; docs/specs at root. Read before writing: the spec for the layer you're touching + existing patterns in sibling files.
|
||||||
|
2. **Verify before claiming**: `cd web && npx tsc --noEmit` and `npm run build` must exit 0 for any TS change. Behavioral changes get exercised (ghost-read for access changes, curl for API changes, screenshot for UI).
|
||||||
|
3. **Ghost-read after touching access logic**: fetch an intended-VIP post logged out and assert what a stranger sees (full body in free early access; preview+paywall when billing is on).
|
||||||
|
4. **Keep contracts verbatim**: the honest-503 Arabic strings, notice-strip copy, and meta lines are product contracts (PRD §3) — copy them exactly.
|
||||||
|
5. **Record evidence**: a claim without command+output is not verification (launch-doc standard). Test rows use `@example.com` and are deleted after; report prod counts post-cleanup.
|
||||||
|
6. **ADR for structural decisions**: anything changing routes, contracts, policy layer, or schema gets `docs/adr/NNNN-*.md` in the same PR.
|
||||||
|
7. **Clean up**: delete throwaway scripts, temp env files, smoke rows, and dev servers (`pkill -f "next dev"`) when done.
|
||||||
|
|
||||||
|
## Allowed without asking
|
||||||
|
|
||||||
|
- Read anything in the repo; read prod **state** via `ssh root@147.79.102.201` (read-only psql selects, `docker compose logs`, `curl` checks).
|
||||||
|
- Create/edit files under `web/src/`, `web/scripts/`, root docs; run `tsc`, `next build`, vitest, `payload generate:types`, local dev server on :3000.
|
||||||
|
- Run the local seed/backfill scripts against the **local** DB.
|
||||||
|
- Commit to a feature branch; open Gitea PRs (merge = **POST** `/repos/ahmed/anwar-pi-website/pulls/{index}/merge` `{"Do":"merge"}`; PUT → 405).
|
||||||
|
|
||||||
|
## Ask the owner before
|
||||||
|
|
||||||
|
- **Deploying**: `rsync` to the VPS or `docker compose up -d --build` (recipe below) — deploy is owner-visible truth.
|
||||||
|
- **Merging to `main`** (except doc/evidence commits recording verified facts).
|
||||||
|
- **Destructive or irreversible**: DB writes on prod (inserts/updates/deletes beyond smoke-row cleanup), `rm` outside your own scaffolds, `git reset --hard`/force-push, rotating credentials.
|
||||||
|
- **Changing contracts**: launch policy values, API status codes/bodies, the access decision, schema fields, or anything marked «verbatim» in the PRD.
|
||||||
|
- **Spending**: new services, paid plans, DNS/Cloudflare console changes, new dependencies (one justification line in the PR).
|
||||||
|
|
||||||
|
## Never
|
||||||
|
|
||||||
|
- Never print, commit, rsync, or video-render secret values (`.env*`, tokens, `DB_PASSWORD`, `AGENT_API_KEY`, `PAYLOAD_SECRET`). Partial leaks → recommend rotation immediately (precedent: 2026-10-10, 8 chars of DB_PASSWORD).
|
||||||
|
- Never rewrite `access` fields to simulate a launch state; the launch state is `LAUNCH_MODE`.
|
||||||
|
- Never edit an applied migration, restore over the live DB, or run pipeline payloads without the secret scan.
|
||||||
|
- Never add CAPTCHA/rate limits beyond the login lockout, a component library, Tailwind, or a global state lib (recorded decisions).
|
||||||
|
- Never let a VIP body reach an anonymous response (JSON route, files, llms-full, or the future chatbot) when billing is on.
|
||||||
|
|
||||||
|
## Deploy recipe (owner-approved only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rsync -az web/ root@147.79.102.201:/root/anwarpi/ \
|
||||||
|
--exclude node_modules --exclude .next --exclude .git \
|
||||||
|
--exclude media --exclude files-private \
|
||||||
|
--exclude '.env*' --exclude '*.tsbuildinfo' # NO --delete (protects backups/, media/, files-private/)
|
||||||
|
ssh root@147.79.102.201 'cd /root/anwarpi && docker compose -f docker-compose.vps.yml up -d --build'
|
||||||
|
# migrations apply on boot; LAUNCH_MODE persists in /root/anwarpi/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
Prod scripts (no `scripts/` in the standalone image): `ssh -f -N -L 15432:<db-container-ip>:5432 root@…`, then locally `set -a; source <tmp env>; set +a; DATABASE_URI=postgresql://anwarpi:$DB_PASSWORD@127.0.0.1:15432/anwarpi npx tsx scripts/<script>.ts`; delete the temp env file after. Never echo the password.
|
||||||
|
|||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
# ARCHITECTURE — anwarpy.ai
|
||||||
|
|
||||||
|
> Source of truth for structure and boundaries. Change this file with the code, in the same PR.
|
||||||
|
|
||||||
|
## 1. Stack (pinned 2026-10-11)
|
||||||
|
|
||||||
|
| Layer | Choice | Version |
|
||||||
|
| ----------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
|
| Framework | Next.js App Router (server components first) | next@15.4.11 — ceiling of `@payloadcms/next` peer support; known CVEs risk-accepted (SECURITY.md §8) |
|
||||||
|
| CMS / API / admin | Payload 3 (Next-native, Lexical rich text) | ^3.60.0 |
|
||||||
|
| Database | PostgreSQL | postgres:16 (container) |
|
||||||
|
| DB adapter | @payloadcms/db-postgres (Drizzle migrations) | ^3.60.0 |
|
||||||
|
| UI runtime | React | ^19.2.0 |
|
||||||
|
| Styling | Hand-rolled CSS (`src/app/design.css` + per-route CSS). No Tailwind, no component library. | — |
|
||||||
|
| Payments | stripe | ^18.5.0 (scaffolded, gated by `billingEnabled()`) |
|
||||||
|
| Email | Cloudflare Email REST API (ADR-0003) — **never SMTP** | — |
|
||||||
|
| Language | TypeScript strict | ^5.9.0 |
|
||||||
|
| Deploy | Docker Compose on VPS `root@147.79.102.201:/root/anwarpi`, Traefik proxy network, apex anwarpy.ai (www 308) | — |
|
||||||
|
|
||||||
|
## 2. Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
/ docs + specs (PRD.md, ARCHITECTURE.md, docs/adr/, docs/launch/)
|
||||||
|
web/ the app — everything below lives here
|
||||||
|
src/app/
|
||||||
|
(frontend)/ public site routes (about, articles, catalog, posts, topics,
|
||||||
|
subscription, signup, login, verify-email, onboarding,
|
||||||
|
account-settings, payment-*, terms, privacy)
|
||||||
|
api/ route handlers (see §5)
|
||||||
|
design.css design tokens — single source of visual truth
|
||||||
|
layout.tsx fonts (Readex Pro), metadata templates
|
||||||
|
src/collections/ Payload collections = the schema (see DATABASE.md)
|
||||||
|
src/lib/ shared modules (see §4)
|
||||||
|
src/migrations/ generated Drizzle migrations
|
||||||
|
src/middleware.ts rate limiting on /api/users (login/lockout fidelity)
|
||||||
|
scripts/ backup-db.sh, backfill-reading-minutes.ts, generate-covers.mjs, seed.ts
|
||||||
|
docker-compose.vps.yml prod topology (db, web, volumes, proxy network)
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules: root holds docs only; the app never imports from outside `web/`; docs are committed with the change they describe.
|
||||||
|
|
||||||
|
## 3. The one decision that rules everything: effective access
|
||||||
|
|
||||||
|
```
|
||||||
|
launch.ts (policy) + doc.access + doc.status → effective access
|
||||||
|
```
|
||||||
|
|
||||||
|
- `src/lib/launch.ts` is the **only** place that reads `LAUNCH_MODE`. `billingEnabled()`, `freeEarlyAccess()`, `READING_WORDS_PER_MINUTE = 160`.
|
||||||
|
- `Posts.ts` field-level `read` ACL on `content` (public iff `status=published && (access=free || freeEarlyAccess())`) — and the **same decision** is consumed independently by:
|
||||||
|
- the article page (`posts/[slug]/page.tsx`) → full body vs H2-preview paywall,
|
||||||
|
- the public post JSON route,
|
||||||
|
- the download gate (`Files.ts` per-file access),
|
||||||
|
- `llms.ts` (`fetchIndexDocs(withContent)`).
|
||||||
|
- If you touch access logic, update **all** surfaces or extract a shared function first. Ghost-read (logged-out VIP post) is the acceptance check.
|
||||||
|
|
||||||
|
## 4. Module boundaries (`src/lib/`)
|
||||||
|
|
||||||
|
| Module | Owns | Must not |
|
||||||
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||||
|
| `launch.ts` | LAUNCH_MODE policy, WPM constant | read env elsewhere for launch state |
|
||||||
|
| `viewer.ts` | `getViewer()` — session → `{email, role, id, interest?, level?, emailVerified?, onboarded?}` | touch collections directly beyond auth |
|
||||||
|
| `lexical.ts` | Lexical serialization: `lexicalToText`, `countLexicalWords` (walks `content.root`!), `extractH2s`, `lexicalToPlain` | know about Posts/DB |
|
||||||
|
| `secret-scan.ts` | `scanForSecrets`/`scanAgentPayload` — pattern names only in responses | log or return matched secret text |
|
||||||
|
| `llms.ts` | llms.txt/full rendering, `fetchIndexDocs(withContent)` | bypass the access decision |
|
||||||
|
| `topics.ts` | TOPICS registry (agents/models/automation/foundations), `topicsFor()`, `isTopic()` | hardcode slugs elsewhere |
|
||||||
|
| `format.ts` | Arabic number/label agreement (`guidesLabel`, `minutesLabel`, `MEMBERSHIP_PRICE`) | duplicate Arabic plurals inline |
|
||||||
|
| `mail.ts` | Cloudflare Email REST send | use SMTP; log recipient addresses |
|
||||||
|
| `agent-media.ts` / `agent-files.ts` | pipeline ingestion of media/attachments | accept `access` from the payload |
|
||||||
|
| `next.ts` | shared Next helpers | — |
|
||||||
|
|
||||||
|
Import rule: pages/components may import lib + payload; lib modules never import each other's internals — if two libs need shared logic, promote it to the lower-level module.
|
||||||
|
|
||||||
|
## 5. API surface & communication patterns
|
||||||
|
|
||||||
|
**Route handlers** (`src/app/api/**/route.ts`) return `NextResponse.json`. Every error body is `{"error": "<real Arabic sentence>"}` — honest, specific, never a bare code.
|
||||||
|
|
||||||
|
| Route | Auth | Notes |
|
||||||
|
| ---------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- |
|
||||||
|
| `POST /api/agent/posts`, `PUT /api/agent/posts/[slug]` | `x-agent-key` header | pipeline; 401 bad key, 400 shape, 409 slug conflict (after sourceId dedup), 422 secret scan |
|
||||||
|
| `GET /api/posts/[slug]` | public | JSON honoring the access decision |
|
||||||
|
| `POST /api/posts/[slug]/comments` | session required | server-forced author; moderated status (ADR-0001/0002) |
|
||||||
|
| `POST /api/posts/[slug]/like` | session required | one per user per post |
|
||||||
|
| `POST /api/posts/[slug]/view` | public | anonymous view counter |
|
||||||
|
| `POST /api/notify` | public | always records; honest 503 when mail transport down; dup → `alreadySubscribed` |
|
||||||
|
| `POST /api/subscribe` | public | 503 honest Arabic while billing disabled |
|
||||||
|
| `POST /api/users`, `/api/auth/verify-email`, `/api/auth/resend-verification` | public / token | verification-token writes at signup; middleware rate-limits this path |
|
||||||
|
| `GET /api/files/[id]` | per-file access gate | streams attachments |
|
||||||
|
| `POST /api/stripe/webhook` | signature | no-op while `billingEnabled()` is false |
|
||||||
|
|
||||||
|
**Data access patterns:**
|
||||||
|
|
||||||
|
- Server components use the **Payload Local API** (`payload.find`, `payload.findByID`) — never internal fetch to own routes.
|
||||||
|
- Client components are thin: forms POST to route handlers and render the returned Arabic message. No client data-fetching library, no global store; if a page needs reactive data, it re-fetches via a route handler or uses Payload's REST with credentials.
|
||||||
|
- Generated types: `npm run generate:types` (runs as part of `build`) — collections' `Collection` types come from `payload-types.ts`, never hand-written interfaces for docs.
|
||||||
|
|
||||||
|
## 6. Rendering & state
|
||||||
|
|
||||||
|
- **Server components by default.** `'use client'` only for: forms (signup, notify, comments, account settings), small interactive bits (menu, like button). Check before adding more.
|
||||||
|
- **State management = URL + server.** No Redux/Zustand/Jotai. Session state = `getViewer()`; launch state = `launch.ts`; content state = Payload. The only client state is form-local.
|
||||||
|
- **No render-time nondeterminism** in components that render on the server: no `Date.now()` in bodies that should be static, no unseeded randomness.
|
||||||
|
|
||||||
|
## 7. Deployment topology
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet → Traefik (proxy net) → web (Next standalone, port 3000) → db (postgres:16, internal only)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `docker-compose.vps.yml` services: `db` (postgres:16, volume `db-data`, **no published ports**), `web` (volume `media-uploads`, `files-uploads`), Traefik labels for apex + `www → apex 308` (priority above API router).
|
||||||
|
- `LAUNCH_MODE=free_early_access` lives in `/root/anwarpi/.env.production` — compose passes it; the file is never committed.
|
||||||
|
- Migrations apply **on container boot**. Deploy = rsync `web/` (excludes `node_modules .next .git media files-private '.env*' '*.tsbuildinfo'`, **no `--delete`**) → `docker compose -f docker-compose.vps.yml up -d --build`.
|
||||||
|
- Prod DB access for scripts: the standalone image has no `scripts/`; open an SSH tunnel to the db container IP and run `npx tsx` locally (see AGENTS.md rulebook).
|
||||||
|
- Rollback: previous image + `docker compose up -d`; DB restores come from `backup-db.sh` dumps (rehearsed 2026-10-10).
|
||||||
|
- **Anticipated: Railway move (owner, 2026-10-11).** Keep the app deployable there without redesign: all config via env vars (already true), **no host-local state in the container** (media/files volumes must move to object storage / managed volume; DB → managed Postgres), Traefik labels replaced by Railway's router + custom domain. Any change that deepens VPS coupling (cron on host, local disk caches) must be flagged in the PR that adds it.
|
||||||
|
|
||||||
|
## 8. Decision records
|
||||||
|
|
||||||
|
Architecture decisions live in `docs/adr/NNNN-*.md` (current: 0001 comments route, 0002 moderated comments/server-forced author, 0003 Cloudflare Email REST not SMTP, 0004 coming-soon subscribe at public launch, 0005 member trust model). New decisions that change structure, contracts, or policy get a numbered ADR — one file, context + decision + consequences.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# CODE_STYLE — anwarpy.ai
|
||||||
|
|
||||||
|
> TypeScript, strict, boring on purpose. The existing codebase is the style guide; this file makes it explicit. Formatting tooling may be added later only if it matches, not fights, this file.
|
||||||
|
|
||||||
|
## 1. TypeScript
|
||||||
|
|
||||||
|
- `strict: true` (tsc is a commit gate — `cd web && npx tsc --noEmit` must exit 0).
|
||||||
|
- No `any`. Use `unknown` + narrowing, or the generated Payload types.
|
||||||
|
- Document types come from `src/payload-types.ts` (`npm run generate:types`, runs inside `npm run build`). **Never hand-write an interface that mirrors a collection doc** — import the generated one; hand-written mirrors drift and lie.
|
||||||
|
- Payload collection access/hook args: annotate the request/doc shape explicitly (existing pattern: `args: { doc?: {...}; req: { user?: {...} } | null }`).
|
||||||
|
- Avoid non-null `!`; if you're sure, prove it with a check or a comment stating the invariant.
|
||||||
|
|
||||||
|
## 2. Naming
|
||||||
|
|
||||||
|
| Thing | Convention | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| Files | kebab-case | `secret-scan.ts`, `notify-form.tsx` |
|
||||||
|
| Route folders | Next convention, exact segment names in brackets | `posts/[slug]/route.ts` |
|
||||||
|
| Components | named exports (function name = file name, PascalCase) | `export function Topnav()` |
|
||||||
|
| Route handlers | Next default exports | `export async function POST(req: NextRequest)` |
|
||||||
|
| Lib functions | camelCase, exported individually | `lexicalToText`, `freeEarlyAccess` |
|
||||||
|
| Constants | SCREAMING_SNAKE | `READING_WORDS_PER_MINUTE`, `MEMBERSHIP_PRICE` |
|
||||||
|
| DB fields | camelCase in Payload | `readingMinutes`, `authorName` |
|
||||||
|
| Booleans | is/has/can/should or unambiguous verb | `billingEnabled`, `emailVerified` |
|
||||||
|
|
||||||
|
## 3. Error handling
|
||||||
|
|
||||||
|
- **Route handlers**: validate → early `NextResponse.json({ error: '<Arabic sentence>' }, { status })`. Statuses: 400 shape, 401 auth, 403 forbidden, 404 missing, 409 conflict (slug), 422 semantic rejection (secret scan), 429 rate-limited, 503 dependency honestly down.
|
||||||
|
- Error text is a **fixed Arabic sentence owned by the route** — specific about what failed («الدفع غير مفعّل خلال فترة الوصول المبكر.»). Never echo user input into the message; never expose stack traces or internal hosts; never a bare code with no words.
|
||||||
|
- Never swallow: no empty `catch`. If you catch to continue, comment why. Server-side failures get `console.error` with context (no secrets).
|
||||||
|
- **Honest failure is a feature** (PRD principle 1): if a dependency is down (mail transport, billing), return 503 with the true Arabic explanation instead of faking success. The notify route is the reference implementation.
|
||||||
|
|
||||||
|
## 4. Server vs client components
|
||||||
|
|
||||||
|
- Default: server components. `'use client'` only for forms and small interactions — check existing (`account-settings/client.tsx`, `notify-form.tsx`) before adding a new one.
|
||||||
|
- No secrets, no `process.env` (non-public), no Payload local API calls in client components. They talk to route handlers with `fetch` and render the returned Arabic message.
|
||||||
|
- No data-fetching libraries, no global client state (ARCHITECTURE §6).
|
||||||
|
|
||||||
|
## 5. Arabic & RTL in code
|
||||||
|
|
||||||
|
- UI copy is Arabic with «» quotes; keep strings verbatim from the PRD where they're contracts (honest 503s, notice strips).
|
||||||
|
- Latin islands inside RTL: wrap in `<bdi dir="ltr">` (`$10`, `anwarpy.ai`, code, `GPTBot`).
|
||||||
|
- Plurals/counts via `src/lib/format.ts` (`guidesLabel`, `minutesLabel`, `formatNumber`) — never inline Arabic number agreement.
|
||||||
|
|
||||||
|
## 6. Comments & docs
|
||||||
|
|
||||||
|
- Comments explain invariants and decisions, not syntax. Reference the contract: «L-02», «ADR-0003», «PRD §3.2» are valid shorthand — these docs are the map.
|
||||||
|
- When you change a documented decision, update the doc in the same PR (ADR for structural decisions — ARCHITECTURE §8).
|
||||||
|
- No commented-out code committed. Delete it; git remembers.
|
||||||
|
|
||||||
|
## 7. Git & PRs (Gitea)
|
||||||
|
|
||||||
|
- `main` is deployable truth. Feature branches: `launch/…`-style prefixes (`feat/…`, `fix/…`, `docs/…`).
|
||||||
|
- PRs via Gitea; merge = **POST** `/repos/ahmed/anwar-pi-website/pulls/{index}/merge` with `{"Do":"merge"}` (PUT → 405). Evidence-updating doc commits may go straight to `main` when they record verified facts.
|
||||||
|
- Commit messages: imperative, specific («fix: unwrap Lexical root in lexicalToText/countLexicalWords»), no «update stuff».
|
||||||
|
- Never commit: `.env*`, `media/`, `files-private/`, `backups/`, build output, `.tsbuildinfo`.
|
||||||
|
- Destructive ops (`reset --hard`, `push -f`, branch deletion) require the owner's explicit go-ahead.
|
||||||
|
|
||||||
|
## 8. Dependencies
|
||||||
|
|
||||||
|
- Reuse ladder (gstack): repo helper → stdlib → platform → installed dep → new dep. `stripe` is already installed (do not add a second payment lib); `sharp` exists for images; `tsx` for scripts.
|
||||||
|
- New runtime deps need a PR-level justification line in the description; dev deps for testing are pre-approved by TESTING.md.
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
# DATABASE — anwarpy.ai
|
||||||
|
|
||||||
|
> The schema is **Payload-first**: collections in `web/src/collections/` are the source of truth; SQL in Postgres is a generated artifact. This document maps the logical schema; never edit SQL by hand.
|
||||||
|
|
||||||
|
## 1. Adapter & migrations
|
||||||
|
|
||||||
|
- `@payloadcms/db-postgres` (Drizzle). Connection = `DATABASE_URI` (prod: composed by `docker-compose.vps.yml` from `DB_PASSWORD`; local: `web/.env` → LAN Postgres 192.168.88.36).
|
||||||
|
- Migrations live in `src/migrations/` (timestamped `YYYYMMDD_HHMMSS.ts` + paired `.json`). Created with `npx payload migrate:create`; applied **on container boot** in prod, or `npx payload migrate` locally.
|
||||||
|
- Rules: never edit an applied migration (history: the `20261003_111451` duplicate-`full_name` fix was done in place *before* prod applied it — that's the exception that proves the risk). Data backfills are standalone scripts under `web/scripts/` (`backfill-reading-minutes.ts` pattern), not migrations. `payload_migrations` is bookkeeping — if it says applied, it is.
|
||||||
|
|
||||||
|
## 2. Collections
|
||||||
|
|
||||||
|
### `posts` — guides (the core table)
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `title`, `slug` | text | slug **unique**, URL identity, 409 on conflict |
|
||||||
|
| `dek` | text | standfirst/summary, feeds metadata + JSON-LD |
|
||||||
|
| `category` | select | وكلاء / نماذج / استرجاع / سلامة / أدوات / أعمال |
|
||||||
|
| `topics` | select/array | registry in `src/lib/topics.ts` (agents, models, automation, foundations) |
|
||||||
|
| `access` | select `free` \| `vip` | **intended tier — data, not state**; never rewritten to simulate launch states |
|
||||||
|
| `content` | lexical (richtext) | serialized `{root:{children:[…]}}`; **field-level read ACL** (ARCHITECTURE §3) |
|
||||||
|
| `contentType` | select | note / tutorial / case_study / comparison — badge |
|
||||||
|
| `authorName` | text | visible byline; also JSON-LD author Person |
|
||||||
|
| `sourceId` | text | agent-pipeline idempotency; dedup before slug-409 |
|
||||||
|
| `cover` | upload → `media` | og:image source (1200×630) |
|
||||||
|
| `readingMinutes` | number | **computed at write** (`countLexicalWords` ÷ `READING_WORDS_PER_MINUTE`=160); stripped from agent payloads; backfilled honestly |
|
||||||
|
| `publishedAt` | date | feeds «حُدّث» + JSON-LD |
|
||||||
|
| `status` | select `draft` \| `published` | public visibility gate; `publish: true` agent opt-in |
|
||||||
|
| derived/rels | — | `views`, `likes` counters (see routes) |
|
||||||
|
|
||||||
|
### `users`
|
||||||
|
Payload auth: `email` (unique), `password` (hash), `fullName`, `interest`/`level` (onboarding), `emailVerified` flag + `verification_token` (written at signup, ADR-compliant), `stripeCustomerId` (Phase 3; hidden/removed when empty — never exposed to frontend). Roles: `admin` vs member (see SECURITY §3). Note: `20261003_111451` removed a duplicate `full_name` column.
|
||||||
|
|
||||||
|
### `comments`
|
||||||
|
`post` (rel → posts), `author` (rel → users, **forced server-side** — the API ignores any client-sent author, ADR-0002), `authorName` (display snapshot), `body` (textarea), `status` (moderation: pending → published / rejected). Anonymous requests are rejected at the route (PRD §3.5). `admin.defaultColumns: post, authorName, status, createdAt`.
|
||||||
|
|
||||||
|
### `launch_notifications`
|
||||||
|
`email`, `topic`, timestamps. Written by `/api/notify` on every submission (even when mail transport is down — the honest-503 still records the row). Duplicate email → no new row, `alreadySubscribed: true`. Zero rows during the 2026-10-10 prod smoke cleanup is the expected state.
|
||||||
|
|
||||||
|
### `post_likes`
|
||||||
|
`user` (rel), `post` (rel) — **one per user per post** (uniqueness enforced in the like route before insert); counter incremented on `posts`. Auth required (401 otherwise).
|
||||||
|
|
||||||
|
### `media`
|
||||||
|
Payload uploads (covers + pipeline images) → volume `media-uploads`; `sharp` for sizes. Public URL pattern `/api/media/file/...`.
|
||||||
|
|
||||||
|
### `files`
|
||||||
|
Guide attachments → volume `files-uploads`; per-file `access` mirrors its post (download gate `GET /api/files/[id]`); ingested via `agent-files.ts` (never accepts `access` from the payload).
|
||||||
|
|
||||||
|
## 3. Relationships & integrity
|
||||||
|
|
||||||
|
```
|
||||||
|
posts 1─* comments *─1 users
|
||||||
|
posts 1─* post_likes *─1 users (unique(user,post))
|
||||||
|
posts *─1 media (cover)
|
||||||
|
posts 1─* files (each file carries its own access)
|
||||||
|
```
|
||||||
|
|
||||||
|
Enforcement lives in collection hooks/route handlers (Payload has no SQL-level FK constraints across its tables for these patterns) — hence the "check then insert" pattern in `like` (query `where user+post` before `create`). Keep that pattern for any new per-user-once resource.
|
||||||
|
|
||||||
|
## 4. Indexing
|
||||||
|
|
||||||
|
- Unique: `posts.slug`, `users.email`, `posts.sourceId` (dedup lookup), `verification_token`.
|
||||||
|
- Hot lookups: `comments.post` (+ `status`), `post_likes.post`, `launch_notifications.email` (dup check).
|
||||||
|
- Add indexes via migrations when a query pattern shows up in slow logs — not speculatively.
|
||||||
|
|
||||||
|
## 5. Backups & restore
|
||||||
|
|
||||||
|
- `web/scripts/backup-db.sh` → `pg_dump` gzip; prod cron/backup policy is an owner action.
|
||||||
|
- Rehearsed restore (2026-10-10): restore into a **temp DB** (`anwarpi_restore_test`), verify counts (`posts`, `users`, `payload_migrations`), then drop. Never restore over the live DB without owner approval.
|
||||||
|
|
||||||
|
## 6. Data rules that are also PRD rules
|
||||||
|
|
||||||
|
- `access` and `readingMinutes` are stripped from agent payloads (`pickFields`) — the pipeline cannot set them.
|
||||||
|
- Reading minutes are computed, never trusted from input; if a script touches them, it must recompute from `content` and report scanned/updated counts.
|
||||||
|
- Personal data in `launch_notifications`/`users` emails: never rendered into docs, videos, or evidence (the smoke test uses `@example.com` rows and deletes them).
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# DESIGN_SYSTEM — anwarpy.ai
|
||||||
|
|
||||||
|
> Canonical tokens live in `web/src/app/design.css`. This file documents them and the rules for using them. If CSS and this file disagree, fix one of them in the same PR.
|
||||||
|
|
||||||
|
## 1. Design personality
|
||||||
|
|
||||||
|
Editorial, calm, premium-dark. Ink surfaces, champagne accents, generous whitespace, Arabic-first typography. Restraint is the brand: one accent color, used sparingly; motion is short and soft; nothing blinks, nothing bounces.
|
||||||
|
|
||||||
|
## 2. Color tokens
|
||||||
|
|
||||||
|
| Token | Value | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `--ink` | `#131519` | page background (`--bg`) |
|
||||||
|
| `--ink-2` | `#17191d` | raised background |
|
||||||
|
| `--basalt` | `#1c1f24` | card/surface (`--surface`) |
|
||||||
|
| `--seam` | `#2b2f36` | hairline borders (`--border`) |
|
||||||
|
| `--seam-2` | `#3a3f47` | hover border, stronger seam |
|
||||||
|
| `--mist` | `#ece9e3` | primary text (`--fg`) |
|
||||||
|
| `--stone` | `#a3a09a` | muted text (`--muted`) |
|
||||||
|
| `--dim` | `#7b7974` | faintest text (meta only) |
|
||||||
|
| `--champagne` | `#d4b47c` | THE accent (`--accent`) — badges, CTAs, highlights |
|
||||||
|
| `--champagne-ink` | `#1a150c` | text/icons on champagne (`--accent-ink`) |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Never introduce a new hue. Semantic aliases (`--bg`, `--surface`, `--fg`, `--muted`, `--border`, `--accent`) are the only tokens components may use — raw hex is for `design.css` only.
|
||||||
|
- Champagne on ink or champagne-ink on champagne. Never champagne text on basalt for body copy (contrast), never stone below 14px.
|
||||||
|
- Success/warning/error states (form errors, honest 503s) use `--mist`/`--stone` + wording first; reserve red only where the CSS already defines it. Do not invent traffic-light colors.
|
||||||
|
|
||||||
|
## 3. Typography
|
||||||
|
|
||||||
|
- **Readex Pro** (Arabic + Latin, single family), loaded in `layout.tsx` via `next/font/google`, weights 300–700. `--font-display: var(--font-sans), "Segoe UI", sans-serif`.
|
||||||
|
- Scale: hero H1 ~2.5rem/700; section H2 ~1.5rem/600; body 1rem/400 with line-height ≥1.8 for Arabic; meta/captions 0.875rem `--stone`.
|
||||||
|
- Arabic-first: `direction: rtl` at the page root; Arabic quotes «»; numerals via `formatNumber` (Arabic-Indic where the site does).
|
||||||
|
- **LTR islands** inside RTL text use `<bdi dir="ltr">`: `anwarpy.ai`, `$10`, code, identifiers, agent names (GPTBot…). Never let a raw Latin token break RTL flow.
|
||||||
|
- No `<br>` for spacing; use margins. No font-size below 0.75rem.
|
||||||
|
|
||||||
|
## 4. Spacing & shape
|
||||||
|
|
||||||
|
- Space scale: 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64px. Section padding 48–64px; card padding 16–24px; page max-width container per existing layout (centered, ≥24px gutters).
|
||||||
|
- Radii: cards/pills 8–12px; badges/buttons pill (999px) where the existing CSS does; inputs match buttons.
|
||||||
|
- Borders: 1px `--seam` everywhere; hover → `--seam-2`. No heavy shadows; elevation = `--ink-2`/`--basalt` layering, not blur.
|
||||||
|
|
||||||
|
## 5. Component inventory (build from these, extend the list, never fork it)
|
||||||
|
|
||||||
|
Existing, in `src/components/` + route CSS: `topnav.tsx`, `site-menu.tsx`, `nav-links.tsx`, `footer.tsx`, `anwarpi-background.tsx`, `set-body-attr.tsx`.
|
||||||
|
|
||||||
|
Recurring patterns (CSS classes are the contract):
|
||||||
|
- **Buttons**: `.btn`, `.btn.btn-primary` (champagne bg, `--accent-ink` text), disabled/coming-soon state = «قريباً» pill, wired to `subscribe-button.tsx comingSoon` prop. Disabled buttons are visually disabled from first paint and explain why in Arabic.
|
||||||
|
- **Badges/pills**: access badge «مجاني» / «للأعضاء»; content-type badge (note/tutorial/case_study/comparison); «وصول مبكر مجاني» champagne strip.
|
||||||
|
- **Notice strip**: the early-access notice on articles — champagne-tinted band, verbatim copy from PRD §3.2, never a modal.
|
||||||
|
- **Guide card**: surface card, dek line, meta row (category · reading minutes · «حُدّث»), champagne hover seam. Topic hubs and «ابدأ من هنا» reuse it.
|
||||||
|
- **Article body**: headings, paragraphs, lists, blockquote, code (LTR island, scrollable), **real `<table>`** for tabular data (never screenshot/ASCII), links `--champagne` underline.
|
||||||
|
- **Forms**: label above input, input bg `--ink-2`, border `--seam`, error text = the API's Arabic sentence verbatim, loading state disables + keeps label, success replaces form state («تم! سنراسلك عند تفعيل العضوية.»).
|
||||||
|
- **Tables renderer**: for Lexical table nodes — mobile-safe overflow wrapper.
|
||||||
|
|
||||||
|
New components: reuse an existing class first, then extend `design.css` (or the route's CSS) — do not add a component library (no shadcn/radui/MUI) and no Tailwind (decision, 2026-10-11).
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
- Durations 150–250ms, ease-out. Hover: border color + 1–2px translate. Entrances: fade/rise ≤8px.
|
||||||
|
- No parallax, no infinite loops, no scroll-hijack. Reduced-motion: honor `prefers-reduced-motion` for any added animation.
|
||||||
|
- Micro-interactions (like button) may pulse once on success — once, not looping.
|
||||||
|
|
||||||
|
## 7. Accessibility floor (enforced by `npx hyperframes check` on the brag video; treat as the site bar too)
|
||||||
|
|
||||||
|
- Contrast: body text ≥ 4.5:1, large text ≥ 3:1 (`--stone` on `--basalt` passes; `--dim` only ≥1.5rem or decorative).
|
||||||
|
- Focus visible: 2px `--champagne` outline on interactive elements.
|
||||||
|
- Every image has meaningful Arabic `alt` (covers: the guide title); icon buttons have `aria-label`.
|
||||||
|
- Landmarks: header/nav/main/footer; one H1 per page; heading order never skips.
|
||||||
|
- Forms: `<label for>`, `aria-live="polite"` status regions (existing pattern: `#notify-done`).
|
||||||
|
|
||||||
|
## 8. Cover images
|
||||||
|
|
||||||
|
Generated via `npm run covers` (`scripts/generate-covers.mjs`) — 1200×630, ink/champagne system, Arabic title shaping checked. og:image dimensions are asserted in tests/evidence (1200×630). Don't hand-drop covers of other sizes.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# PRD — أنور بآي (anwarpy.ai)
|
||||||
|
|
||||||
|
> Owner: Ahmed (anwarpy). Maintainer of record: this document. Last revised: 2026-10-11.
|
||||||
|
> Status vocabulary in this doc: **MVP** = shipped 2026-10-10 (verified, see `docs/launch/2026-10-10-launch-readiness.md` §11). **Roadmap** = committed, not built. **Out of scope** = we will not build it; do not ask.
|
||||||
|
|
||||||
|
## 1. Vision
|
||||||
|
|
||||||
|
Anwarpy.ai is an Arabic-first library of practical AI tips and tricks — automation, agentic workflows, models, tooling — whose single goal is: **help the Arabic reader actually use AI and keep discovering new tricks.** Arabic engineers, founders, and curious professionals learn to *understand* AI and then *build* with it: «افهم الذكاء الاصطناعي، ثم ابنِ به.»
|
||||||
|
|
||||||
|
Three product principles, all enforced server-side:
|
||||||
|
|
||||||
|
1. **Honesty over theater.** Reading times are computed from real content, errors say what failed in real Arabic, the paywall never lies about what the visitor can read.
|
||||||
|
2. **The reader is sovereign.** During free early access every published guide — including membership-tier ones — is fully readable. No dark patterns, no fake scarcity, no confirm-password, no CAPTCHA.
|
||||||
|
3. **Machines are readers too.** Guides are served clean to humans and to AI crawlers (robots.txt policy, llms.txt/llms-full.txt, honest JSON-LD).
|
||||||
|
|
||||||
|
## 2. Personas
|
||||||
|
|
||||||
|
| Persona | Who | What they get |
|
||||||
|
|---|---|---|
|
||||||
|
| **The Curious Reader** | Arabic speaker googling «كيف أبني وكيل ذكاء اصطناعي» | Free guides, topic hubs, honest reading times, notify list |
|
||||||
|
| **The Practitioner** | Engineer/founder implementing agents at work | Advanced guides + work files during early access (free); after billing launches: the $10/mo membership (S-01…S-08) |
|
||||||
|
| **The Writer (Ahmed)** | Solo owner; writes in `/admin`, ships via the agent pipeline | Payload admin, agent API with secret scanning, moderation queue, comment management |
|
||||||
|
|
||||||
|
## 3. MVP — shipped 2026-10-10
|
||||||
|
|
||||||
|
The MVP is exactly what launched. Nothing below may regress without an explicit owner decision.
|
||||||
|
|
||||||
|
### 3.1 Reading & content
|
||||||
|
- **Guides** (`Posts`): Arabic titles/deks, Lexical bodies, categories وكلاء/نماذج/استرجاع/سلامة/أدوات/أعمال, topics (agents|models|automation|foundations), `contentType` badge (note/tutorial/case_study/comparison), `authorName` byline, «حُدّث» freshness, computed `readingMinutes` (160 wpm, written at save + backfilled honestly).
|
||||||
|
- **Topic hubs** `/topics/{agents,models,automation,foundations}` with sitemap entries.
|
||||||
|
- **Tables** in article bodies render as real tables (mobile-safe), not code blocks.
|
||||||
|
- **AI/SEO surfaces**: robots.txt allows the 10 named AI crawlers; `/llms.txt` index; `/llms-full.txt` full bodies; JSON-LD Article + BreadcrumbList (+ FAQPage on /subscription, Organization on home); `isAccessibleForFree` reflects reality.
|
||||||
|
- **Downloads**: guide attachments gated by the same access policy as their post (per-file `access`).
|
||||||
|
|
||||||
|
### 3.2 Access policy (the core contract)
|
||||||
|
- `LAUNCH_MODE` server-side policy (`web/src/lib/launch.ts`): `free_early_access` = every published post serves its full body; billing routes honestly 503 in Arabic. `paid` = membership-tier (`access: 'vip'`) posts paywall with H2-preview + fine print.
|
||||||
|
- The *intended tier* (`access: free|vip`) is data; the *effective access* is policy. Never rewrite `access` to simulate a launch state.
|
||||||
|
- Ghost rule: any verification must be done as a logged-out visitor reading a VIP post.
|
||||||
|
|
||||||
|
### 3.3 Accounts & trust
|
||||||
|
- Email+password signup (no confirm-password field), verification email with token, honest 429 lockout message, duplicate-email inline error, email-typo suggestion, live password hint, reassurance microcopy.
|
||||||
|
- Verification state is written at signup (`verification_token`); verify-email flow supports changing the email.
|
||||||
|
- Account settings + onboarding (interest/level) exist; `getViewer()` resolves the session.
|
||||||
|
|
||||||
|
### 3.4 Notify list
|
||||||
|
- `/api/notify` records every submission; sends a confirmation mail when transport is live; honest 503 «خدمة البريد غير مربوطة بعد.» when Cloudflare Email Sending is off (row still kept); duplicate emails return `alreadySubscribed: true` without re-mailing.
|
||||||
|
|
||||||
|
### 3.5 Comments (decided 2026-10-11 — in MVP scope)
|
||||||
|
- Any **logged-in** user may comment on a post — free or VIP, tier does not matter. **Anonymous visitors cannot comment** (API rejects unauthenticated; UI prompts login).
|
||||||
|
- Moderated: comments enter a non-public status; the author identity is **forced server-side** from the session (client cannot spoof `author`), display name from `authorName` (ADR-0002).
|
||||||
|
- Route contract: not a public CRUD API — comment creation is a server-validated route bound to the post (ADR-0001).
|
||||||
|
- Admin moderation lives in `/admin` (status transitions: pending → published / rejected).
|
||||||
|
|
||||||
|
### 3.6 Agent pipeline (authoring automation)
|
||||||
|
- `POST/PUT /api/agent/posts` with `x-agent-key` header; strips `access`/`readingMinutes` from accepted fields; `sourceId` dedup (idempotent re-runs); secret scanning (`scanAgentPayload`) rejects AWS keys, PEM blocks, `sk-` keys, GitHub tokens, generic credentials with **422 + pattern names only**; string `content` is a 400 (Lexical JSON required); `publish: true` is explicit opt-in, PUT cannot set `status`.
|
||||||
|
- Media/Files ingestion helpers (`agent-media.ts`, `agent-files.ts`).
|
||||||
|
|
||||||
|
### 3.7 Interactions
|
||||||
|
- One like per logged-in user per post (`PostLikes`, uniqueness enforced), public like counter; anonymous view counting on `/api/posts/[slug]/view`.
|
||||||
|
|
||||||
|
### 3.8 Ops floor
|
||||||
|
- `web/scripts/backup-db.sh` + documented restore rehearsal (VPS).
|
||||||
|
- Feedback = `mailto:hello@anwarpy.ai` in the footer.
|
||||||
|
- Deployment: rsync → `docker compose -f docker-compose.vps.yml up -d --build` on the VPS; migrations apply on boot.
|
||||||
|
|
||||||
|
## 4. Roadmap (committed, phased)
|
||||||
|
|
||||||
|
### Phase 2 — Trust & legal
|
||||||
|
- **L-03 /terms + /privacy**: real content from owner facts recorded in `docs/launch/owner-inputs-terms-privacy.md`; effective date; §9 items 1–7. Acceptance: pages live, linked in footer, listed in sitemap.
|
||||||
|
- **Comments shipping polish**: notification to writer on new pending comment; abuse valve (delete + block author) in admin.
|
||||||
|
|
||||||
|
### Phase 3 — Money & measurement (gate: S-01…S-08 checklist §6)
|
||||||
|
- **Stripe paid tier S-01…S-08**: checkout, webhook (`/api/stripe/webhook` exists; `stripeCustomerId` on Users), `vip-onboarding`, payment success/cancel pages; flip = `LAUNCH_MODE=paid` policy change only. Paywall CRO (H2 preview + fine print + #faq) already built.
|
||||||
|
- **L-11 analytics**: vendor decision (owner); event list is pre-agreed in the launch checklist.
|
||||||
|
- **L-12 editorial distribution**: owner-driven announcement; no agent-posted content.
|
||||||
|
|
||||||
|
### Phase 4 — Product growth (owner-added 2026-10-11)
|
||||||
|
- **Mailing list v2**: move from notify rows to a real newsletter (send digests of new guides; unsubscribe honoring; Cloudflare Email transport).
|
||||||
|
- **Direct messaging with the writer**: logged-in members can message Ahmed from within the site; inbox in `/admin`; rate-limited; no exposure of his personal email.
|
||||||
|
- **In-post tool-finder chatbot**: assistant that reads the site's own posts (llms-full corpus) and points the reader to the right guide/tool inside anwarpy.ai; Arabic-first; cites posts; never invents tools. Must reuse the same access policy (never leak VIP bodies to unauthenticated users through the bot).
|
||||||
|
|
||||||
|
## 5. Strictly out of scope
|
||||||
|
|
||||||
|
Do not build, do not scaffold, do not "prepare" these:
|
||||||
|
|
||||||
|
- OAuth / Passkeys / SMS or WhatsApp login in the MVP — **anticipated evolution** (owner, 2026-10-11): Google auth or Clerk may be added later for easy integration; keep `viewer.ts` as the single session seam so providers can swap behind it.
|
||||||
|
- Native mobile apps.
|
||||||
|
- Multi-tenant or white-label anything; a second site on the same deploy.
|
||||||
|
- Public third-party REST API beyond what exists (agent API is for the owner's pipeline only).
|
||||||
|
- User-generated uploads of arbitrary files; media uploads stay author/admin-only.
|
||||||
|
- CAPTCHA, aggressive rate limiting beyond the existing login lockout (owner decision, recorded).
|
||||||
|
- Real-time anything (WebSockets/presence) until Phase 4 messaging is designed.
|
||||||
|
- Any rewrite of `access` fields to simulate launch states.
|
||||||
|
|
||||||
|
## 6. Success metrics
|
||||||
|
|
||||||
|
Available now (no vendor dependency): notify signups, verified-account ratio, likes per post, views per post, comment quality (published/rejected ratio). Post-L-11: activation (first guide read), return rate, member conversion once billing is live.
|
||||||
|
|
||||||
|
## 7. Acceptance culture
|
||||||
|
|
||||||
|
A checkbox without evidence is not verification. Every claim in the launch doc has a command + observed output. This PRD inherits that rule: features in §3 are verified by the §11 evidence; new features get the same treatment (ghost-read checks, honest-error checks, row-count checks).
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
# SECURITY — anwarpy.ai
|
||||||
|
|
||||||
|
> Threat model: a solo-writer content site. The assets are the content, the member trust (ADR-0005), and the pipeline key. Not a fintech app — but every rule below is load-bearing.
|
||||||
|
|
||||||
|
## 1. Authentication
|
||||||
|
|
||||||
|
- **Payload auth** (email + password). Signup writes `verification_token` + `emailVerified` flag at creation (ADR-0005 trust model); verification/resend routes: `/api/auth/verify-email`, `/api/auth/resend-verification`. Forgot/reset via Payload's built-in flow.
|
||||||
|
- **Session seam**: `src/lib/viewer.ts` `getViewer()` is the only place components read identity. If auth provider changes (anticipated: Google OAuth or Clerk — owner decision 2026-10-11), it changes behind this seam; callers keep consuming `Viewer`.
|
||||||
|
- **No password rules theater**: one live hint (strength guidance) at signup; no confirm-password field (owner decision, recorded).
|
||||||
|
- **Lockout**: login attempts on `/api/users` are rate-limited by `src/middleware.ts`; the 429 message stays honest («too many attempts» fidelity — step 14 evidence). No CAPTCHA (owner decision, recorded 2026-10-10).
|
||||||
|
- Verification emails go through **Cloudflare Email REST** (ADR-0003, never SMTP). Transport down → the UI/API says so in Arabic («خدمة البريد غير مربوطة بعد.»); it does not pretend success.
|
||||||
|
|
||||||
|
## 2. Environment variables & secrets
|
||||||
|
|
||||||
|
| Var | Scope | Rule |
|
||||||
|
| --------------------------------------------------------------- | -------- | --------------------------------------------------- |
|
||||||
|
| `DATABASE_URI` / `DB_PASSWORD` | server | compose builds the URI; never commit, never echo |
|
||||||
|
| `PAYLOAD_SECRET` | server | signing/encryption; rotate = invalidate sessions |
|
||||||
|
| `AGENT_API_KEY` | server | pipeline auth; header `x-agent-key` |
|
||||||
|
| `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`, `MAIL_FROM` | server | mail transport |
|
||||||
|
| `LAUNCH_MODE` | server | launch policy (SECURITY-relevant: gates paid flows) |
|
||||||
|
| `NEXT_PUBLIC_SITE_URL` | public | canonical/og origin |
|
||||||
|
| `STRIPE_SECRET_KEY`, `STRIPE_PRICE_ID`, `STRIPE_WEBHOOK_SECRET` | server | Phase 3 |
|
||||||
|
| `SEED_TEST_USERS` | dev only | never set in prod |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `.env*` files are gitignored and **excluded from rsync deploys**; prod secrets live only in `/root/anwarpi/.env.production`.
|
||||||
|
- Never print secret values into transcripts, docs, videos, or evidence. Known incident: 8 chars of prod `DB_PASSWORD` leaked in a session transcript on 2026-10-10 → **owner to rotate**; rotation is the standing remedy for any partial leak.
|
||||||
|
- Read secrets at runtime via `process.env` inside server modules only; never in `'use client'` code (only `NEXT_PUBLIC_*` there).
|
||||||
|
|
||||||
|
## 3. RBAC & content access
|
||||||
|
|
||||||
|
- Two tiers: `role: 'admin'` (Ahmed, `/admin` + agent pipeline ownership) and everyone else (member/public). No roles table beyond that until a real need.
|
||||||
|
- **Content ACL is the crown jewel** (ARCHITECTURE §3): `Posts.content` field-level read rule — public iff `status=published && (access=free || freeEarlyAccess())`. VIP previews (H2 list, dek) are the only VIP fields served to logged-out users when billing is on.
|
||||||
|
- Files: `GET /api/files/[id]` enforces **per-file access** before streaming; the download gate and the article page must agree (one decision, ARCHITECTURE §3).
|
||||||
|
- Admin UI access = Payload's built-in `admin` gate; never hand-roll admin pages on the frontend.
|
||||||
|
|
||||||
|
## 4. Input handling & pipeline hardening
|
||||||
|
|
||||||
|
- **`pickFields`** on the agent API strips privileged fields (`access`, `readingMinutes`, `status` except explicit `publish: true` opt-in on POST; PUT cannot set `status`).
|
||||||
|
- **`scanForSecrets` / `scanAgentPayload`** (`src/lib/secret-scan.ts`): POST and PUT scan the _real text_ (Lexical → `lexicalToText`, or the raw string) for AWS AKIA keys, PEM blocks, `sk-` API keys, GitHub tokens (`ghp_`…), generic credential shapes → **422 with pattern NAMES only**. Never echo the matched text, never log it.
|
||||||
|
- String `content` → 400 (Lexical JSON required); no silent placeholder substitution.
|
||||||
|
- Comment bodies: textarea, moderated (pending until approved), author forced server-side from the session — client-supplied `author`/`authorName` identity is ignored/overwritten (ADR-0002). Display names are a snapshot, not a live join, so renamed users can't retroactively forge old comments.
|
||||||
|
- All user-facing error strings are fixed Arabic sentences from the route handlers — errors never reflect raw input back (no echo = reflected-input hygiene).
|
||||||
|
|
||||||
|
## 5. Transport & external calls
|
||||||
|
|
||||||
|
- Everything behind Traefik with TLS on anwarpy.ai; www → apex 308.
|
||||||
|
- Outbound: Cloudflare Email REST with scoped token; Stripe webhook verifies signature (`STRIPE_WEBHOOK_SECRET`) before trusting the body; agent API requires the key header (Authorization Bearer → 401).
|
||||||
|
- The db container publishes **no ports**; prod DB access for maintenance = SSH tunnel (AGENTS.md rulebook recipe), never a public 5432.
|
||||||
|
|
||||||
|
## 6. Operational security
|
||||||
|
|
||||||
|
- Backups: `web/scripts/backup-db.sh` → gzipped `pg_dump`; rehearsed restore 2026-10-10 (temp DB, count check, drop). Backup artifacts stay on the VPS `backups/` dir, never in git, never in the deploy rsync.
|
||||||
|
- Test rows (smoke signups, notify rows) use `@example.com` addresses and are **deleted after verification** — prod counts (`users=6`, `launch_notifications=0`) are the post-cleanup ground truth.
|
||||||
|
- Media/files volumes are excluded from rsync (`no --delete` protects them).
|
||||||
|
- Incident rule: any suspected leak (secret in transcript/log/URL) → rotate the credential first, then investigate. Recording the incident in docs is required; pretending it didn't happen is not.
|
||||||
|
|
||||||
|
## 7. What NOT to add (recorded decisions)
|
||||||
|
|
||||||
|
- No CAPTCHA, no aggressive rate limits beyond login lockout (owner, 2026-10-10).
|
||||||
|
- No WAF/Cloudflare proxy changes without owner approval — the domain's DNS/proxy is Ahmed's console.
|
||||||
|
- No secrets in client bundles; no `NEXT_PUBLIC_` for anything except the site URL.
|
||||||
|
- No new auth provider without routing it through `viewer.ts` (see §1).
|
||||||
|
|
||||||
|
## 8. Accepted risks
|
||||||
|
|
||||||
|
### next@15.4.11 — 28 known CVEs, max 9.5 (risk-accepted by owner, 2026-10-13)
|
||||||
|
|
||||||
|
- **Why pinned**: `@payloadcms/next@3.90.2` peer-supports next `<15.5.0 || >=16.3.3` — no Payload 3.x release supports 15.5.x, so the safe ladder is 15.4.11 → (Payload 4 or a verified Next 16 migration). Forcing 15.5.x would run the CMS on an unsupported combo.
|
||||||
|
- **Exposure analysis**: the 9.5-critical (Image Optimizer DoS, GHSA-9g9p-9gw9-jx7f) requires attacker-reachable `remotePatterns` — this app configures none (local covers only). The cache-poisoning advisory targets SSG/ISR caching; the site is predominantly dynamic SSR via the Payload Local API.
|
||||||
|
- **Revisit trigger**: any of — Payload publishes 15.5/16 peer support (then upgrade next in one verified PR), Payload 4 adoption, or a new next advisory with remote-code-execution class impact.
|
||||||
|
- **Gate note**: `procoder check` re-flags this on any `package-lock.json` change. If you must touch the lockfile before upgrading, either upgrade next in the same PR or skip the gate once with the reason in the commit message. Transitive CVE fixes already applied: undici 8.11.2, sharp 0.35.5, postcss 8.5.30, sass 1.105.1.
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
# TESTING — anwarpy.ai
|
||||||
|
|
||||||
|
> Owner decision 2026-10-11: **tier (b) is the bar** — Vitest unit tests for the core libs + regression tests for API route behavior. Tier (c) — Playwright E2E for critical journeys — activates when Phase 3 (Stripe) lands; journeys to cover then: signup→verify→login, VIP ghost-read (billing on), checkout happy path, webhook apply.
|
||||||
|
|
||||||
|
## 1. Standing gates (before any commit/merge)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web && npx tsc --noEmit # must exit 0
|
||||||
|
cd web && npm run build # must exit 0 (includes generate:types)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both green = minimum. Gates alone are not proof of behavior — see §3.
|
||||||
|
|
||||||
|
## 2. Unit tests (Vitest, tier b)
|
||||||
|
|
||||||
|
- Add `vitest` as a dev dependency; tests live beside the code or under `web/src/lib/__tests__/` — pick one location and stay consistent (recommend `__tests__/` next to `src/lib`).
|
||||||
|
- **Required suites (the known-risk list):**
|
||||||
|
|
||||||
|
| Suite | Must pin |
|
||||||
|
|---|---|
|
||||||
|
| `launch.test.ts` | policy truth table: `billingEnabled()` false when `LAUNCH_MODE` unset/**free_early_access**/**paid**… — including the fail-safe default |
|
||||||
|
| `lexical.test.ts` | `lexicalToText`/`countLexicalWords` **descend into `content.root`** (regression: the 2026-10-10 launch-blocking bug where both returned 0/'' on real serialized trees); `extractH2s`, `lexicalToPlain` |
|
||||||
|
| `secret-scan.test.ts` | one case per pattern (aws_access_key, pem_private_key, openai_api_key, github_token, generic_credential) returns **pattern name only, never the matched text**; clean payload passes; string content is scanned too |
|
||||||
|
| `topics.test.ts` | `isTopic` accepts exactly agents/models/automation/foundations; `topicsFor` mapping |
|
||||||
|
| `format.test.ts` | `guidesLabel` (1 دليل، 2 دليلان، 3–10 أدلة، 11+), `minutesLabel`, `MEMBERSHIP_PRICE` |
|
||||||
|
|
||||||
|
- Rule: tests assert **behavior**, not implementation text. No snapshot-of-source, no tautologies (`expect(fn).toBeDefined()`), no mock-echo tests, no bare "does not throw".
|
||||||
|
- Runtime env manipulation: use `vi.stubEnv`/`unstubAllEnvs` — never let one test's `LAUNCH_MODE` leak into another.
|
||||||
|
|
||||||
|
## 3. API route regression tests
|
||||||
|
|
||||||
|
Behavior tests for the routes with contracts (per PRD §3.6, SECURITY §4):
|
||||||
|
|
||||||
|
- `POST/PUT /api/agent/posts`: 401 wrong/missing key (note: **`x-agent-key`**, not Bearer); 400 on string `content`; 422 with pattern names on secret-bearing Lexical; `sourceId` dedup returns existing post (no 409, no duplicate row); `pickFields` strips `access`/`readingMinutes`; PUT cannot set `status`.
|
||||||
|
- `POST /api/subscribe`: 503 + exact Arabic body while billing is off.
|
||||||
|
- `POST /api/notify`: records row then honest 503 when mail transport fails; duplicate email → 200 `alreadySubscribed: true`; no double-send on dup.
|
||||||
|
- Comments route: unauthenticated → reject; client-sent `author` ignored (server-forced).
|
||||||
|
- Likes: 401 anonymous; second like by same user → no duplicate row.
|
||||||
|
|
||||||
|
Implementation: pure logic moves into `src/lib` helpers where possible and gets unit-tested; handler-level tests run the route module with a mocked Payload local API (or hit a dev server for the smoke tier — §5). Prefer extracting decision logic over wiring up heavy mocks.
|
||||||
|
|
||||||
|
## 4. What must NOT become a test
|
||||||
|
|
||||||
|
- Wiring/copies/forwarding, mock echoes, source-text greps, "file exists".
|
||||||
|
- Incidental defaults that aren't contracts (an unstyled div's class list is not a spec).
|
||||||
|
- Anything already verified cheaper by the ghost-read/smoke script in §5.
|
||||||
|
- Per the gstack rule: existing tests that pin wording/implementation instead of behavior get deleted, not "fixed".
|
||||||
|
|
||||||
|
## 5. Smoke & verification scripts (throwaway, not committed)
|
||||||
|
|
||||||
|
- Prod verification is HTTP-level and scripted ad hoc (curl batches): ghost-read a VIP post logged out; check `isAccessibleForFree`, og:image 1200×630, JSON-LD types; subscribe 503; notify row + cleanup; sitemap contents; www 308. Use `@example.com` rows and **delete them after** (ground truth: users=6, launch_notifications=0).
|
||||||
|
- Keep the failing-before/passing-after pair when fixing a bug — a permanent regression test in §2/§3 if it's pure logic, the script transcript if it's deploy/config.
|
||||||
|
|
||||||
|
## 6. Before-merge checklist (condensed)
|
||||||
|
|
||||||
|
1. `tsc --noEmit` + `npm run build` exit 0.
|
||||||
|
2. New/changed lib behavior → matching Vitest suite updated or added.
|
||||||
|
3. Changed route contract → §3 regression updated (status + body shape).
|
||||||
|
4. Changed access logic → ghost-read check (logged-out VIP post) recorded in the PR.
|
||||||
|
5. No new `any`, no new `'use client'` without justification, no new dependency without a line of justification (CODE_STYLE §8).
|
||||||
@@ -329,15 +329,19 @@ Companion document: anwarpy-blog-publishing-agent-prompt.md. The prompt controls
|
|||||||
|
|
||||||
Launch implemented on branch `launch/early-access-2026-10-10` (step references = execution plan). `LAUNCH_MODE=free_early_access` is the compose default; billing stays off until owner approval. Per the checklist rule, a checkbox without evidence is not verification — prod-verify items below are re-checked at step 48 and the lines updated.
|
Launch implemented on branch `launch/early-access-2026-10-10` (step references = execution plan). `LAUNCH_MODE=free_early_access` is the compose default; billing stays off until owner approval. Per the checklist rule, a checkbox without evidence is not verification — prod-verify items below are re-checked at step 48 and the lines updated.
|
||||||
|
|
||||||
- **L-01 (paid claims off)** — Steps 8–11, 21, 22: `/subscription` shows an early-access panel + working notify form; all subscribe CTAs render a disabled «قريباً» button when billing is off (`subscribe-button.tsx` comingSoon prop); `POST /api/subscribe` returns 503 server-side with the exact Arabic error; home membership copy swept for purchase claims. *Prod re-check: step 48.*
|
- **L-01 (paid claims off)** — Steps 8–11, 21, 22: `/subscription` shows an early-access panel + working notify form; all subscribe CTAs render a disabled «قريباً» button when billing is off (`subscribe-button.tsx` comingSoon prop); `POST /api/subscribe` returns 503 server-side with the exact Arabic error; home membership copy swept for purchase claims. **Prod-verified 2026-10-10: `POST https://anwarpy.ai/api/subscribe` → `503 {"error":"الدفع غير مفعّل خلال فترة الوصول المبكر."}`.**
|
||||||
- **L-02 (launch-free access policy)** — Steps 1–6: `src/lib/launch.ts` policy (`LAUNCH_MODE`, fail-safe default = free early access, no date expiry), field-level `content` read ACL in `Posts.ts` (public iff `status=published && (access=free || early)`) — same decision consumed by pages, `/api/posts/[slug]`, file gate, and llms-full. Intended tier (`access`) never rewritten. *Verified: migration chain + step 45/48 ghost-read.*
|
- **L-02 (launch-free access policy)** — Steps 1–6: `src/lib/launch.ts` policy (`LAUNCH_MODE`, fail-safe default = free early access, no date expiry), field-level `content` read ACL in `Posts.ts` (public iff `status=published && (access=free || early)`) — same decision consumed by pages, `/api/posts/[slug]`, file gate, and llms-full. Intended tier (`access`) never rewritten. **Prod-verified: ghost-read of intended-VIP `/posts/agent-loop-stop-condition` serves the full body + early-access notice, zero paywall markers, `isAccessibleForFree: true`.**
|
||||||
- **L-03 (terms/privacy)** — **NOT DONE — owner facts pending.** `docs/launch/owner-inputs-terms-privacy.md` lists exactly what Ahmed must supply; `/terms` and `/privacy` untouched until then.
|
- **L-03 (terms/privacy)** — **NOT DONE — owner facts pending.** `docs/launch/owner-inputs-terms-privacy.md` lists exactly what Ahmed must supply; `/terms` and `/privacy` untouched until then.
|
||||||
- **L-04 (reading/download QA)** — Step 45 local smoke + step 48 prod QA (desktop 1280px, mobile 390px Chromium, home → library → topic hub → article → download). Safari and Telegram in-app browsers are owner-manual tests.
|
- **L-04 (reading/download QA)** — Step 45 local smoke + step 48 prod QA at HTTP level (home → library → topic hub → article; reading time «دقيقتان», no horizontal-marker regressions observed in markup). Safari and Telegram in-app browsers are owner-manual tests. A full visual browser pass remains open.
|
||||||
- **L-05 (private notes / pipeline permissions)** — Steps 28–30: agent API strips `access`/`readingMinutes` from accepted fields; `sourceId` dedup returns the existing post (no duplicate); `scanForSecrets` 422s payloads matching AWS AKIA / PEM / sk- / ghp_ / generic-credential patterns (pattern names only in the response); publish stays an explicit opt-in and PUT cannot set `status`.
|
- **L-05 (private notes / pipeline permissions)** — Steps 28–30: agent API strips `access`/`readingMinutes` from accepted fields; `sourceId` dedup returns the existing post (no duplicate); `scanForSecrets` 422s payloads matching AWS AKIA / PEM / sk- / ghp_ / generic-credential patterns (pattern names only in the response); publish stays an explicit opt-in and PUT cannot set `status`. **Post-copy fixes found in smoke, included in the merged commit:** `lexicalToText`/`countLexicalWords` never unwrapped the Lexical `root` (scan was inert on real content — fixed + verified 422), string `content` was silently replaced with a placeholder (now 400), and llms-full never requested `content` (`fetchIndexDocs(true)` — prod llms-full now 49 KB with bodies).
|
||||||
- **L-06 (feedback, backup, rollback)** — Step 40: `scripts/backup-db.sh` + restore rehearsal on the VPS (2026-10-10): `pg_dump` → gzip 25K, restored into `anwarpi_restore_test` with 0 SQL errors, counts `posts=15, users=6, payload_migrations=6`, temp DB dropped. Feedback: footer `mailto:hello@anwarpy.ai` (opens the visitor's mail client; no server dependency). Production mail transport test via the same Cloudflare Email Sending REST call the app uses → `10203 email.sending_disabled` — **the account feature is off; transactional mail (verification, notify confirmations) will honestly report failure until the owner enables Email Sending**. Monitoring/uptime tooling: owner decision, recorded not built.
|
- **L-06 (feedback, backup, rollback)** — Step 40: `scripts/backup-db.sh` + restore rehearsal on the VPS (2026-10-10): `pg_dump` → gzip 25K, restored into `anwarpi_restore_test` with 0 SQL errors, counts `posts=15, users=6, payload_migrations=6`, temp DB dropped. Feedback: footer `mailto:hello@anwarpy.ai` (opens the visitor's mail client; no server dependency). Production mail transport test via the same Cloudflare Email Sending REST call the app uses → `10203 email.sending_disabled` — **the account feature is off; transactional mail (verification, notify confirmations) will honestly report failure until the owner enables Email Sending** (prod `/api/notify` honestly 503s and still records the row). Monitoring/uptime tooling: owner decision, recorded not built.
|
||||||
- **L-07 (labels + reading estimates)** — Steps 24–26: `contentType` (note/tutorial/case_study/comparison) badge, `authorName` byline, «حُدّث» freshness line; `readingMinutes` computed at write time via `countLexicalWords`/`READING_WORDS_PER_MINUTE` (160 wpm) with `scripts/backfill-reading-minutes.ts` (13 scanned, 0 drift on seeded content).
|
- **L-07 (labels + reading estimates)** — Steps 24–26: `contentType` badge, `authorName` byline, «حُدّث» freshness line; `readingMinutes` computed at write time via `countLexicalWords`/`READING_WORDS_PER_MINUTE` (160 wpm). **Prod backfill run 2026-10-10 (local tsx over an SSH tunnel to the prod DB — the standalone container has no `scripts/`): 15 scanned, 15 updated** (old estimates were inflated: 18→2, 12→2, 10→1, …).
|
||||||
- **L-08 (metadata/domain hygiene)** — Steps 32, 37, 38, 39: Arabic descriptions on articles/subscription/about; sitemap gains `/topics/*` and drops `/login`,`/signup`; those pages (plus forgot-password) are `robots: noindex, follow`; Traefik `anwarpi-www` router 301s `https://www.anwarpy.ai/*` → apex with priority above the API router. *Prod re-check: `curl -sI https://www.anwarpy.ai/posts`, step 48.*
|
- **L-08 (metadata/domain hygiene)** — Steps 32, 37, 38, 39: Arabic descriptions on articles/subscription/about; sitemap gains `/topics/*` and drops `/login`,`/signup`; those pages (plus forgot-password) are `robots: noindex, follow`; Traefik `anwarpi-www` router permanently redirects `https://www.anwarpy.ai/*` → apex preserving path (prod-verified: `HTTP/2 308`, `location: https://anwarpy.ai/posts`). Sitemap prod-verified: 4 `/topics/*`, no `/login`,`/signup`. og:image 1200×630 + og:site_name present on articles.
|
||||||
- **L-09 (author/evidence/structured data)** — Step 33: Article JSON-LD with `image`, `dateModified`, `description`, `mainEntityOfPage`, publisher logo, `author` = Person (authorName) or Organization; `isAccessibleForFree` = actual current access (never a false paid Offer while billing is off); BreadcrumbList (home → topic hub → post); FAQPage on `/subscription`; Organization on home. Visible byline and schema share the same fields.
|
- **L-09 (author/evidence/structured data)** — Step 33: Article JSON-LD with `image`, `dateModified`, `description`, `mainEntityOfPage`, publisher logo, `author` = Person (authorName) or Organization; `isAccessibleForFree` = actual current access (never a false paid Offer while billing is off); BreadcrumbList (home → topic hub → post); FAQPage on `/subscription`; Organization on home. Visible byline and schema share the same fields.
|
||||||
- **L-10 (publishing workflow/dedup)** — Steps 24, 28–31: agent creates drafts (`status` forced draft unless `publish: true`), `sourceId` unique-ish dedup before slug-409, secret scan on POST/PUT.
|
- **L-10 (publishing workflow/dedup)** — Steps 24, 28–31: agent creates drafts (`status` forced draft unless `publish: true`), `sourceId` unique-ish dedup before slug-409, secret scan on POST/PUT.
|
||||||
- **L-11 (measurement)** — **NOT BUILT — owner decision** (analytics vendor; recorded only). Suggested events remain in the checklist.
|
- **L-11 (measurement)** — **NOT BUILT — owner decision** (analytics vendor; recorded only). Suggested events remain in the checklist.
|
||||||
- **L-12 (content/distribution)** — **Owner/editorial.** No announcement drafted or sent by the agent.
|
- **L-12 (content/distribution)** — **Owner/editorial.** No announcement drafted or sent by the agent.
|
||||||
|
|
||||||
|
### Prod verification 2026-10-10 (post-deploy)
|
||||||
|
|
||||||
|
Deployed to `root@147.79.102.201:/root/anwarpi` via rsync + `docker compose -f docker-compose.vps.yml up -d --build`; boot applied migration `20261010_142214`; `LAUNCH_MODE=free_early_access` set in `.env.production`. Verified live: home/admin 200; ghost VIP read full-body; subscribe 503 honest; notify honest-503 + row persisted (row deleted after); signup 201 + `verification_token` written (row deleted after); robots lists the 10 named AI crawlers; `/llms.txt` + `/llms-full.txt` (49 KB, bodies); `/topics/agents` 200; sitemap 4 topics, no auth pages; www 308 → apex; Article + BreadcrumbList JSON-LD; reading minutes honest («دقيقتان»). PR #3 merged to `main` (638a590).
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Publish and manage blog posts programmatically. Auth: send the agent key in the
|
|||||||
## Create + publish a post with an image
|
## Create + publish a post with an image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
KEY=033d1eed44ca6abf483de10735c47155d38f7b5bc9d46449
|
KEY="$AGENT_API_KEY" # never hardcode — export AGENT_API_KEY in your shell; it lives in the server's .env.production
|
||||||
|
|
||||||
# Inline base64 image (jpeg/png/webp, ≤5MB)
|
# Inline base64 image (jpeg/png/webp, ≤5MB)
|
||||||
python3 - <<'EOF' > /tmp/post.json
|
python3 - <<'EOF' > /tmp/post.json
|
||||||
@@ -54,11 +54,20 @@ Response `201`:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": 113, "title": "دليلي الجديد", "slug": "my-new-guide",
|
"id": 113,
|
||||||
"status": "published", "access": "vip", "category": "agents",
|
"title": "دليلي الجديد",
|
||||||
|
"slug": "my-new-guide",
|
||||||
|
"status": "published",
|
||||||
|
"access": "vip",
|
||||||
|
"category": "agents",
|
||||||
"cover": { "id": 113, "url": "/api/media/file/my-cover.jpg" },
|
"cover": { "id": 113, "url": "/api/media/file/my-cover.jpg" },
|
||||||
"attachments": [
|
"attachments": [
|
||||||
{ "id": 42, "filename": "agent-state.md", "access": "vip", "url": "/api/files/42" }
|
{
|
||||||
|
"id": 42,
|
||||||
|
"filename": "agent-state.md",
|
||||||
|
"access": "vip",
|
||||||
|
"url": "/api/files/42"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Minimal flat config — procoder's lint domain checks .ts/.tsx with this.
|
||||||
|
// Report-only by default (see .procoder/config.toml policy); add rules as needed.
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
".next/**",
|
||||||
|
"node_modules/**",
|
||||||
|
"src/payload-types.ts", // generated — do not lint
|
||||||
|
"src/migrations/**",
|
||||||
|
"media/**",
|
||||||
|
"files-uploads/**",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...tseslint.configs.recommended.map((c) => ({
|
||||||
|
...c,
|
||||||
|
files: ["**/*.{ts,tsx,mjs,cjs}"],
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Generated Payload types use `any` in payload-types.ts; local code may pass
|
||||||
|
// narrow unknowns. Keep the floor permissive until a lint pass is scheduled.
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
Generated
+3395
-870
File diff suppressed because it is too large
Load Diff
+14
-2
@@ -12,6 +12,7 @@
|
|||||||
"covers": "node scripts/generate-covers.mjs"
|
"covers": "node scripts/generate-covers.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clerk/nextjs": "^7.9.14",
|
||||||
"@payloadcms/db-postgres": "^3.60.0",
|
"@payloadcms/db-postgres": "^3.60.0",
|
||||||
"@payloadcms/next": "^3.60.0",
|
"@payloadcms/next": "^3.60.0",
|
||||||
"@payloadcms/richtext-lexical": "^3.60.0",
|
"@payloadcms/richtext-lexical": "^3.60.0",
|
||||||
@@ -19,17 +20,28 @@
|
|||||||
"payload": "^3.60.0",
|
"payload": "^3.60.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"sharp": "^0.34.0",
|
"sharp": "^0.35.5",
|
||||||
"stripe": "^18.5.0"
|
"stripe": "^18.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.64.0",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@types/nodemailer": "^8.0.2",
|
"@types/nodemailer": "^8.0.2",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"typescript": "^5.9.0"
|
"eslint": "^10.12.0",
|
||||||
|
"prettier": "^3.9.10",
|
||||||
|
"typescript": "^5.9.0",
|
||||||
|
"typescript-eslint": "^8.71.2",
|
||||||
|
"vitest": "^5.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"undici": "^8.11.2",
|
||||||
|
"sharp": "^0.35.5",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"sass": "^1.79.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user