Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.5.27 (CVE-driven bump from 15.4.11, owner-approved 2026-10-13) |
|
||||||
|
| 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).
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
# 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).
|
||||||
+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
+3657
-904
File diff suppressed because it is too large
Load Diff
+15
-3
@@ -12,24 +12,36 @@
|
|||||||
"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",
|
||||||
"next": "15.4.11",
|
"next": "^15.5.27",
|
||||||
"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