Skip to content

Commit 948333c

Browse files
author
VKirill
committed
chore(wiki+docs): full wiki INIT + CLAUDE.md derive
1 parent a88e0df commit 948333c

39 files changed

Lines changed: 4974 additions & 0 deletions

CLAUDE.md

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# ПодписачЪ
2+
3+
Self-hosted attribution for Telegram and MAX channel subscriptions. The repo is a pnpm/Turborepo monorepo with a Nuxt admin app, a separate bot worker, and a shared contracts package; both runtimes write to one PostgreSQL database through Prisma ([wiki/overview.md](wiki/overview.md)).
4+
5+
**Stack:** Node 22 · Nuxt 4 SPA + Nitro · grammY (Telegram) + MAX polling · PostgreSQL 16 + Prisma 7 · `@ps/shared` (Zod, AES-256-GCM) · pnpm + Turborepo · Docker Compose
6+
7+
> Auto-generated from `wiki/`. Edit the wiki, not this file. Manual edits are overwritten on next derive.
8+
9+
## Critical invariants
10+
11+
Violating any of these causes data loss, downtime, or silent corruption. Read before every code change.
12+
13+
1. **Never run `docker compose down -v`** — the `pgdata` named volume is the only durable store for Settings, encrypted tokens, channels, visits, subscribers, reports, and conversions (`docker-compose.yml:50-62`, [wiki/gotchas.md](wiki/gotchas.md#docker-compose-down--v-deletes-the-only-durable-data-volume)).
14+
2. **`Settings.internalApiSecret` is both auth credential and AES key material.** Rotation must re-encrypt every encrypted bot/integration field in the same migration; otherwise bot startup token decryption breaks (`packages/shared/src/crypto.ts:8-55`, `apps/bot/src/config/index.ts:49-61`, [wiki/gotchas.md](wiki/gotchas.md#rotating-internalapisecret-can-make-encrypted-tokens-unreadable)).
15+
3. **Public report cookie check is presence-only (P0 bug).** A forged `report-session-<token>=1` cookie unlocks password-protected reports — fix before extending report visibility (`apps/web/server/api/reports/[token].get.ts:20-29`, [wiki/active-tasks.md](wiki/active-tasks.md#harden-public-report-password-sessions)).
16+
4. **GA `apiSecret` is stored as plaintext JSON in `Integration.config` (P0 bug).** Use `encrypt(value, settings.internalApiSecret)` like Yandex tokens (`apps/web/server/api/integrations/ga/index.post.ts:10-23`, [wiki/gotchas.md](wiki/gotchas.md#google-analytics-api-secrets-are-stored-in-plaintext-json)).
17+
5. **Schema changes require a checked-in Prisma migration.** `migrate deploy` runs in `apps/web/docker-entrypoint.sh:10-12`; editing only `schema.prisma` will not apply in production ([wiki/gotchas.md](wiki/gotchas.md#migration-sql-is-the-real-deploy-artifact-not-only-schemaprisma)).
18+
6. **Admin APIs are session-protected by default; `/api/internal/**` requires Bearer `internalApiSecret`.** New routes outside `PUBLIC_PREFIXES` get auth automatically (`apps/web/server/middleware/auth.ts:3-29`, `apps/web/server/middleware/internal.ts:3-16`).
19+
7. **Telegram = exact attribution by invite-link URL (confidence 1.0). MAX = probabilistic by fingerprint/IP within `Settings.maxCorrelationWindowSec`.** Do not unify these matchers (`apps/bot/src/attribution/correlator.ts:13-37`, [wiki/decisions.md](wiki/decisions.md#adr-007-attribute-subscriptions-by-platform-specific-matching-strategies)).
20+
8. **Public report visibility filtering happens server-side in `getReportData()`.** Never query report-only fields outside this utility (`apps/web/server/utils/reportData.ts:103-190`).
21+
22+
Full ADRs: [wiki/decisions.md](wiki/decisions.md) (11)
23+
Full sharp edges: [wiki/gotchas.md](wiki/gotchas.md) (4 critical, 7 high)
24+
25+
## Verify changes
26+
27+
```bash
28+
pnpm install # workspace install
29+
pnpm typecheck # turbo: tsc --noEmit across web, bot, shared
30+
pnpm lint # turbo lint (web lint script is a placeholder — see active-tasks)
31+
pnpm test # turbo test: vitest in web, bot, packages/shared
32+
pnpm build # turbo build: writes .output/** for web, dist/** for bot
33+
pnpm db:generate # regenerate Prisma client after schema.prisma edit
34+
```
35+
36+
Tests live in `apps/web/tests/`, `apps/bot/tests/`, `packages/shared/tests/` — utilities, attribution dispatch/matching, shared crypto, shared validation. Route- and job-level integration coverage is a known gap ([wiki/active-tasks.md](wiki/active-tasks.md#add-ci-quality-test-coverage-for-api-and-jobs)).
37+
38+
## Code style
39+
40+
- Validate every external HTTP payload with a Zod schema from `@ps/shared/validation` before any DB write (`packages/shared/src/validation.ts:1-111`).
41+
- Encrypt secrets with `encrypt(value, settings.internalApiSecret)` from `@ps/shared/crypto`; never store bot/integration secrets in plain JSON.
42+
- Use the shared Prisma utility (`apps/web/server/utils/prisma.ts`, `apps/bot/src/utils/prisma.ts`) — Prisma 7 needs `PrismaPg` adapter; do not revert to `datasourceUrl`.
43+
- Keep `Settings` as singleton `id=1`; every lookup uses `where: { id: 1 }`.
44+
- Use Nuxt auto-import-prefixed component names (`SetupStepPassword`, not `StepPassword`) ([wiki/gotchas.md](wiki/gotchas.md#setup-component-names-must-use-nuxt-auto-import-prefixes)).
45+
- Add explicit `timeout` / `AbortSignal.timeout()` to outbound `$fetch` calls (Telegram, MAX, Yandex) — defaults are unbounded.
46+
- `attributionConfidence`: 1.0 Telegram exact, 0.5 Telegram fallback, 0.80 MAX fingerprint, 0.70 MAX IP (`packages/shared/src/constants.ts:34-40`).
47+
48+
## Project map
49+
50+
```
51+
apps/
52+
web/ # Nuxt 4 SPA + Nitro API, public port 3000
53+
nuxt.config.ts # ssr: false; transpile @ps/shared, jsonwebtoken
54+
pages/, components/ # admin UI, setup wizard, reports
55+
server/api/ # Nitro routes (track, links, setup, reports, internal)
56+
server/middleware/ # auth.ts (session), internal.ts (Bearer)
57+
server/utils/ # prisma, session, reportData, ymClient
58+
docker-entrypoint.sh # wait pg → migrate deploy → upsert Settings → start
59+
bot/ # Telegram + MAX worker, internal port 3001
60+
src/index.ts # startup: load Settings, polling, jobs
61+
src/api/internal.ts # Bearer-auth HTTP API (link/create, bot/start)
62+
src/telegram/ # grammY bot, member updates, linkService
63+
src/max/ # marker-based long polling
64+
src/attribution/ # correlator, telegramMatcher, maxMatcher
65+
src/jobs/ # linkCleanup, statsSync, conversionRetry
66+
packages/
67+
shared/ # @ps/shared: types, constants, validation, crypto
68+
prisma/
69+
schema.prisma # PostgreSQL model; Settings singleton id=1
70+
migrations/ # checked-in SQL — the real deploy artifact
71+
docker-compose.yml # app, bot, postgres + pgdata volume
72+
```
73+
74+
## Key symbols
75+
76+
| Symbol | Location | Role |
77+
|---|---|---|
78+
| `POST /api/track` | `apps/web/server/api/track/index.post.ts:1-91` | Tracking ingest: validates, hashes IP, creates Visit, requests TG invite link |
79+
| `POST /api/links` | `apps/web/server/api/links/index.post.ts:1-63` | Manual invite-link creation; rejects non-Telegram channels |
80+
| `auth` middleware | `apps/web/server/middleware/auth.ts:3-29` | Session-gates `/api/**` except public prefixes |
81+
| `internal` middleware | `apps/web/server/middleware/internal.ts:3-16` | Bearer `internalApiSecret` for `/api/internal/**` |
82+
| `getReportData()` | `apps/web/server/utils/reportData.ts:41-191` | Server-side visibility filter for public reports |
83+
| `ymClient` | `apps/web/server/utils/ymClient.ts:20-131` | Yandex Metrika OAuth refresh + offline conversion upload |
84+
| Bot internal API | `apps/bot/src/api/internal.ts:12-62` | `/internal/link/create`, `/internal/bot/start`, `/internal/bot/status` |
85+
| `correlator` | `apps/bot/src/attribution/correlator.ts:6-37` | Routes joins to Telegram/MAX matchers, returns `{visitId, confidence, method}` |
86+
| `linkService` | `apps/bot/src/telegram/services/linkService.ts:36-113` | Auto links: `member_limit=1` + TTL; manual links: no limits |
87+
| `startMaxPolling` | `apps/bot/src/max/poller.ts:10-60` | Marker-based MAX polling (marker not yet persisted) |
88+
| `conversionRetry` job | `apps/bot/src/jobs/conversionRetry.ts:10-158` | Every 10 min, retries pending/failed conversions, max 3 attempts |
89+
| `encrypt` / `decrypt` | `packages/shared/src/crypto.ts:1-55` | AES-256-GCM, format `salt:iv:tag:ciphertext`, key derived from secret |
90+
| `trackPayloadSchema` | `packages/shared/src/validation.ts:7-21` | Zod contract for `/api/track` body |
91+
92+
## Tools
93+
94+
No code-intelligence index configured — use Read/Grep/Glob directly, then fall back to wiki pages for architecture context.
95+
96+
## Wiki protocol
97+
98+
**Always check `wiki/` before answering questions about this project's architecture, patterns, or decisions.**
99+
100+
Every architectural choice, every sharp edge, every in-flight task lives in `wiki/`. The wiki is the single source of truth; this file is a pointer.
101+
102+
### Query order
103+
104+
1. **Wiki semantic search (QMD)**`qmd query "<concept>" -c podpisach --no-rerank --json` returns top-N wiki pages by hybrid lex+vec match. **Default first step** for any question about architecture, decisions, gotchas, or how something works. HTTP for tools without shell: `GET http://127.0.0.1:9092/wiki/search/podpisach?q=...&limit=10&mode=hybrid`. Falls back silently to grep if QMD CLI is missing.
105+
2. **Direct read** — when QMD points to a page, open it for full context (`wiki/decisions.md`, `wiki/gotchas.md`, etc.). The catalog is `wiki/index.md`.
106+
3. **Grep fallback**`rg -n "<term>" wiki/` when QMD unavailable.
107+
4. **Code last** — only for line-level details the wiki didn't cover.
108+
109+
### Wiki pages
110+
111+
- [overview.md](wiki/overview.md) — project overview, stack, quick start
112+
- [architecture.md](wiki/architecture.md) — C4 containers, runtime views, invariants
113+
- [decisions.md](wiki/decisions.md) — 11 ADRs with commit evidence
114+
- [gotchas.md](wiki/gotchas.md) — sharp edges (Critical / High / Medium / Low)
115+
- [data-model.md](wiki/data-model.md) — Prisma entities and relationships
116+
- [deployment.md](wiki/deployment.md) — Docker Compose runbook + troubleshooting
117+
- [active-areas.md](wiki/active-areas.md) — modules under recent git activity
118+
- [active-tasks.md](wiki/active-tasks.md) — pipeline tasks snapshot
119+
- [gaps.md](wiki/gaps.md) — undocumented code, open questions
120+
- [components/](wiki/components/) — per-area reference (api, attribution, bot, config, integrations, jobs, max, shared, telegram, web)
121+
122+
Every page has YAML frontmatter with `confidence` (high/medium/low) and `sources:` listing `file:line` evidence. Claims without sources are `low` confidence.
123+
124+
### Planning protocol
125+
126+
Before any non-trivial change:
127+
128+
1. Query wiki — start with `wiki/index.md`, then relevant pages.
129+
2. Synthesize "Past Knowledge": relevant decisions / applicable patterns / known gotchas / reusable components.
130+
3. Check `/home/ubuntu/wiki-master/` for cross-project patterns.
131+
4. Only then plan.
132+
133+
Bug fixes too — `gotchas.md` often names the exact bug you're about to re-encounter.
134+
135+
### Save-on-session-end
136+
137+
After non-trivial work (feature, bug fix, refactor):
138+
139+
1. **Update touched wiki pages** — if behavior in `components/*.md` or `decisions.md` changed, update; `sources:` must still resolve to `file:line`.
140+
2. **Append to `wiki/log.md`** — timestamp, what was done, pages touched, new gaps.
141+
3. **Log new gaps** to `wiki/gaps.md`.
142+
4. **Flag cross-project patterns** under `## cross-project candidates` in `gaps.md`.
143+
144+
Trivial sessions (typo, one-liner) skip this.
145+
146+
### Wiki update flow
147+
148+
Wiki re-derives automatically after every successful dev-orchestrator task, on the WikiFreshness scheduler tick (~5 min), and during the nightly LINT pass.
149+
150+
Manual triggers:
151+
- `POST http://127.0.0.1:9092/wiki/ingest/podpisach?diffMode=worktree` — uncommitted changes
152+
- `POST http://127.0.0.1:9092/wiki/ingest/podpisach?diffMode=committed` — new commits since last ingest
153+
- `POST http://127.0.0.1:9092/entity/wiki/podpisach` — full INIT rewrite (~10 min)
154+
155+
Pipeline runs: wiki updater → QMD re-embed → re-derives this CLAUDE.md / PROJECT.md / README.md.
156+
**Never edit derived files manually** — they get overwritten on next derive. Edit `wiki/` instead.
157+
158+
### Master wiki
159+
160+
`/home/ubuntu/wiki-master/` — cross-project knowledge. Sync runs automatically on every wiki change.
161+
162+
## Behavioral principles
163+
164+
**At the start of any non-trivial task, load `Skill("karpathy-guidelines")`** — mandatory reading.
165+
166+
1. **Think Before Coding** — surface assumptions, ask when unclear.
167+
2. **Simplicity First** — minimum code for the ask, no speculative abstractions.
168+
3. **Surgical Changes** — touch only what you must; match existing style.
169+
4. **Goal-Driven Execution** — verifiable goals, plan multi-step work with checkpoints.
170+
171+
Trivial edits skip the skill load.
172+
173+
## Derive lineage
174+
175+
Built from: `wiki/index.md`, `wiki/overview.md`, `wiki/architecture.md`, `wiki/decisions.md`, `wiki/gotchas.md`, `wiki/active-tasks.md`, `wiki/deployment.md`.
176+
177+
Derive date: `2026-04-29` · snapshot: `9eecf2f5`

wiki/_briefs/active-areas.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## Brief: active-areas.md
2+
3+
Focus: Where is the team working right now.
4+
5+
Files to read first:
6+
- `wiki/_git-signals.md` (pre-computed "Active areas" block)
7+
8+
For each hot area: name + recent commit hashes + one-sentence summary of what's in flight + what to avoid refactoring while it's hot.
9+
10+
Target: 4-8 KB.

wiki/_briefs/active-tasks.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## Brief: active-tasks.md
2+
3+
Focus: In-flight pipeline tasks — snapshot from the task board.
4+
5+
Files to read first:
6+
- `wiki/_active-tasks.json` (pre-fetched from pipeline_tasks DB table)
7+
8+
Group by status (planning / approved / in_work / questions / blocked). If the file is empty, write "No active pipeline tasks at snapshot time (<today>)." and stop.
9+
10+
Target: 3-8 KB.

wiki/_briefs/architecture.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
## Brief: architecture.md
2+
3+
Focus: How modules connect, what invariants hold, and C4-level picture of the system.
4+
5+
Files to read first:
6+
- `turbo.json`
7+
- `package.json`
8+
- `docker-compose.yml`
9+
10+
Top functional domains (aggregated by label — GitNexus splits large domains into sub-clusters):
11+
12+
13+
GitNexus queries for deeper context:
14+
- `gitnexus_cypher` with `MATCH (c:Community) WHERE c.symbolCount > 30 RETURN c.label, c.symbolCount ORDER BY c.symbolCount DESC`
15+
- `gitnexus_cypher` with `MATCH (a:File)-[r:CodeRelation]->(b:File) WHERE r.type = "IMPORTS" AND a.path STARTS WITH "apps/pipeline" AND b.path STARTS WITH "apps/crm" RETURN a.path, b.path LIMIT 10` — cross-app dependencies (boundaries of the C4 diagram)
16+
- `gitnexus_route_map` to see HTTP boundaries between apps
17+
18+
Do NOT cover:
19+
- Per-module internals (those live in `components/*.md`)
20+
- Known sharp edges (those live in `gotchas.md`)
21+
22+
Target: 10-15 KB, MUST include a C4Container mermaid diagram + numbered Key Decisions with WHY.

wiki/_briefs/components/bot.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## Brief: components/bot.md
2+
3+
Focus: The `bot` application — entry points, public surface, main flows.
4+
5+
Files to read first:
6+
- `apps/bot/package.json`
7+
8+
- `apps/bot/src/index.ts`
9+
10+
11+
Package snapshot:
12+
```json
13+
{
14+
"name": "bot",
15+
"version": "0.1.0",
16+
"private": true,
17+
"type": "module",
18+
"scripts": {
19+
"dev": "tsx watch src/index.ts",
20+
"build": "prisma generate --schema ../../prisma/schema.prisma && tsc",
21+
"start": "node dist/index.js",
22+
"typecheck": "tsc --noEmit",
23+
"test": "vitest run"
24+
},
25+
"dependencies": {
26+
"@ps/shared": "workspace:*",
27+
"@prisma/client": "^7.5.0",
28+
"grammy": "^1.41.1",
29+
"node-cron": "^4.2.1",
30+
"ofetch": "^1.5.1",
31+
"pino": "^10.3.1",
32+
"pino-pretty": "^13.1.3"
33+
},
34+
"devDependencies": {
35+
"@types/node": "^20.0.0",
36+
"@types/node-cron": "^3.0.11",
37+
"tsx": "^4.21.0",
38+
"typescript": "^5.9.3",
39+
"vitest": "^4.1.0"
40+
}
41+
}
42+
43+
```
44+
45+
46+
47+
GitNexus queries:
48+
- `gitnexus_cypher` with `MATCH (f:File) WHERE f.path STARTS WITH "apps/bot/" RETURN f.path LIMIT 30`
49+
- `gitnexus_query` with "main flow in bot"
50+
51+
Target: 10-15 KB with a sequence diagram + Public API table.

wiki/_briefs/components/shared.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## Brief: components/shared.md
2+
3+
Focus: The `shared` application — entry points, public surface, main flows.
4+
5+
Files to read first:
6+
- `packages/shared/package.json`
7+
8+
- `packages/shared/src/index.ts`
9+
10+
11+
Package snapshot:
12+
```json
13+
{
14+
"name": "@ps/shared",
15+
"version": "0.1.0",
16+
"type": "module",
17+
"main": "./dist/index.js",
18+
"exports": {
19+
".": {
20+
"import": "./dist/index.js",
21+
"types": "./src/index.ts"
22+
},
23+
"./types": {
24+
"import": "./dist/types.js",
25+
"types": "./src/types.ts"
26+
},
27+
"./constants": {
28+
"import": "./dist/constants.js",
29+
"types": "./src/constants.ts"
30+
},
31+
"./validation": {
32+
"import": "./dist/validation.js",
33+
"types": "./src/validation.ts"
34+
},
35+
"./crypto": {
36+
"import": "./dist/crypto.js",
37+
"types": "./src/crypto.ts"
38+
}
39+
},
40+
"scripts": {
41+
"build": "tsc",
42+
"typecheck": "tsc --noEmit",
43+
"test": "vitest run"
44+
},
45+
"dependencies": {
46+
"zod": "^4.3.6"
47+
},
48+
"devDependencies": {
49+
"typescript": "^5.9.3",
50+
"vitest
51+
```
52+
53+
54+
55+
GitNexus queries:
56+
- `gitnexus_cypher` with `MATCH (f:File) WHERE f.path STARTS WITH "packages/shared/" RETURN f.path LIMIT 30`
57+
- `gitnexus_query` with "main flow in shared"
58+
59+
Target: 10-15 KB with a sequence diagram + Public API table.

wiki/_briefs/components/web.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## Brief: components/web.md
2+
3+
Focus: The `web` application — entry points, public surface, main flows.
4+
5+
Files to read first:
6+
- `apps/web/package.json`
7+
8+
9+
10+
11+
Package snapshot:
12+
```json
13+
{
14+
"name": "web",
15+
"version": "0.1.0",
16+
"private": true,
17+
"scripts": {
18+
"dev": "nuxi dev",
19+
"build": "nuxi build",
20+
"typecheck": "nuxi typecheck",
21+
"lint": "echo 'lint placeholder'",
22+
"test": "vitest run"
23+
},
24+
"dependencies": {
25+
"@nuxt/ui": "^4.5.1",
26+
"@ps/shared": "workspace:*",
27+
"@prisma/client": "^7.5.0",
28+
"@vueuse/core": "^14.2.1",
29+
"bcryptjs": "^3.0.3",
30+
"chart.js": "^4.5.1",
31+
"date-fns": "^4.1.0",
32+
"jsonwebtoken": "^9.0.3",
33+
"nuxt": "^4.4.2",
34+
"ofetch": "^1.5.1",
35+
"vue-chartjs": "^5.3.3",
36+
"zod": "^4.3.6"
37+
},
38+
"devDependencies": {
39+
"@types/bcryptjs": "^3.0.0",
40+
"@types/jsonwebtoken": "^9.0.10",
41+
"typescript": "^5.9.3",
42+
"vitest": "^4.1.0"
43+
}
44+
}
45+
46+
```
47+
48+
49+
50+
GitNexus queries:
51+
- `gitnexus_cypher` with `MATCH (f:File) WHERE f.path STARTS WITH "apps/web/" RETURN f.path LIMIT 30`
52+
- `gitnexus_query` with "main flow in web"
53+
54+
Target: 10-15 KB with a sequence diagram + Public API table.

0 commit comments

Comments
 (0)