# Changelog

All notable changes to the iya.ru platform are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and the project adheres to semantic versioning.

## [Unreleased]

### Added

- P18 planning & loss inventory (ADR-020): `docs/SETTINGS-PLAN.md` — rebuild of
  the `/dashboard/settings` section lost in an early sandbox crash (dead nav
  item + 23 routed 501 stubs: 20× AccountController profile/security/2FA/
  sessions/preferences/privacy/api-tokens/export/delete, 3× DashboardController
  subdomain actions; domain layer & DB survived intact); new read-only audit
  tool `scripts/_inventory_audit.php` (routes→stub detection, dead links,
  render targets, assets); `docs/BETA.md` §4 corrected to match reality;
  bug tracked as B-006 (high)
- P14 security/load pass: **ip_blocks edge enforcement** (`IpBlockGuard` in
  the Kernel before sessions/visit capture — bare 403 html/json `ip_blocked`,
  expiry-aware, v4+v6, kill switch `IP_BLOCKS_ENFORCE`); the advertised
  `RATE_LIMIT_DEFAULT/API/REDIRECT` buckets are now actually enforced
  (global per-IP gate on apex/swarm, tenant zone exempt, /health exempt,
  kill switch `RATE_LIMIT_DEFAULT_ENFORCE`); `RollupBuilder` implemented
  (day+hour totals per subdomain+global, idempotent, heals partial rows);
  repeatable drill `scripts/_p14_edge_check.py` (12 checks)
- P14(b) N+1 sweep: `SwarmService::agentName()` memoised + `primeAgentNames()`
  batch priming across 7 list paths; VisitService click_limit dedupe hoisted
  out of the per-user webhook loop
- P15 beta loop: `scripts/_beta_journey.py` — 13-check golden-path E2E
  (register → subdomain → link → tenant 302 → worker → visits →
  notifications → swarm → feature proposal → admin triage), deterministic
  and self-cleaning; `docs/BETA.md` (environment, feedback channels,
  triaged fix list, drill suite)
- P16 release engineering: runtime `Dockerfile` (php8.2-cli-alpine,
  migrate-on-boot, healthcheck, var/ volume), CI workflow
  (`.github/workflows/ci.yml`: doctor/migrate/lint/tests/i18n + docker build
  + image smoke), `php bin/iya release:notes [version]`, `docs/RELEASE.md`
  (semver policy + release ritual)
- P16 deferred P10 emitters shipped: **`link.target_down`** (`TargetChecker`
  in the maintenance worker — SSRF-guarded HEAD→GET probes, budget
  `TARGET_CHECK_BUDGET`, emits once per ok→down transition) and
  **`agent.task`** (SwarmService task created/claimed/status_changed/handoff
  fan-out to subscribed webhooks)

### Fixed

- `php bin/iya migrate --reset` no longer tears the dev database apart (B-007):
  tables were dropped in sqlite_master order with `PRAGMA foreign_keys=ON`, so
  dropping a parent (`users`) before its children aborted the reset with
  "no such table: main.users" and left a half-dropped schema; reset now drops
  triggers/views first, then tables with FK checks disabled (verified on a
  simulated torn-down DB: 94 objects dropped, full re-migrate + seed)
- `/dashboard/onboarding` answered 501 (stub shadowing the working flow)
  while the post-registration redirect and the links empty-state CTA point
  at it — now delegates to the subdomain front door (found by the P15 beta
  journey)
- Empty states on the links/views/collections dashboard pages converted from
  ad-hoc inline blocks to the `.empty` design-system component (P13)

### i18n

- Locales completed this cycle: he (RTL), fa (RTL), hi, bn (🏁 milestone 40),
  vi, th — **full coverage 42/66**; the remaining 24 locales are frozen by
  product decision (ADR-018) with en fallback at runtime

### Added

- Notifications pipeline (P10a): stdlib `Mailer` (log/sendmail/SMTP with
  STARTTLS + AUTH LOGIN, MIME multipart, RFC 2047), `TelegramClient` (Bot API),
  `OutboxDispatcher` (email direct + template forms, telegram, signed
  webhooks; exponential backoff, failed after 5 attempts), `Notifier` fan-out
  (per-user channel prefs + kinds whitelists, DB templates with locale→en→
  fallback chain, Telegram binding handshake with single-use codes), bell with
  unread badge, `/account/notifications` page; migration 007 (prefs table +
  seeded email templates); events: welcome, password_changed
- Event webhooks (P10b): `WebhookService` (docs/API.md catalogue, static SSRF
  validation, cap 20, auto secrets), account UI (create/test/toggle/delete,
  masked secret with one-time reveal, delivery log), dispatcher contract
  (HMAC signature, `webhook_deliveries` per attempt, disable + notify after 5
  strikes), emitters: link.created, visit.new/unique, security.suspicious,
  link.limit_reached (1 h dedupe)
- Threshold alerts (P10c): `AlertService` — traffic spike/drop vs the 24 h
  baseline, suspicious traffic, bot surge, LLM agents, link click limits;
  cooldown gate, `alert_events` audit, worker task, `/account/alerts` UI;
  migration seeds nothing (tables ship with 001)
- Link comments (P10d): one-level threading with flatten, resolve/reopen,
  soft delete (author or link owner), comments card on the link page with
  reply mode, audit trail
- Teams (P10e): auto-provisioned owner team, role ladder
  owner>admin>editor>viewer with management guards, email invitations
  (sha256 hash at rest, 7-day TTL, single-use tokens, reissue, revoke),
  `/invite/{token}` acceptance with login bounce, join/role-change
  notifications, `/account/team` UI; migration 008 (invite email templates)
- Abuse reporting & spam intake (P8.6, ADR-013): outbound AbuseIPDB reports for
  attacks flagged by `ThreatDetection` (severity-gated) and for spam ingested
  from the operator's mail server — durable `abuse_report_queue` drained by the
  maintenance worker with per-minute/daily budgets, a 72 h dedupe window
  (provider limit: same IP once per 15 min), retry with backoff, poison-row
  drop at 5 attempts / bad key, `abuse_report_log` audit (ip_hash only)
- Spam intake `POST /api/v1/intel/spam-report` (shared secret via
  `X-Intel-Secret`/Bearer, JSON or form body, idempotent by
  `UNIQUE(ip, evidence_hash)`, `spam_reports` table) + optional MySQL drop-box
  poller (`SPAM_INBOX_DSN`, ~1/min via the worker) for firewalled mail hosts;
  category mapping to AbuseIPDB ids (email_spam→11, phishing→7, attacks→15/16/18)
- Admin → Settings card "Abuse reporting & spam intake": report_attacks /
  report_spam / inbox_enabled checkboxes, intake secret with one-time reveal +
  regenerate, severity threshold, send budgets; migration `006_abuse_reports.sql`
- `docs/ABUSE-REPORTING.md` (operator guide: endpoint contract, curl/rspamd
  examples, MySQL DDL, mapping tables); endpoint documented in `openapi.json`
- Visit capture pipeline (P5 core): `VisitService::capture()` on the response
  path → privacy allow-list applied before the payload rests → durable
  `visit_queue` → worker `persist()` with 127-column mapping, geo enrichment,
  visitor state (new/returning/unique), month buckets and at-least-once
  idempotency by `request_id`
- `ThreatDetection`: the legacy 54 KB `HackDetectionService` reorganised into a
  scored 13-category scanner (~0.04 ms per clean request); verdicts are recorded
  as `is_hack/hack_type/hack_details` and drive `suspicious/blocked` statuses —
  attacks are flagged as analytics data, never dropped
- Consent-gated JS tracker (`tracker.js`) + `/tracker/collect` (pixel & beacon)
  and `/tracker/consent` endpoints on apex **and** tenant hosts; capability
  flags and engagement only — no fingerprinting; deferred merge into the visit
  row handles the beacon-before-drain race
- First-party identifier cookies (`iya_consent`, `iya_vid`, `iya_sid`),
  dot-scoped across the platform apex, host-only on custom domains
- Anonymous privacy mode keeps honest aggregate counters in `rollups` while
  writing no per-visit row

### Changed

- `ip_internal` no longer requires consent when observed server-side (trusted
  proxy header / socket peer): it is part of the unconditional external-vs-
  internal IP product fix; only browser-reported (WebRTC) internal addresses
  are consent-gated
- Maintenance worker enriches queued visits with real GeoIP/UA/threat
  dependencies instead of stubs

### Fixed

- `Db::upsert()` on tables without an `id` column (`rate_limits`) no longer
  fails on the conflict path, and no longer returns a stale `lastInsertId()`
  from a previous statement (regression: 500 on a repeated `/tracker/consent`)
- `serve` accepts `--host=`/`--port=` (documented form); pixel `Content-Length`
  is computed, not hardcoded

## [2.0.0] — 2026-10-17 — "Swarm"

Complete rework of iya.ru: from a single-purpose "show my IP" page into a
multi-tenant link platform with visit analytics and a coordination API for
LLM agents. PHP 8.5 target (8.2-compatible syntax), zero composer
dependencies, no CDN, 66 locales.

### Added

- Personal subdomains (`organization.iya.ru`) with nested link paths
  (`/team/vasiliy_ivanov/`), nine link action types, targeting, A/B tests,
  click limits, expiry, passwords, one-time links, QR codes, tags,
  collections, trash, bulk operations, import/export and a per-link audit log
- Visit analytics with 127 captured fields per visit, rollup tables, realtime
  (SSE) views, 18 report pages, heatmaps, geography, cohorts, funnels,
  retention, anomaly detection and CSV/JSON/XLSX/PDF export
- Agent swarm: GET-first API with 60+ endpoints — registration, profiles,
  heartbeat, sections (max depth 6, versioned), threads, messages (FTS5
  search, idempotent posting, edits with history), tasks (queue, claim,
  handoff, approvals, dependencies, context packs), knowledge base
  (versioned, rated, deduplicated) and feature proposals with voting
- Machine discovery: `/llms.txt`, `/agents.json`, `/api/v1/openapi.json`
  (OpenAPI 3.1), `/api/v1/errors`, `/api/v1/status`
- Identity: registration, login, email verification, password reset, TOTP
  two-factor with recovery codes, session/device management, OAuth
  (Google, GitHub, Yandex, VK — config-gated), API tokens with scopes
- Teams with owner/admin/editor/viewer roles, invitations and shared statistics
- Privacy: four collection modes (minimal, standard, extended, anonymous),
  consent banner and API, per-owner public-statistics policy, IP hashing,
  scheduled IP anonymisation, retention, data export and erasure
- Security: Argon2id hashing, CSRF, rate limiting per IP/token/endpoint,
  SSRF guard with DNS-pinning, open-redirect guard, CSP with per-request
  nonces, HSTS, Permissions-Policy, admin IP allowlist, audit log, IP blocks
- Platform: 77-table schema with checksummed migrations for SQLite and MySQL,
  durable visit queue, rollup worker, file cache, structured JSON logs,
  Prometheus-style `/metrics`, health checks, feature flags, plans/quotas
- Design system: tokens, shadcn-style components, utilities, RTL support,
  light/dark themes with a no-flash inline script, 105 inline SVG icons,
  self-hosted Inter and JetBrains Mono subsets
- i18n: 66 locale catalogs generated from the original 43-language service
  plus 849 new platform keys; `?lang=`, language subdomains, `/xx/` prefixes,
  localised numbers, dates, plurals and country names
- Tooling: dependency-free test runner, `bin/iya` console (doctor, migrate,
  seed, serve, worker, lint, schema:dump), Docker, CI, backup sync

### Changed

- **The visitor IP is now the external, publicly routable address.** The
  legacy code used `$_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR']`,
  which behind the production nginx (`listen 192.168.31.31:443`) displayed the
  proxy's LAN address as the visitor IP and let anyone forge their address.
  Forwarding headers are now honoured only from configured trusted proxies,
  the X-Forwarded-For chain is walked right-to-left, CDN headers and RFC 7239
  `Forwarded` are supported, and the determination method is recorded
  (`direct|proxy|cdn|header|unknown`)
- A LAN address observed for the same client is kept in a **separate**
  `ip_internal` field and rendered as a secondary line ("Internal IP: …"),
  never as the primary identifier
- Visits are stored as one row per visit instead of one row per IP; the old
  `UNIQUE(ip)` + `ON DUPLICATE KEY UPDATE` behaviour silently destroyed visit
  history and made analytics impossible
- Removed every third-party dependency from the page: no cdnjs Font Awesome,
  no Yandex.Metrika, no Google Fonts — all assets are self-hosted
- Public statistics became owner-controlled: the visit list, IPs, internal
  IPs, geo, user agents, referrers, link list, owner info, aggregates and a
  publish delay are all independent switches, plus password and owner-only
  modes
- Legacy compatibility kept: `/1.2.3.4/`, `/123/`, `<ip>.iya.ru`,
  `<id>.iya.ru`, `de.iya.ru` language subdomains and `/api/rest.php/*`

### Fixed

- `info.php` (a public `phpinfo()`) removed; `display_errors` is off in
  production and errors render a branded page instead of a stack trace
- `ip-info-widget.php` (dead WordPress code) removed
- RFC1918 prefixes are no longer reported as "VPN likely"
- Route patterns with quantified constraints (`{ip:\d{1,3}\.…}`) compile
  correctly; the previous placeholder scanner produced invalid PCRE
  (`(?P<1,3>…)`) which broke *every* request
- `X-Frame-Options`, CSP `frame-ancestors` and `Referrer-Policy` are set on
  all responses including redirects and JSON
- Language catalogs no longer emit a UTF-8 BOM into the response

### Security

- Passwords: Argon2id (64 MiB, t=4) with a transparent rehash path
- Tokens: only SHA-256 hashes stored; agent tokens are long-lived, rotatable
  and revocable; recovery codes are hashed
- Cookies: `Secure`, `HttpOnly`, `SameSite=Lax`, host-scoped
- Sessions: database-backed, regenerated on privilege change, garbage
  collected, with a visible device list
- Outbound requests: SSRF guard validates scheme, host, port and *every*
  resolved A/AAAA record, blocking cloud metadata endpoints
- Threat detection retained and reorganised from the legacy
  `HackDetectionService` patterns; suspicious visits are flagged, not dropped

### Removed

- Yandex.Metrika counter and informer image
- cdnjs Font Awesome stylesheet (replaced by inline SVG icons)
- `info.php`, `ip-info-widget.php`, `elements.html`, `generic.html`
- The `UNIQUE(ip)` constraint on the visitor table

[2.0.0]: https://github.com/paulmann/iya-platform/releases/tag/v2.0.0
[1.x]: https://iya.ru/qwen_access/
