1
0
mirror of git://f0xx.org/ac/ac-docs synced 2026-07-29 06:59:18 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-23 12:20:43 +02:00
commit 69a448f156
151 changed files with 33372 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,454 @@
# Android Cast — Email agent config & 2FA / mobile auth flow
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-08 |
| Last modification date | 2026-06-08 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Date: 2026-06-07 — FR §9 (email) + FR §10 (registration, 2FA, mobile) — **alpha must-have**_
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Document history](#document-history)
- [Executive summary](#executive-summary)
- [Current state (repo)](#current-state-repo)
- [§9 — Email addresses & agent config](#9-email-addresses-agent-config)
- [§9.1 Prerequisites](#91-prerequisites)
- [§9.2 Register addresses (free — DNS, not “buy mailboxes”)](#92-register-addresses-free-dns-not-buy-mailboxes)
- [§9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)](#93-forward-duplicate-to-gmail-info-foxxspambox0gmailcom)
- [§9.4 Recommended prod layout for this project](#94-recommended-prod-layout-for-this-project)
- [§9.5 Outbound mail from BE (verification emails)](#95-outbound-mail-from-be-verification-emails)
- [§10 — Registration, 2FA, mobile auth](#10-registration-2fa-mobile-auth)
- [§10.1 Registration & login stages](#101-registration-login-stages)
- [§10.2 Open-source building blocks (recommended)](#102-open-source-building-blocks-recommended)
- [§10.3 Users DB migration (privacy-first)](#103-users-db-migration-privacy-first)
- [§10.4 On-demand user maintenance (admin + self-service)](#104-on-demand-user-maintenance-admin-self-service)
- [§10.5 Operator checklist (track progress)](#105-operator-checklist-track-progress)
- [§10.6 Mobile-friendly UX (not BE-only)](#106-mobile-friendly-ux-not-be-only)
- [§10.7 Alpha implementation plan](#107-alpha-implementation-plan)
- [UI/UX alignment (existing consoles)](#uiux-alignment-existing-consoles)
- [Security & compliance notes](#security-compliance-notes)
- [Open questions](#open-questions)
- [Source linkage](#source-linkage)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Document history
| Rev | Date | Focus |
|-----|------|--------|
| **1** | 2026-06-07 | Initial FR capture: email forwarding + registration/2FA for alpha; infra + BE + mobile flows. |
**Alpha gate:** agreed with project owners — user registration with verified email + second factor is **must-have for full alpha** (alongside RSSH remote access). LAN-only cast demo may still skip account flows.
---
## Executive summary
| Track | Goal |
|-------|------|
| **§9 Email** | Public-facing addresses `info@`, `admin@`, `root@` on **`apps.f0xx.org`** (and optional aliases on **`f0xx.org`**) that **forward** to personal Gmail; no paid mailbox SKU required. |
| **§10 Auth** | Replace “Register (coming soon)” with **email verification + flexible 2FA** (TOTP QR, optional WebAuthn/passkey on Android), captcha on abuse-prone endpoints, **privacy-preserving** audit tables, admin tools for root/slug admins. |
| **Mobile** | Same flows usable on phone browsers **and** deep links / Custom Tabs from the Android app where appropriate. |
| **Licenses** | Prefer **MIT / BSD / Apache-2.0** PHP libraries; avoid proprietary-only auth SaaS for core path. |
---
## Current state (repo)
| Area | Today |
|------|--------|
| BE login | Username + `password_hash` only — [`Auth.php`](../examples/crash_reporter/backend/src/Auth.php) |
| Registration | UI placeholder: “Register (coming soon)” — [`login.php`](../examples/crash_reporter/backend/views/login.php) |
| Users table | `username`, `password_hash`, `role` — no email, no 2FA — [`schema.mariadb.sql`](../examples/crash_reporter/backend/sql/schema.mariadb.sql) |
| Outbound email | **Not configured** — no SMTP in [`config.example.php`](../examples/crash_reporter/backend/config/config.example.php) |
| Mobile app | No account linking to crashes console yet |
| Roadmap | “User registration + email verification” — [`examples/crash_reporter/ROADMAP.md`](../examples/crash_reporter/ROADMAP.md) |
---
## §9 — Email addresses & agent config
Target addresses (any one canonical pair is fine; document both for flexibility):
| Role | Preferred | Alias |
|------|-----------|--------|
| Public / info | `info@apps.f0xx.org` | `info@f0xx.org` |
| Admin / ops | `admin@apps.f0xx.org` | `admin@f0xx.org`, `root@apps.f0xx.org` |
**Agent config** (future): BE `config.php``mail.from`, `mail.reply_to`, `mail.envelope_from` pointing at these addresses once DNS and forwarding work.
### §9.1 Prerequisites
You do **not** need to buy Google Workspace or Microsoft 365 mailboxes. You **do** need:
| # | Prerequisite | Why |
|---|--------------|-----|
| 1 | **DNS control** for `f0xx.org` and `apps.f0xx.org` | MX, SPF, DKIM, DMARC live at the DNS host (registrar or Cloudflare). |
| 2 | **Decide canonical mail domain** | Recommend **`apps.f0xx.org`** for product mail; keep `f0xx.org` as forward/alias only if desired. |
| 3 | **Inbound path** | MX records → free forwarder **or** self-hosted Postfix alias on FE/BE. |
| 4 | **Outbound path (BE)** | SMTP relay for verification/reset mail (see §9.5) — can be Gmail SMTP, SendGrid free tier, or local Postfix with SPF alignment. |
| 5 | **TLS on web** | Already have `https://apps.f0xx.org` — verification links use same vhost. |
| 6 | **Gmail destination** | e.g. `foxxspambox0@gmail.com` — receives forwarded + BCC copies. |
**Deliverability minimum (avoid spam folder):**
```text
SPF TXT @apps.f0xx.org "v=spf1 include:<forwarder-or-smtp-provider> ~all"
DKIM TXT (provider gives name/value)
DMARC TXT _dmarc.apps.f0xx.org "v=DMARC1; p=none; rua=mailto:admin@apps.f0xx.org"
```
Start with `p=none`; tighten after monitoring.
### §9.2 Register addresses (free — DNS, not “buy mailboxes”)
**“Register” here = DNS + forwarder rules**, not purchasing seats.
#### Option A — **Cloudflare Email Routing** (recommended if DNS is on Cloudflare)
1. Cloudflare dashboard → **Email****Email Routing** → enable for `apps.f0xx.org`.
2. Add **destination address** `foxxspambox0@gmail.com` (verify Gmail link once).
3. Create **Custom addresses**:
- `info@apps.f0xx.org` → forward to Gmail
- `admin@apps.f0xx.org` → forward to Gmail
- `root@apps.f0xx.org` → forward to Gmail (or same rule as admin)
4. Cloudflare adds MX automatically; copy **SPF** hint from dashboard.
5. Repeat for **`f0xx.org`** zone if you want `info@f0xx.org` (separate zone rules or redirect-only).
**Cost:** $0 on Cloudflare free plan.
#### Option B — **ImprovMX** (DNS at any registrar)
1. Sign up at [improvmx.com](https://improvmx.com) (free tier: limited aliases).
2. Add domain `apps.f0xx.org` → set MX to ImprovMX hosts (they show exact records).
3. Create aliases `info`, `admin`, `root``foxxspambox0@gmail.com`.
4. Add SPF include for ImprovMX.
#### Option C — **Self-hosted alias** (Gentoo FE or Alpine BE)
If you already run Postfix on FE:
```text
/etc/postfix/virtual:
info@apps.f0xx.org foxxspambox0@gmail.com
admin@apps.f0xx.org foxxspambox0@gmail.com
root@apps.f0xx.org foxxspambox0@gmail.com
```
Then `postmap /etc/postfix/virtual && systemctl reload postfix`. Requires **MX** pointing to FE public IP and port **25** reachable (often blocked on residential; OK on HVM with firewall rule).
**Do first:** pick **A or B** unless you explicitly want to operate SMTP on FE.
### §9.3 Forward / duplicate to Gmail (info@ → foxxspambox0@gmail.com)
| Method | Inbox copy | Send-as From | IMAP on apps domain |
|--------|------------|--------------|---------------------|
| **Forward only** (Cloudflare / ImprovMX) | Yes — same subject/body in Gmail | Reply-from Gmail unless configured | N/A — read in Gmail |
| **Gmail “Send mail as”** | N/A | Can send as `info@apps.f0xx.org` if SMTP creds exist | Optional |
| **BCC archive** | Forward + hidden copy to second Gmail | — | Rarely needed |
For FR §9.3 (“same topic and body redirected”): **plain forward** is enough. Gmail shows original `To:` header; you can filter with Gmail rule `to:info@apps.f0xx.org`.
**Optional Gmail filter:**
```text
To: info@apps.f0xx.org → label: androidcast-info
```
No IMAP/POP3 server on `apps.f0xx.org` is required for this FR.
### §9.4 Recommended prod layout for this project
```text
Inbound (public):
info@apps.f0xx.org ──forward──► foxxspambox0@gmail.com
admin@apps.f0xx.org ──forward──► foxxspambox0@gmail.com
root@apps.f0xx.org ──forward──► foxxspambox0@gmail.com (same inbox or +tag)
Outbound (BE PHP):
From: Android Cast Issues <noreply@apps.f0xx.org>
Reply-To: info@apps.f0xx.org
SMTP: transactional relay (see §9.5)
```
Document chosen provider in [`INFRA.md`](INFRA.md) § changelog once live.
### §9.5 Outbound mail from BE (verification emails)
Add to `config.example.php` (implementation phase):
```php
'mail' => [
'transport' => 'smtp', // smtp | sendmail
'from' => 'Android Cast Issues <noreply@apps.f0xx.org>',
'reply_to' => 'info@apps.f0xx.org',
'smtp' => [
'host' => 'smtp.example.com',
'port' => 587,
'encryption' => 'tls',
'username' => '',
'password' => '', // env / secrets file, not git
],
],
```
**Free/low-cost SMTP options:**
| Provider | Notes |
|----------|--------|
| Gmail SMTP + App Password | OK for low volume; From may need “Send as” alignment |
| Brevo / SendGrid free tier | Better for `noreply@apps.f0xx.org` alignment |
| Local Postfix on BE | Full control; must publish SPF for BE egress IP |
PHP library: **Symfony Mailer** (MIT) — already common in PHP ecosystems.
---
## §10 — Registration, 2FA, mobile auth
### §10.1 Registration & login stages
Proposed **alpha** flow (flexible — toggles in config):
```mermaid
sequenceDiagram
participant U as User (mobile browser)
participant BE as apps.f0xx.org PHP
participant M as Mail / Gmail
participant A as Android app (optional)
U->>BE: POST /register (email, username, password)
BE->>BE: captcha verify, rate limit
BE->>BE: store pending registration (hashed secrets)
BE->>M: Send verify link (24h token)
M->>U: Email with https://apps.f0xx.org/.../verify?token=
U->>BE: GET verify (or Custom Tab from app)
BE->>U: Prompt enroll 2FA (TOTP QR and/or WebAuthn)
U->>BE: POST /2fa/enroll (TOTP secret or passkey)
BE->>U: Account active → login
Note over U,BE: Login thereafter
U->>BE: username + password
BE->>U: 2FA challenge (TOTP code or WebAuthn)
U->>BE: verify second factor
BE->>U: Session cookie (existing ac_crash_sess)
```
| Stage | Method | Mobile-friendly |
|-------|--------|-----------------|
| **10.1.2 Email confirmation** | HTTPS link with signed token | Email app → Chrome Custom Tab; large tap target |
| **10.1.3 TOTP / QR** | Standard `otpauth://` QR + manual key | Scan with Google Authenticator / Aegis; or type 6 digits |
| **10.1.4 WebAuthn / passkey** | Platform authenticator (fingerprint) | **Android Chrome** + future app WebView if needed |
| **Captcha** | ALTCHA (MIT, self-hosted) or rate limit only at alpha | Invisible/PoW — no Google reCAPTCHA dependency |
| **Recovery** | Backup codes + optional second recovery email | Printable one-time codes; store hashed |
**Roles:** new self-registered users default to `viewer` + default company membership (existing RBAC). Elevation to `admin`/`root` remains **admin-only**.
### §10.2 Open-source building blocks (recommended)
| Function | Library | License | Notes |
|----------|---------|---------|-------|
| SMTP | [symfony/mailer](https://github.com/symfony/mailer) | MIT | Transport abstraction |
| TOTP | [spomky-labs/otphp](https://github.com/spomky-labs/otphp) | MIT | RFC 6238; QR via endroid/qr-code |
| WebAuthn | [web-auth/webauthn-lib](https://github.com/web-auth/webauthn-lib) | MIT | Passkeys; PHP 8.1+ |
| Password hash | `password_hash()` | PHP core | Already used |
| Captcha | [altcha-org/altcha](https://github.com/altcha-org/altcha) | MIT | Self-hosted PoW widget |
| JWT / tokens | Signed random + HMAC in DB | — | Prefer opaque tokens stored hashed |
**Avoid as hard dependency for alpha:** proprietary Auth0/Okta-only flows (OK as optional future SSO).
**Android app (phase 2 of alpha auth):**
| Feature | Approach |
|---------|----------|
| Open verify link | `CustomTabsIntent``https://apps.f0xx.org/.../verify` |
| TOTP | Optional in-app entry (Web login sufficient for alpha) |
| Passkey | Chrome on device; link account in settings later |
| Deep link | `androidcast://auth/verify?token=` optional — not required if HTTPS works |
### §10.3 Users DB migration (privacy-first)
New migration **`008_auth_email_2fa.sql`** (sketch — apply on MariaDB prod once implemented):
**Extend `users`:**
| Column | Type | Notes |
|--------|------|-------|
| `email_normalized` | VARCHAR(254) NULL UNIQUE | Lowercase email |
| `email_verified_at` | TIMESTAMP NULL | NULL until link clicked |
| `status` | ENUM | `pending`, `active`, `locked`, `disabled` |
| `recovery_email_normalized` | VARCHAR(254) NULL | Optional second address |
**New tables (anonymous / hashed where possible):**
```sql
-- auth_pending_registrations: delete after verify or TTL
-- auth_factors: user_id, type ENUM('totp','webauthn','backup_code'), secret_or_credential_encrypted, label, created_at
-- auth_tokens: purpose ENUM('verify_email','reset_password','login_magic'), token_hash, expires_at, used_at
-- auth_attempts: ip_hash, username_hash, outcome, created_at (no raw IP in prod if avoidable)
-- auth_audit: actor_user_id, action, meta_json, created_at
```
**Privacy rules:**
- Store **hashed** opaque tokens (SHA-256), never raw token in DB.
- Store TOTP secrets **encrypted at rest** (libsodium `secretbox`, key in `config.php` env).
- `auth_attempts.ip_hash` = HMAC(server_pepper, ip) — supports rate limit without retaining IP.
- WebAuthn credentials: store `credential_id` + `public_key` only (standard).
SQLite dev: mirror schema in `Database::ensureSchema` like existing RBAC migrations.
### §10.4 On-demand user maintenance (admin + self-service)
| Action | Who | UI location |
|--------|-----|-------------|
| Add / change **recovery email** | User | `?view=account_security` (new) |
| Re-enroll TOTP / add passkey | User | Same; requires recent password + 2FA |
| Revoke passkeys / backup codes | User | Same |
| Clear failed login/register attempts | `root`, company `admin` | `?view=rbac` → user row → “Clear auth lockouts” |
| Disable user / force password reset | `root`, `platform_admin` | RBAC admin |
| View auth audit tail | `root` | `?view=auth_audit` (read-only) |
Session cookie path stays **`/app/androidcast_project`** (shared across crashes/build consoles).
### §10.5 Operator checklist (track progress)
Use this table during implementation (copy to ticket **#9 / #10**):
| # | Task | Owner | Done |
|---|------|-------|------|
| E1 | Enable Cloudflare Email Routing (or ImprovMX) for `apps.f0xx.org` | Ops | ☐ |
| E2 | Forward `info@`, `admin@`, `root@` → Gmail | Ops | ☐ |
| E3 | SPF + DKIM + DMARC records live | Ops | ☐ |
| E4 | Test inbound: send mail to `info@apps.f0xx.org`, arrives Gmail | Ops | ☐ |
| E5 | Configure BE SMTP + send test from staging | Dev | ☐ |
| A1 | Migration `008_auth_email_2fa.sql` + `Database::ensureSchema` | Dev | ☐ |
| A2 | Register + verify email views (EN/RU, `app.css`) | Dev | ☐ |
| A3 | TOTP enroll + login challenge | Dev | ☐ |
| A4 | WebAuthn enroll (optional alpha) | Dev | ☐ |
| A5 | ALTCHA on register/login | Dev | ☐ |
| A6 | Admin: clear lockouts + audit view | Dev | ☐ |
| A7 | Mobile soak: register on phone Chrome, verify, login | QA | ☐ |
| A8 | Document secrets in INFRA + config.example.php | Dev | ☐ |
### §10.6 Mobile-friendly UX (not BE-only)
| Requirement | Implementation |
|-------------|----------------|
| Viewport | Already `width=device-width` in [`layout.php`](../examples/crash_reporter/backend/views/layout.php) |
| Touch targets | Buttons ≥ 44px; reuse `.login-card`, `.btn` from [`app.css`](../examples/crash_reporter/backend/public/assets/css/app.css) |
| i18n | Extend [`en.json` / `ru.json`](../examples/crash_reporter/backend/public/assets/i18n/) |
| Email links | Single primary button “Confirm email” — no raw URL only |
| 2FA QR | Responsive `<img>` or SVG; “Cant scan?” reveals manual secret |
| Errors | Same `.alert` pattern as login |
| Android app | Settings → “Project account” → opens Custom Tab to account/security (post-alpha polish OK) |
### §10.7 Alpha implementation plan
| Phase | Scope | Estimate |
|-------|--------|----------|
| **P0 — Email infra** | §9 DNS + forward + outbound SMTP test | 0.51 day ops |
| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 23 days |
| **P2 — TOTP login** | Enroll QR + challenge on login | 12 days |
| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day |
| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 12 days optional for alpha |
| **P5 — Mobile app link** | Custom Tab entry points | 0.5 day |
**Alpha sign-off (auth):** new user can register on phone, verify email, enroll TOTP, log in with 2FA; admin can clear lockout; inbound `info@` mail reaches Gmail.
Update [`ALPHA.md`](ALPHA.md) § G (auth) and [`ROADMAP.md`](ROADMAP.md) alpha backend checklist when P1 starts.
---
## UI/UX alignment (existing consoles)
Do **not** introduce a new design system. Reuse:
| Asset | Path |
|-------|------|
| Styles | `public/assets/css/app.css``.login-page`, `.login-card`, `.alert`, `.muted`, `.locale-picker` |
| Layout shell | `views/layout.php` — nav, hub card styling |
| i18n | `public/assets/js/i18n.js` + JSON catalogs |
| Analytics hook | `AnalyticsHead::render()` on auth pages (optional) |
New views (planned):
- `views/register.php`
- `views/verify_email.php`
- `views/account_security.php`
- `views/two_factor_challenge.php`
Hub landing contact form (`landing-pages.inc.php`) should eventually use **`info@apps.f0xx.org`** as `mailto:` — align when email live.
---
## Security & compliance notes
- Enforce **rate limits** on register, login, verify resend (e.g. 5/min/IP hash).
- Lock account after N failed 2FA attempts; unlock via email or admin.
- Do **not** log passwords or TOTP codes.
- Backup codes: generate 10 one-time codes, display once, store bcrypt hashes.
- GDPR-style minimization: optional `auth_attempts` retention job (purge > 90 days).
---
## Open questions
| # | Question | Default proposal |
|---|----------|------------------|
| Q1 | Require WebAuthn for alpha or TOTP-only? | **TOTP required**; WebAuthn optional P4 |
| Q2 | Allow registration on prod immediately or invite-only? | **Open register** with captcha; `viewer` role |
| Q3 | Single Gmail inbox for all aliases? | Yes for now (`foxxspambox0@gmail.com`) |
| Q4 | Separate `noreply@` for outbound? | Yes — no forward needed; SPF only |
---
## Source linkage
| Topic | Doc / code |
|-------|------------|
| Infra / DNS | [INFRA.md](INFRA.md) |
| Crash console | [CRASH_REPORTER.md](CRASH_REPORTER.md), [backend README](../examples/crash_reporter/backend/README.md) |
| RBAC | [backend README § RBAC](../examples/crash_reporter/backend/README.md#rbac-phase-1--admin-ui) |
| Alpha gates | [ALPHA.md](ALPHA.md), [ROADMAP.md](ROADMAP.md) |
| PDF build | [`scripts/build-all-docs-pdf.sh`](../scripts/build-all-docs-pdf.sh) |

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,370 @@
# Android Cast — alpha priorities (2026-06-08 renumber)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-08 |
| Last modification date | 2026-06-08 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | pre-requisite to |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Owner-agreed task order, dependencies, and implementation prep. Supersedes informal numbering in older roadmap tickets._
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Bottom line (agent rules)](#bottom-line-agent-rules)
- [Priority overview](#priority-overview)
- [0.x Emergency debug (not alpha blocker)](#0x-emergency-debug-not-alpha-blocker)
- [0.1 FR — Stream dump while casting](#01-fr-stream-dump-while-casting)
- [1.x Graphs console](#1x-graphs-console)
- [2.x Email → SMTP → 2FA → RBAC](#2x-email-smtp-2fa-rbac)
- [3.x Auth implementation (agent)](#3x-auth-implementation-agent)
- [4.x Reverse SSH alpha](#4x-reverse-ssh-alpha)
- [5.x Alpha backend validation](#5x-alpha-backend-validation)
- [6.x Alpha app LAN](#6x-alpha-app-lan)
- [9.x Landing (lowest)](#9x-landing-lowest)
- [Dependency graph](#dependency-graph)
- [Licenses & tests checklist](#licenses-tests-checklist)
- [Developer help (blocked on you)](#developer-help-blocked-on-you)
- [Source linkage](#source-linkage)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Bottom line (agent rules)
Before and during alpha implementation:
| Rule | Action |
|------|--------|
| **Docs** | All specs in `docs/` — classic TOC template; PDF via `scripts/build-all-docs-pdf.sh` |
| **Alpha must-have** | RSSH (4.x), auth (2.5 + 3.x), backend validation (5.x) — see [ALPHA.md](ALPHA.md) |
| **UI/UX** | Reuse crashes console `app.css`, hub styles, EN/RU i18n — no new design system |
| **Licenses** | Update `app/.../licenses/combined.html` + `THIRD_PARTY_LICENSES.md` when adding libs/tools |
| **Tests** | Unit tests for pure logic; simulate network/SSH where E2E needs no device; run `./gradlew :app:testDebugUnitTest` + PHP scripts |
| **Build** | `./gradlew assembleDebug` after native/Gradle changes |
| **Dev input** | Blocked items → brief list in `tmp/YYYYMMDD_help_request.md` + PDF; remind at end of session |
| **Git on `next`** | Alpha sprint may commit directly on `next`; **atomic commits** per feature for easy `git revert` — [GIT_FLOW.md §4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint) |
---
## Priority overview
| Tier | IDs | Owner split | Alpha blocker? |
|------|-----|-------------|----------------|
| **P0 debug** | 0.1 | Agent impl; enables 6.5/6.6 | No |
| **P1** | 1.x | Agent | No |
| **P2** | 2.12.4 dev, 2.5+3.x agent | Mixed | **Yes** (auth path) |
| **P3** | 4.x | Agent (+ deploy assist) | **Yes** (RSSH) |
| **P4** | 5.x | 50/50 OTA/URLs; rest agent | **Yes** |
| **P5** | 6.x | ~99% dev | LAN alpha |
| **P9** | 9.x landing | Agent when idle | No |
**Effective order (agent):** 0.1 (parallel) → 4.x + 2.5/3.x + 5.35.5 → 1.x → 9.x when bandwidth allows.
---
## 0.x Emergency debug (not alpha blocker)
### 0.1 FR — Stream dump while casting
**Priority:** High emergency debug. **Cyclic with 6.6** (same capability; 6.6 references 0.1 for audio lag / lifecycle analysis).
**Goal:** While audio or video cast is active, optionally record what flows through the encode/send (or receive/decode) path for offline analysis.
| Sub | Mode | Deliverable |
|-----|------|-------------|
| **0.1.1** | Structured | `YYYYMMDD_HHMMSS_sess_record.zip` containing `meta.json` + streams |
| **0.1.2** | Raw quick | Same archive layout; **uncompressed** elementary streams only |
| **0.1.3** | Tooling | Update session stats / graphs ingest to read `meta.json` schema |
**Archive layout (both 0.1.1 and 0.1.2):**
```text
20260608_120422_sess_record.zip
├── meta.json
├── 20260608_audio_stream.raw # or .aac / .opus per meta
└── 20260608_video_stream.raw # or .h264 / annex-b per meta
```
**`meta.json` (sketch):**
```json
{
"schema_version": 1,
"session_id": "20260608_120422",
"role": "sender",
"record_mode": "raw_elementary",
"started_at_epoch_ms": 0,
"ended_at_epoch_ms": 0,
"video": {
"mime": "video/avc",
"width": 1280,
"height": 720,
"frame_count": 1200,
"file": "20260608_video_stream.raw",
"format": "length_prefixed_avc",
"csd0_hex": "…",
"csd1_hex": "…"
},
"audio": {
"mime": "audio/mp4a-latm",
"sample_rate": 44100,
"channels": 2,
"frame_count": 800,
"file": "20260608_audio_stream.raw",
"format": "length_prefixed_aac"
},
"cast_settings_snapshot": { "transport": "udp", "protection": "NONE" }
}
```
**Implementation hooks (prep):**
| Tap point | File | Notes |
|-----------|------|-------|
| Sender video frames | `ScreenCastService` / `CastSendPump` | After `onEncodedFrame`, before UDP wrap |
| Sender audio frames | `AudioEncoderSink.Callback.onEncodedFrame` | Same |
| Receiver (optional phase 2) | `ReceiverCastService` / `AudioDecoder` | Inbound before decode |
**Dev gate:** Developer settings → **Record stream dump** (off by default); cap size/duration; write under `files/stream_dumps/`.
**Spec detail:** [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md)
---
## 1.x Graphs console
| ID | Task | Owner |
|----|------|-------|
| **1** | Graphs dashboard — parent | Agent |
| **1.1** | All pending **fixes** (5.1 brick drill-down, perf, permalinks, etc.) | Agent |
| **1.2** | All pending **FRs** (5.2 columns 1/2/4, full-width grid) | Agent |
Depends on BE deploy (soft). Not alpha blocker.
---
## 2.x Email → SMTP → 2FA → RBAC
| ID | Task | Owner | Notes |
|----|------|-------|-------|
| **2.1** | DNS: MX + SPF for mail — **use apex `f0xx.org`** if `apps.f0xx.org` is CNAME (cannot add MX) | **Developer** | `tmp/20260608_help_request.md` |
| **2.2** | Register forward addresses (`info@`, `admin@`, `root@` → Gmail) | **Developer** | Cloudflare Email Routing or ImprovMX |
| **2.3** | Inbound forward verification | **Developer** | Send test mail |
| **2.4** | Gmail filters / reply-as (optional) | **Developer** | |
| **2.5** | Outbound SMTP from BE (`noreply@`, verification mail) | **Agent** | Symfony Mailer; `config.php` |
| **3.x** | Registration + verify + TOTP + mobile UI + migration `008` | **Agent** | See [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) |
| **→ RBAC** | Finish admin UI after pages stable | **Agent** | Former task 7 |
Chain: `2.1 → 2.2 → 2.3` (dev) ∥ `2.5` (agent after 2.1 DNS) → `3.x` → RBAC.
---
## 3.x Auth implementation (agent)
All items from [20260607-2FA-email-mobile-auth-flow.md §10.7](20260607-2FA-email-mobile-auth-flow.md#107-alpha-implementation-plan):
- P1 Register + verify + ALTCHA
- P2 TOTP enroll + login challenge
- P3 Admin lockout clear + audit
- P4 WebAuthn (optional alpha)
- P5 Android Custom Tab entry (optional)
**Tests:** PHP API smoke scripts; unit tests for token hash / TOTP verify helpers.
---
## 4.x Reverse SSH alpha
**Replace WireGuard as alpha deliverable** — WG stays for lab ([REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md)).
| ID | Task | Owner |
|----|------|-------|
| **4.1** | BE: `opt_in_mode: rssh`, bastion user, `authorized_keys` / `-R` port map | Agent |
| **4.2** | BE: nginx `stream` or SSH on 443 per [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | Agent + deploy |
| **4.3** | Android: `RemoteAccessMode.RSSH` selectable, no `VpnService` | Agent |
| **4.4** | Android: outbound SSH client (libssh2 / dropbear JNI — TBD) + FG service | Agent |
| **4.5** | E2E: whitelist → open session → operator shell/SFTP on bastion | Agent + QA |
| **4.wg** | WireGuard fixes only if lab broken | Agent as needed |
Depends on: BE deploy (5.x). Blocks **5.3**.
---
## 5.x Alpha backend validation
| ID | Task | Owner split |
|----|------|-------------|
| **5.1** | OTA channel + artifacts on `apps.f0xx.org` | 50% dev / 50% agent |
| **5.2** | Crash + OTA URLs on device (`settings.json`, dev settings) | 50% dev / 50% agent |
| **5.3** | RSSH BE validation | Agent (deploy assist dev) — **depends 4.x** |
| **5.4** | auth-2fa validation (G2G6) | Agent |
| **5.5** | RBAC validation | Agent |
---
## 6.x Alpha app LAN
| ID | Task | Owner | Deps |
|----|------|-------|------|
| **6.1** | Opus/Speex E2E | Dev + agent JNI | native `.a` |
| **6.2** | LAN soak A/B/C | Dev | — |
| **6.3** | App lifecycle (notifications, recents) | Dev | |
| **6.4** | Rotation + preview crash | Dev | ↔ 6.3 cyclic |
| **6.5** | Audio lag — buffers, handshake, graphs | Dev | **0.1 helps** |
| **6.6** | Debug stream analysis | Dev | **= 0.1** |
Agent: fixes / urgent FRs only (~1%).
---
## 9.x Landing (lowest)
Renumbered from former 6.x landing track:
| ID | Task |
|----|------|
| **9** | Landing hub parent |
| **9.1** | Hub cards (graphs, builder, RA) |
| **9.2** | Globe / earth animations |
| **9.3** | Mobile chrome validation (LOW) |
---
## Dependency graph
```mermaid
flowchart TB
subgraph P0["P0 debug"]
S01["0.1 stream dump"]
end
subgraph P2["P2 auth — dev"]
D21["2.1 DNS"]
D22["2.2 forward"]
D23["2.3 verify inbound"]
end
subgraph P2a["P2 auth — agent"]
A25["2.5 SMTP"]
A3["3.x 2FA"]
RBAC["RBAC"]
end
subgraph P3["P3 RSSH"]
R4["4.x reverse SSH"]
end
subgraph P4["P4 backend alpha"]
B51["5.1 OTA"]
B52["5.2 URLs"]
B53["5.3 RSSH validate"]
B54["5.4 auth validate"]
end
subgraph P5["P5 app LAN — dev"]
A61["6.1 Opus"]
A62["6.2 soak"]
A63["6.3 lifecycle"]
A64["6.4 rotation"]
A65["6.5 audio lag"]
end
subgraph P9["P9 landing"]
L9["9.x"]
end
ALPHA["FULL_ALPHA"]
S01 -.-> A65
S01 -.-> A64
D21 --> D22 --> D23
D21 --> A25 --> A3 --> RBAC --> B54
A3 --> ALPHA
R4 --> B53 --> ALPHA
B51 --> ALPHA
B52 --> ALPHA
A62 --> ALPHA
A63 -.-> A64
A64 -.-> A63
L9 -.-> RBAC
```
---
## Licenses & tests checklist
| When adding… | Update |
|--------------|--------|
| libssh2 / dropbear / Symfony Mailer | `combined.html`, `THIRD_PARTY_LICENSES.md` |
| Stream dump (no new deps if pure Java) | — |
| RSSH BE scripts | BE licenses doc |
| Area | Test command |
|------|----------------|
| Android unit | `./gradlew :app:testDebugUnitTest` |
| RSSH API | `BASE=… ./scripts/ra_e2e_cli.sh` (extend for RSSH) |
| Auth API | `scripts/test_rbac_api.sh` + new `test_auth_api.sh` (TBD) |
| Stream dump | Unit test `StreamDumpWriter` framing + `meta.json` schema |
---
## Developer help (blocked on you)
**Email/DNS (2.12.4):** hands-on checklist with record examples:
**[tmp/20260608_help_request.md](../tmp/20260608_help_request.md)** (PDF: `tmp/20260608_help_request.pdf`)
Please complete 2.12.3 before agent enables production verification mail (2.5).
---
## Source linkage
| Doc | Topic |
|-----|--------|
| [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) | 0.1 implementation spec |
| [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) | 2.x / 3.x |
| [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) | Ticket-level graph |
| [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) | 4.x |
| [ALPHA.md](ALPHA.md) | Sign-off F/G |

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,278 @@
# BE services & infrastructure — validation map
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-08 |
| Last modification date | 2026-06-08 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Last validated: 2026-06-08. Companion to [INFRA.md](INFRA.md), [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md)._
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Topology](#topology)
- [Service inventory (mobile + console)](#service-inventory-mobile-console)
- [Data paths](#data-paths)
- [Cast session (LAN — alpha scope)](#cast-session-lan-alpha-scope)
- [Telemetry path](#telemetry-path)
- [Remote access control plane](#remote-access-control-plane)
- [Validation status](#validation-status)
- [DNAT chain (WireGuard lab)](#dnat-chain-wireguard-lab)
- [RSSH / RBAC](#rssh-rbac)
- [Planned: SFU relay (Janus-class)](#planned-sfu-relay-janus-class)
- [Agent scripts](#agent-scripts)
- [Open findings → tickets](#open-findings-tickets)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Topology
```mermaid
flowchart LR
subgraph Internet
MOB["📱 Android app"]
OPS["🖥 Operator browser"]
end
subgraph Router["f0xx.org router"]
RDNAT["UDP 45340 DNAT"]
end
subgraph FE["FE f0xx-monstro · apps.f0xx.org"]
TLS["TLS :443"]
FEDNAT["UDP 45340 DNAT"]
NGXFE["nginx reverse proxy"]
end
subgraph BE["BE Alpine artc0 · :80"]
NGXBE["nginx apps.conf"]
FPM["PHP-FPM"]
DB["MariaDB / SQLite"]
WG["wg0 :45340"]
SSH["sshd bastion :443"]
end
MOB -->|"HTTPS crashes/graphs/heartbeat"| TLS
OPS --> TLS
TLS --> NGXFE --> NGXBE --> FPM --> DB
MOB -->|"UDP WG lab"| RDNAT --> FEDNAT --> WG
MOB -->|"RSSH outbound"| SSH
FPM --> WG
```
| Hop | Host | Role |
|-----|------|------|
| Public DNS | `apps.f0xx.org`, `ra.apps.f0xx.org` | CNAME → FE |
| Router | `f0xx.org` (`134.17.26.161` WAN) | UDP `45340` → FE `10.7.0.10` |
| FE | Gentoo HVM | TLS, HTTP proxy → BE `:80`, UDP DNAT → BE `10.7.16.128` |
| BE | Alpine `10.7.16.128` | PHP consoles, device APIs, `wg0`, RSSH bastion |
---
## Service inventory (mobile + console)
| Service | Public URL / port | Mobile uses | Console uses | Status |
|---------|-------------------|-------------|--------------|--------|
| **Hub** | `https://apps.f0xx.org/app/androidcast_project/` | Indirect (links) | Landing | ✅ OK |
| **Crashes console** | `…/crashes/` | — | Reports, home | ✅ OK |
| **Crash upload** | `…/crashes/api/upload.php` | ✅ gzip JSON | — | ✅ OK |
| **Heartbeat / RA** | `…/crashes/api/heartbeat.php` | ✅ RA poll | — | ✅ OK |
| **Session stats / graphs ingest** | `…/graphs/api/graph_upload.php` | ✅ (when enabled) | — | ✅ OK |
| **Graphs dashboard** | `…/graphs/` | — | Analytics UI | ✅ OK |
| **Tickets** | `…/crashes/?view=tickets` | — | Issue tracking | ✅ OK |
| **Remote access** | `…/crashes/?view=remote_access` | — | WG/RSSH ops | ✅ OK |
| **RBAC admin** | `…/crashes/?view=rbac` | — | Roles / lockouts | ✅ OK |
| **APK builder** | `…/build/` | — | Internal builds | ✅ OK |
| **OTA channel** | `…/v0/ota/channel/stable.json` | ✅ `settings.json` | — | <span style="color:#dc2626">**PENDING**</span> HTTP 400 — not on apps vhost |
| **OTA manifest/APK** | TBD host/path | ✅ | — | <span style="color:#dc2626">**PENDING**</span> align with device `ota.base_url` |
| **Diag API** | `…/crashes/api/diag.php` | Optional | — | ⚠️ 403 at edge (use upload/heartbeat) |
| **WireGuard data** | `ra.apps.f0xx.org:45340/udp` | ✅ lab mode | — | ✅ DNAT chain + BE listen |
| **RSSH bastion** | `ra.apps.f0xx.org:443/tcp` | ✅ alpha mode | Operator SSH | ✅ API connect payload |
---
## Data paths
### Cast session (LAN — alpha scope)
```text
Sender UDP/TCP :41235 ←→ Receiver :41235
Discovery UDP :41234 (+ NSD)
```
No BE required for LAN alpha.
### Telemetry path
```text
Phone → HTTPS → FE → BE PHP
upload.php crash JSON
heartbeat.php session + RA
graph_upload.php session metrics
```
### Remote access control plane
```text
Phone poll → heartbeat.php (type:ra)
Operator → remote_access.php (whitelist, open_session, dashboard)
BE → wg set / rssh session row → connect JSON on next poll
```
---
## Validation status
Run: `examples/crash_reporter/backend/scripts/validate_be_services.sh`
| Check | 2026-06-08 result |
|-------|-------------------|
| Hub / crashes / graphs / build HTTP | ✅ |
| JS syntax (app, graphs, tickets, i18n) | ✅ |
| RBAC API smoke (on BE localhost) | ✅ |
| RA API smoke | ✅ (BE) |
| RSSH API smoke | ✅ `test_rssh_api.sh` |
| OTA public URL | <span style="color:#dc2626">FAIL 400</span> |
| Browser SyntaxError (historical) | ✅ cleared — deployed JS passes `node --check` |
---
## DNAT chain (WireGuard lab)
```mermaid
sequenceDiagram
participant M as Mobile / laptop
participant R as Router f0xx.org
participant F as FE monstro
participant B as BE wg0
M->>R: UDP 45340 (public)
R->>F: DNAT → 10.7.0.10:45340
F->>B: DNAT → 10.7.16.128:45340
B->>B: wg0 decrypt
```
| Script | Host | Persist hook |
|--------|------|--------------|
| `wireguard_router_dnat.sh` | `f0xx.org` `/home/scripts/` | `iodine_warmup_helper.sh apply_rules` |
| `wireguard_fe_dnat.sh` | `f0xx-monstro` | `xen_firewall.sh start/stop` |
| `install_be_remote_access.sh` | `alpine-be` | `wg0` + sudoers |
---
## RSSH / RBAC
| Component | Path | Tune on BE |
|-----------|------|------------|
| RSSH sessions | `sql/migrations/009_rssh_sessions.sql` | MariaDB migrate |
| Provisioner | `src/RsshSessionProvisioner.php` | `config.php``remote_access.rssh` |
| Bastion | `sshd` / stream on `:443` | host keys, port range `1800018999` |
| RBAC API | `api/rbac.php` | `privilege_sets`, lockout clear |
| Cron purge | `scripts/purge_remote_access.php` | hourly crontab |
**Smoke:** `test_rssh_api.sh`, `test_rbac_api.sh`, `ra_e2e_cli.sh`
---
## Planned: SFU relay (Janus-class)
> **Not in alpha.** Gray = future design surface; implementation on branch `feature/sfu-relay`, hidden from users until spec lands.
```mermaid
flowchart TB
classDef future fill:#e5e7eb,stroke:#9ca3af,color:#374151
subgraph Future["SFU path (planned)"]
SFU["Janus / mediasoup SFU<br/>BE :8089 or dedicated"]
SIG["Signaling API<br/>PHP or sidecar"]
TURN["TURN optional"]
end
subgraph Today["Alpha today"]
LAN["LAN UDP/TCP cast"]
RA["RSSH debug"]
end
MOB2["📱 Android"] --> LAN
MOB2 -.->|"post-alpha"| SIG
SIG -.-> SFU
SFU -.-> TURN
OPS2["Operator"] -.-> SIG
class SFU,SIG,TURN future
```
| Piece | Status | Branch / flag |
|-------|--------|---------------|
| `CastConfig.SFU_RELAY_PREVIEW` | Stub `false` | `feature/sfu-relay` |
| `config.php``sfu.enabled` | Stub `false` | BE |
| `api/sfu_health.php` | Returns `disabled` | BE |
| Android `SfuRelayGate` | Hidden dev compile gate | App |
| FE nginx `location /janus/` | Exists for other stack — **do not mix** with androidcast vhost | INFRA §8089 |
| Nginx + UDP ports for WebRTC | <span style="color:#6b7280">Design TBD with owner spec</span> | — |
| Mobile `TRANSPORT_WEBRTC` UI | Frozen (`ALPHA_FEATURE_FREEZE`) | — |
When the owner spec arrives: wire signaling auth to existing session/RBAC, add SFU room lifecycle beside (not replacing) LAN cast, and extend this doc § with concrete ports and URLs.
---
## Agent scripts
| Script | Purpose |
|--------|---------|
| `validate_be_services.sh` | Public reachability + JS syntax |
| `verify_remote_access_prod.sh` | BE WG/RSSH preflight |
| `test_rssh_api.sh` | RSSH connect payload |
| `test_rbac_api.sh` | RBAC panel API |
| `ra_e2e_cli.sh` | Full RA operator+device flow |
| `test_graphs_api.sh` | Graph ingest + dashboard |
---
## Open findings → tickets
| ID | Severity | Topic | Track in |
|----|----------|-------|----------|
| **SVC-OTA** | High | OTA `v0/ota` returns 400 on `apps.f0xx.org` — wire nginx + staging path | **Tickets** dashboard |
| **SVC-DIAG** | Low | `diag.php` 403 at public edge | **Issues** (documented workaround) |
| **SVC-URL** | Medium | Legacy `f0xx.org` crash URL 404 — devices should use `apps.f0xx.org` | **Tickets** + device `settings.json` |
| **SFU-SPEC** | Planning | Owner SFU spec integration | **Issues** (gray / post-alpha) |
File via crashes console → **Tickets** for service/URL/nginx work; **Issues** for infra validation notes and SFU planning.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,160 @@
# Stream dump — developer debug recording (FR 0.1)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-08 |
| Last modification date | 2026-06-08 |
| Co-authored | |
| Severity | medium |
| State | postponed |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Emergency debug — not an alpha release blocker. Enables 6.5 (audio lag) and 6.6 analysis._
**Parent:** [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md) §0.1
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Goals](#goals)
- [Modes 0.1.1 vs 0.1.2](#modes-011-vs-012)
- [Archive format](#archive-format)
- [meta.json schema](#metajson-schema)
- [Wire frame format (raw files)](#wire-frame-format-raw-files)
- [Android implementation plan](#android-implementation-plan)
- [Visualization / 0.1.3](#visualization-013)
- [Tests](#tests)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Goals
- Record **elementary** encoded streams during an active cast session (sender first; receiver optional later).
- Produce analyst-friendly bundles compatible with session stats tooling (0.1.3).
- Dev-only; default **off**; bounded disk (e.g. 100 MB or 5 min).
---
## Modes 0.1.1 vs 0.1.2
| Mode | ID | Content | Use |
|------|-----|---------|-----|
| **Structured** | 0.1.1 | Raw annex-B / ADTS-style with rich `meta.json` | Preferred for graphs replay |
| **Raw quick** | 0.1.2 | Same files, minimal meta, no zip compression of streams | Fastest path to ship |
Both use the **same archive naming**; only `record_mode` and optional fields differ.
---
## Archive format
```text
{filesDir}/stream_dumps/20260608_120422_sess_record.zip
meta.json
20260608_audio_stream.raw
20260608_video_stream.raw
```
Pull via `adb pull` or share intent from dev settings.
---
## meta.json schema
| Field | Type | Description |
|-------|------|-------------|
| `schema_version` | int | `1` |
| `session_id` | string | `yyyyMMdd_HHmmss` |
| `role` | string | `sender` \| `receiver` |
| `record_mode` | string | `raw_elementary` \| `compressed_wire` (future) |
| `started_at_epoch_ms` | long | |
| `ended_at_epoch_ms` | long | |
| `video` | object? | absent if audio-only |
| `audio` | object? | absent if video-only |
| `cast_settings_snapshot` | object | transport, protection, codecs |
Per-stream object:
| Field | Description |
|-------|-------------|
| `mime` | e.g. `video/avc`, `audio/mp4a-latm` |
| `file` | basename inside zip |
| `format` | `length_prefixed` — see below |
| `frame_count` | int |
| `sample_rate` / `channels` | audio |
| `width` / `height` | video |
| `csd0_hex` / `csd1_hex` | codec config (video) |
---
## Wire frame format (raw files)
Each frame in `.raw`:
```text
[4 bytes BE length][payload bytes]
```
Payload = same bytes as `CastProtocol` audio/video frame body (after codec). PTS optional in meta sidecar or future v2 header.
---
## Android implementation plan
| Step | Component |
|------|-----------|
| 1 | `AppPreferences.KEY_DEV_STREAM_DUMP` |
| 2 | `StreamDumpWriter` — thread-safe append, rotate on session end |
| 3 | Hook `CastSendPump` or encoder callbacks when flag on |
| 4 | On `ScreenCastService` stop → finalize zip + `meta.json` |
| 5 | Dev settings UI: toggle + “Open dumps folder” |
**No new native deps** if Java-only zip (`java.util.zip`).
---
## Visualization / 0.1.3
| Tool | Update |
|------|--------|
| `SessionStatsRecorder` / graphs ingest | Accept `meta.json` + frame counts |
| Future offline decoder script | `scripts/decode_stream_dump.py` (TBD) |
---
## Tests
- `StreamDumpWriterTest` — length-prefix round-trip, zip contains expected names
- Manual: cast 30s with dump on → pull zip → verify `frame_count` > 0

View File

@@ -0,0 +1,289 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 13 0 R /F5 14 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 762.0394 66.58104 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 751.0394 119.949 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 740.0394 97.25304 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 729.0394 110.597 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 718.0394 146.581 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 707.0394 147.061 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 696.0394 115.053 707.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 685.0394 65.24504 696.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
17 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Outlines 23 0 R /PageMode /UseNone /Pages 32 0 R /Type /Catalog
>>
endobj
22 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Stream dump \204 developer debug recording \(FR 0.1\)) /Trapped /False
>>
endobj
23 0 obj
<<
/Count 8 /First 24 0 R /Last 31 0 R /Type /Outlines
>>
endobj
24 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 25 0 R /Parent 23 0 R /Title (Goals)
>>
endobj
25 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 26 0 R /Parent 23 0 R /Prev 24 0 R /Title (Modes 0.1.1 vs 0.1.2)
>>
endobj
26 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 27 0 R /Parent 23 0 R /Prev 25 0 R /Title (Archive format)
>>
endobj
27 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 28 0 R /Parent 23 0 R /Prev 26 0 R /Title (meta.json schema)
>>
endobj
28 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 29 0 R /Parent 23 0 R /Prev 27 0 R /Title (Wire frame format \(raw files\))
>>
endobj
29 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 30 0 R /Parent 23 0 R /Prev 28 0 R /Title (Android implementation plan)
>>
endobj
30 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 31 0 R /Parent 23 0 R /Prev 29 0 R /Title (Visualization / 0.1.3)
>>
endobj
31 0 obj
<<
/Dest [ 15 0 R /Fit ] /Parent 23 0 R /Prev 30 0 R /Title (Tests)
>>
endobj
32 0 obj
<<
/Count 6 /Kids [ 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R ] /Type /Pages
>>
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 856
>>
stream
Gat=*a`=.1&A@rkqO")$MHX\E@qNh^:$%P*b/kI>a9a9t*XkX%n#+"</7W1/@9Z4'p\K$5[30F7YkhYE!8i16]gk<grYeTg$2t&)oOiYdpbbr!VHq.e(h4D,4@qLf1[YT?3PZV1F5m<X8pCslH^PHn*o%V%@r`^m<Tui+dXfsPk<bBg&)h8cB/YPh/E/64KeO(lGIpGU2PYbOMAOA#.a>gYH%G)-r=a.)bNis5hn[!XH'aJs5LOUH^IG(=rJIAJ&c-?O^VePb!\?^4*:^>p!PWZkY,2*W2;2/7"hU;#f(oRR.DpgS.REG&g8-o1]!N"Rqd@:)'':TOp6\+f`152BL%PmpQ;9Y5n7,s174=keegQ!=ft2piSEJFhnB1@8c:*R,k,jr4!4Ss@nc(YE:1leV]^"c`R]':[<G11&a<J5&:KA)XA<N_%K_[X\'Xl';7[F9C&.3VO35i8C]-;`!ZOsHI5b6tM!dErQ$$MrL35+mu)j@00pQL#dG#=]u(OcBD%S/I@e*roOU7A#G)!(Xp4ed/3"$sQcSC>M!d&e[niINm\Lsn%]8#n'>9_U@n!'*44krboEgQ#C:b4PFT$-qU&(3DlT;>tjthfS;=L/ANnA-aPqBT\@EdHSU(FI-ppA0CE,h5R27dT$n6,_u;=4$rUNrnOY_'<:`M&anQKI@Drm(oc_"dVGJ(dDZ8Q%OZ_\S4Mc-"r<J\[gg"!c!(9S6V\2X2hnb41h>^UTP`Ztb3OSj#%K\*di``;Tc8[O+Cp+D3XDX[X"15?TJZQ!.WL613nD>MG;P;JBlpUdZcks.US6T=%S`#KY>_c-%*C:5?.sIU@LCMb/s]QE\"Gg:!4..b=+Tf~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 576
>>
stream
Gatn#9lJc/(kD)N(%3m5Jb"C%Gdg^FHWd.^V;!:o`#Q)0g*QnLOnS*2S?,oK8@9oTa]JcEH/9'a5h!O:>1N]*)D5q>OF^^=Ag@KW"f?V<H\A;f=\aXmBa.I-6@"]ipX1gjMXd2=iJ!q&!KfAm15,.4PeN"#(o3hj`*aqs@gBkQS"GrL^OjlmX_ld:!W!E#VZZg3)X$mIIXob*GQcPcL`QHFS#QuR.u#7nr6e'-":;s5Gf+h5E&Elq<@HI:B49mhD;$%Ep5s*^7G"t*j>$K8@_&qTklF:/'`<^o.Hk1)J"ac-*6]I)Y-N%rdDL\#K@QVt#4Il9l:kjUeFf\].4^n12",dI+re\b]8Kll\N`U^A_2pSLOnGP)/'udr:2$P3F#HE&C58+JuI.QUm+e/"N'L#N=s?o;B03p71Tolf"KsqdXDBiR@o/3`P)7/]p^N"&$rmeA:"2n9BmhI)3YVKfk%@-bu9;/%o;+%\=;9hcIol?&__AB[^b.0Ht^u4CI;S]0MT+O+d+e#':C"9!O([]r,VoI,#1Z#Ij*oY&lbh9/13HPlG=LH<*JgD"/EUuW*HY~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3227
>>
stream
Gb"/)lYkP!&c_:6@_NOq%Q;SnRhlcmF*91H`i*D5H77&D@R[FF-@BArfhm\,n"&Q5'ZHs_'I_<6M*gTKG3?+l^R84+14DdnY8[PU=hrQl!^qUY$A^$CYkS4-MmG'LUnlL^afoki"4K.R7Fk.[n*FO3JYc"O8ath8dUcl,"PVa\I>3rKH6'#]_]m$!W*nbGOMq_Pi;>KDI*EjZ#9I)efC5Ra-:DhX1]eR@)[`)=N2@#L60s&4>_:^d/d%DkJLmD<l.5H9#jD-L'u&_eY6KI)@$g_hoElA+@.Al2oja%BVI%ICj`&I:Z0#f#=)ShX`7j>N_q/jV:H=?=+7iWgK(IrB:h7P,%G>d)5MX#cG4$I/s6\eDir/8>'RD1#B$(aSZ8+jH9ptWX_QY=le[5DZ"&1(G!sGW7)91#;7$:QEpClbVKEemj#]rWq'P2dporONM5CmE>L91kX6-PdjAO8_:"`a7aIc@nP^n5*TU_?EFX!hQ"%(N'm]S*Z*jGB6N4>8_V*gEg*HI=gbG).2-q6'*P\0YUFH&uX.d4b`BbPc+Fmg;#FT,G-qbogS3lGjKSK9SeGj08*"R_?^(j"_n>&18?Udf;<t`q\#`#s/2gfhc<-7^pn?K'WB*$[^30DsO]L+(giimc3ki*A"\pKj3/g_h;VcDI[q]ZV<#ARaE[?/<0fUm*dt4'M4k(C.nA''A]nUb;]iPYOO#Ed;=uQXVJ$?/Mk(pAW.h,SNK.8N!j7G>/H,,\9e<4#9?,qR^R[oQ;l[<HbJ4]baMq:Qf"JaV''kOi48WDd^p^@.X&IB)3+G;fhtdT"\H*2fsF#]D/3q_3?R3q\qpg&-&!`O>S7eM%otn;9CXH&1p'&qCFr)QS=KlOn+%d#lZfDkS&[C_N$uhG:fBY9qRm`V(G8rJY]KgZ]'@fT_3;l*m@:udrCnqH7E#]`)lKe2V`;igr9.^oU=3e]Xe!9(f<hZ8k.>(1oW)7tkK`dgnZA9t3V17U5D_I9EmJ$@>lT]4]=I:ni%P\NecU?:;_DNd0AjP[>i.(R0@O`=*c';bkM18?eR>0e/C$VK;<_!t8)qUY0CV,;/&kGUYej=Pc"XITEUtFVh=XPk7%J$M+o[h1msZ=eg*GfN<pMuJp/W-l>?J)JplU;UNqaHI0#[SPLDV+e3A(kf_E\!p[[RP$]F7O)Wf"dU#+I:3>E%\Q"$/oj%E:V'2u@/Ck4o2\IQEZD`RJu4gTBq"T+R.%96DG6qm-s.!dMoojQ>q%_9_\DY!LI[#*8Aq!'XBbGY0n!n>R6IR!.P>+;iZ?2+'g;$YtXYqOns226G6,(p4t'iK&CA<OXeV[cMsOWLK9APQYLSrs1EnM:#:,9MD`$CEnO4GSoQ17Nf*pU6D^?OFr'%m6cHf.+&@%+OgCE,.FQ!gra8\LS3"$iAR<Y/p'cI&;g-kBO/#\Jski^\eGfRRZR5uUA\&s;GZs5EDYccA^&&.MY:m)]u$7BT(_+EXF/q'/)S?*56(@WYr`tE)^afLSUqGImM*VP!7NC(.lDN1^sP0$^X[8cae_pTQ8j<WU3ZGqejYd<TMp;BdO_F`,LS,IZ:ff&??s9p!dOEQgU!ifKET?LDM@$mh-_-/IZ>[]`T%O"mk[<$/46n;P!a#N.1sclq^'*)YTfJ!?+"r#FZ1kIUUX<(87Se7ge(Ok'Y%TTl_57_Y!gi13SWTt`[JbB?UI&/YHEiT<UCIpMWcak@<IZB']fu`OK<U-QabrSm0>5S+j$TtT>GlY9DT;-<OO3JTgPBdpkGUtp[\Z01,(*HPWV3H:0VNrkF(l4GigWD9q(&Ar:iVs9WbSq9CIZ6.bIZRoq<Q:+R&S6r+6T9"!(eIW'J4m)Wn5MLO\a[4O8<I/e$20oEpmHQT'tXJS_mF`[&8GBqs[8bcjdn[!';eeB-u(=9M-=H5]V<@EqnH$cWWjB+k**Zt"2Y[>2,AnAXXj8P[ZrY0Gg"3![;aLLq#h@X$i@UfOn3-;AQSXWX42X/!fV#,Nb_gjr#5\Rl1*Bp',4dcFti'Pjj:3Og(G@M7]'+l_T2k%9)\QX`3p\kE9joiI6mpUm@b<;"W$.OA^h+VIOYI%!sqZed2Y2sH]-#)as:3YC`bVofmI1nmRQM>P3ehK)H@d0.4Pb;eN=8s>WT(Qe,G_auQ9?[uh-\6\uVH7_Ts)3()@Gaq`mfb8OqA"&?EQI6`sqbiDE7@HU7^0$"*87MdpWWC=!)?]b6%:F)$(r3%a5X24EkXn!g=[Q[!TB]%2EDS)>p'Xn3V6!9[\m"l&?:(Y5p.A.mUG.+&,cVocn_PaX8hA)io_pJB.U/'>'(e4a?U*nKn]RXb!/9V831@Z5ZIYC]/;*nNT(ua:1FHW")/3\g_Of*KHR+78iI:!GrID>BAe^IOF]%_4BrbX,LT)g#m']aS`DL2>7,t=%dHG.,ZgbR.Gu7ie(O/,q?W)j&U.4K>.E^=[3d_`W4]73?=0?ai]EU0FJ48d?j!:.sr3'\@Y*gJilT6P*Y[g&t1*G%nCq\i,0ZT0^"4`!O.s3bM*\9Y-.!.qQ8])f]%]:1[/*X)$_X9nc&-m*Im$h;PdI88\h8A.7P.gWsg"RL0BuBlAAqHlA6eUFgUrKRd"`dO;FuJ^Q\=HNYe:a_Y-+eiQ@4$TNB]Y/10=d$;YJ(Hhp>`kqfNAm]^s;s[!861=XMj?,Ll/9)KNS2^jG8dO=TeU5@[K$hbl5Y$^-F,0mo\b.3][MD01GcLdm;]/l\]4rcTU,Z\u"hlSDB7[]:j+cmFt52i-)jV9rll%[J`UA\pqJ1YhWe@Kf?J;"K"D4:ZsEeWE0N+1"M:.)Cg=%rRY<]mV'-VR,<iQfCERq)oBQtC"cVK:RF6$A',\$nPOe=5WALY:`<d\HTQ69Fd4[fA>`5,,e.2Dmb>Wm/($9XJLt-L7?_Yt't7u:=b.$WhP@#'_&\,Fhs^<J=c^nr)+Pa/I1L];,P,ZF=?.W-mI)aY(Xob2X<ZCqG:%<k'QiLIA)4h:bTnNhn^[jmediAU_1U;JZ7q-1j#^f#dtmf#I)8mbq*hj[,UmUoZ,gLUo[hiI3&e<rG9.P2\(>iLc*)?chcT;-&7PFKV!0So0eX3qCr><mmUmP7$A#=c%2*rQ.:"[$8\u\?!q>D=PR4'S?=$[LT>([noS2-&+k/_u",%G<#LN60Oh7-]$J^fD&Rl[*rn]_VI\u,JV&)'aUT<B*_ZN<^1r3RlTBt`%69sQrO`:"9h;BLm~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1206
>>
stream
GauHJD3LGI&H;*)YoeBc:IYBgcA0[j]jBGeg,(Nu^Nemq@c?@!"\#^MQXAW=(lW$RB/_JrD(P(4oW35.'a4gaC4;?]^oI_ff+]Wc-jAlJ&#)#PF#<`;7(8UI,`YNG#e5M:VB20DC&M#SO[LL>$D'I.N%tm0e:gL[qJ8Qsg^-:Xm.]':FU*m!r8R'nMISQ!.bhWAB'\ekj"@R?fkfPtFoajde]u"<[NE*t&D]YEXS28rT:Iq'1;Y'GF*d&n\EFp@Mo,8@UA,#6n`8D>K]A\6V.W+Ir`lYLX_;K/L8"_mP@>[b8E)c3-VciT\A&akdQCi&+cgNh"[&_>+a[!PDOm@/q5q")c]Um+ftaaK=X%\38f)&SjS(SmB0P3SDWW+eA=R;)"anTfG]#1E&iq@`lHs:MF+`SHO<<Z^]Fl/[6L.;t'n#fD"mBF+bQT2Z1ZlD'n_=qJ,mr'Ea.0reTd+oDhn'I,=lB)im.^Ba7`:(/;cO=/R[VK0L(djHeaqUi'0H*_:,e.N_tl@X$1QCNY_%3'*&Rf;VG6.:Kt>+OiB&R6SkjUp4*\hq5_=LjFgQo=]R[5gJt9Ii$Tn[ZeF,GJ)2.n_qaUZbgk1Q.[Lo_TV<TfiCsk16iH;ei0+414Qirq?XTZ:(0j3fn-RcFIk832m-M7om+1mRVUo]hc!%G&?1"N)"a*i2^ASPM;%`0_A9Y_g9*2gNZ")pH[X[enC\\8Hi27*i=nCc<<U*]02qeuX^U2(1HK[-c8$1(;h!pc1c<##/m?/]n?+3I$RL#r.&hY&3BHT!7tKMm'M1EC9(?6hsnA7Cbl"I<%mhC:cG&k[Y3f'kFd7&_8@le[Yn6X%GZ!D0P^>('*t^L$4`VGE>OGp_i$Dd';9WiS;`3BK2!NFOh[(:mAS'(P7aVKPj$fWaQ!kge5]ierWl-po(boQ(thpE=k."%)h54\C?sHc1%iITuo7XAq6D-mL9pj%6^aJHdA>=oh:jT\3/Rg!".A**eh(G$JHrr*<l;<)HkZ]bZ8[JH0^6QhpE]X@/M*JdSPhEN(\?IP=JF.AVt[WBbh\7A@a&>M;]>K#e7Q@s0o-hd_L;josUMPm5jV.r;)$buFtOYe-Br0l;@+?hX1^:p0EFT_]b=)c:22K,nMj/N8Dfm,7jKc9[[ci)(D3YhqOV2TD-:>JfgIc$(n%6Yr"2-U+B#$/q6rj79m+@.YPDYoDL~>endstream
endobj
xref
0 39
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000502 00000 n
0000000649 00000 n
0000000797 00000 n
0000000944 00000 n
0000001091 00000 n
0000001238 00000 n
0000001386 00000 n
0000001535 00000 n
0000001651 00000 n
0000001757 00000 n
0000001835 00000 n
0000002041 00000 n
0000002309 00000 n
0000002515 00000 n
0000002721 00000 n
0000002927 00000 n
0000003133 00000 n
0000003220 00000 n
0000003548 00000 n
0000003622 00000 n
0000003709 00000 n
0000003824 00000 n
0000003933 00000 n
0000004044 00000 n
0000004170 00000 n
0000004292 00000 n
0000004408 00000 n
0000004495 00000 n
0000004591 00000 n
0000005538 00000 n
0000006205 00000 n
0000006433 00000 n
0000006661 00000 n
0000009980 00000 n
trailer
<<
/ID
[<cc05b2c62200d6b1af79eff88bcbad38><cc05b2c62200d6b1af79eff88bcbad38>]
% ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R
/Root 21 0 R
/Size 39
>>
startxref
11278
%%EOF

411
20260610_SERVICES.md Normal file
View File

@@ -0,0 +1,411 @@
# Android Cast — exposed services catalog
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-10 |
| Last modification date | 2026-06-10 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Validated: 2026-06-10 after FE `apps.conf` consolidation + BE sync. Public base: **https://apps.f0xx.org**._
**Documentation index:** [README.md](README.md) · Companion: [INFRA.md](INFRA.md), [20260608_BE_SERVICES_and_infra.md](20260608_BE_SERVICES_and_infra.md)
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Validation snapshot](#validation-snapshot)
- [Hub landing](#hub-landing)
- [Console home](#console-home)
- [Issues (crash reports)](#issues-crash-reports)
- [Related endpoints](#related-endpoints)
- [Upload via curl](#upload-via-curl)
- [Bulk / synthetic issues](#bulk-synthetic-issues)
- [Tickets](#tickets)
- [Related endpoints](#related-endpoints-1)
- [Create via UI](#create-via-ui)
- [Create via curl (session cookie)](#create-via-curl-session-cookie)
- [Graphs](#graphs)
- [Related endpoints](#related-endpoints-2)
- [Fake session data (dev / BE)](#fake-session-data-dev-be)
- [Remote access](#remote-access)
- [Related endpoints](#related-endpoints-3)
- [RBAC / access control](#rbac-access-control)
- [Related endpoints](#related-endpoints-4)
- [APK builder](#apk-builder)
- [Related endpoints](#related-endpoints-5)
- [Git (Gitea)](#git-gitea)
- [OTA updates](#ota-updates)
- [Layout](#layout)
- [Publish (operator)](#publish-operator)
- [Heartbeat (device)](#heartbeat-device)
- [Auth & account security](#auth-account-security)
- [URL shortener](#url-shortener)
- [Non-HTTP services](#non-http-services)
- [Validation scripts](#validation-scripts)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Validation snapshot
Run: `examples/crash_reporter/backend/scripts/validate_be_services.sh`
| Check | URL / target | 2026-06-10 |
|-------|----------------|------------|
| Hub | `/app/androidcast_project/` | ✅ HTTP 200 |
| Crashes console | `…/crashes/` | ✅ HTTP 302 → login |
| Graphs | `…/graphs/` | ✅ HTTP 302 → login |
| Builder | `…/build/` | ✅ HTTP 302 → login |
| Git | `…/git/` | ✅ HTTP 200 |
| OTA channel | `/v0/ota/channel/stable.json` | ⚠️ HTTP **404** (FE proxy OK; artifact or BE `location` missing) |
| Crash upload | `…/crashes/api/upload.php` GET | ✅ HTTP **400** (expects POST JSON) |
| Heartbeat | `…/crashes/api/heartbeat.php` GET | ✅ HTTP **405** (expects POST) |
| Graph upload | `…/graphs/api/graph_upload.php` GET | ✅ HTTP **400** (expects POST) |
| Two-factor | `…/crashes/two-factor` | ✅ HTTP 302 → login (no pending session) |
**Alerts — may need follow-up patch**
1. **OTA:** Public returns **404**; BE direct `curl -H 'Host: apps.f0xx.org' http://artc0…/v0/ota/channel/stable.json` returned HTML lander redirect (not JSON). Confirm BE `listen 80` has `location ^~ /v0/ota/` **before** catch-all, and publish `stable.json` under `ota-artifacts/v0/ota/channel/`.
2. **2FA:** Fixed in repo — `completeTotpLogin()` no longer clears pending session on wrong code; login redirects to `/two-factor` when pending. **Sync BE** (`Auth.php`, `index.php`, `two_factor_challenge.php`, `app.css`).
---
## Hub landing
**Web:** [https://apps.f0xx.org/app/androidcast_project/](https://apps.f0xx.org/app/androidcast_project/)
Marketing multipage shell; left nav links to Console, Git, Issues, Tickets, Graphs, Remote access, Builder. Shared session cookie `ac_crash_sess` path `/app/androidcast_project`.
| Kind | Path | Notes |
|------|------|--------|
| Static/PHP | `index.php` | Hub landing |
| CSS/JS | `hub.css`, `hub-landing.css`, `assets/hub-*.js` | Theme + i18n |
| Crashes assets | `…/crashes/assets/css/app.css` | Shared console theme |
No device ingest on hub root.
---
## Console home
**Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=home](https://apps.f0xx.org/app/androidcast_project/crashes/?view=home) (auth required)
Landing cards: Issues, New issue, Tickets, New ticket, Graphs; quick links to all services.
---
## Issues (crash reports)
**Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=reports](https://apps.f0xx.org/app/androidcast_project/crashes/?view=reports)
Triage anonymous crash JSON; grouped/by-time views; tag editor; tree expand.
### Related endpoints
| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `POST` | `…/crashes/api/upload.php` | None | Device crash ingest (JSON or gzip JSON) |
| `GET` | `…/crashes/api/reports.php` | Session | List/filter reports (console JS) |
| `POST` | `…/crashes/api/report_tags.php` | Session | Update report tags |
| `POST` | `…/crashes/api/report_viewed.php` | Session | Mark report viewed |
| `GET` | `…/crashes/api/tag_catalog.php` | Session | Tag suggestions |
| `GET` | `…/crashes/api/diag.php` | Optional | Diagnostics (may 403 at edge) |
### Upload via curl
```bash
BASE=https://apps.f0xx.org/app/androidcast_project/crashes
SAMPLE=examples/crash_reporter/sample_crash_java.json
RID="manual-$(date +%s)"
jq --arg rid "$RID" '.report_id = $rid' "$SAMPLE" | \
curl -sS -X POST "$BASE/api/upload.php" \
-H 'Content-Type: application/json' --data-binary @-
```
Gzip upload smoke: `examples/crash_reporter/backend/scripts/test_gzip_upload.sh`
### Bulk / synthetic issues
- Optional payload fields: `ingest.source`, `ingest.client_id`, `ingest.run_id` — see [OPEN_API.md](OPEN_API.md).
- Loop `upload.php` with varied `report_id` / fingerprints for load tests.
- Tag harness traffic `synthetic` via console or `report_tags.php`.
---
## Tickets
**Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=tickets](https://apps.f0xx.org/app/androidcast_project/crashes/?view=tickets)
Roadmap / QA tasks; workflow tags (`ticket` vs `issue` meta).
### Related endpoints
| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `GET` | `…/crashes/api/tickets.php` | Session | List tickets |
| `POST` | `…/crashes/api/ticket_create.php` | Session | Create ticket/issue |
| `POST` | `…/crashes/api/ticket_update.php` | Session | Update fields/tags |
| `POST` | `…/crashes/api/ticket_tags.php` | Session | Tag catalog ops |
| `POST` | `…/crashes/api/ticket_comments.php` | Session | Comments |
| `POST` | `…/crashes/api/ticket_upload.php` | Session | Attachment upload |
| `GET` | `…/crashes/api/ticket_attachments.php` | Session | List attachments |
| `GET` | `…/crashes/api/ticket_attachment.php` | Session | Download attachment |
### Create via UI
- Issues view: **New issue**`ticket_create.js``ticket_create.php` with `issue` meta tag.
- Tickets view / home: **New ticket** → same API with `ticket` meta tag.
### Create via curl (session cookie)
```bash
BASE=https://apps.f0xx.org/app/androidcast_project/crashes
JAR=/tmp/ac-cookie.txt
curl -sS -c "$JAR" -b "$JAR" -X POST -d 'username=admin&password=admin' "$BASE/login" -o /dev/null
curl -sS -b "$JAR" -X POST -H 'Content-Type: application/json' \
-d '{"title":"API test","brief":"from curl","body":"","tags":[{"id":"ticket","label":"ticket","bg":"#6366f1"},{"id":"open","label":"open","bg":"#22c55e"}]}' \
"$BASE/api/ticket_create.php"
```
---
## Graphs
**Web:** [https://apps.f0xx.org/app/androidcast_project/graphs/](https://apps.f0xx.org/app/androidcast_project/graphs/)
Session metrics dashboards (user / slug / platform scopes); drag-reorder bricks; empty-session notice when no `graph_upload` data.
### Related endpoints
| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `GET` | `…/graphs/api/graphs.php?days=14` | Session | Aggregated metrics JSON |
| `POST` | `…/graphs/api/graph_upload.php` | Device/session | Ingest `graph_session` NDJSON/JSON |
Graphs PHP routes through crashes `public/index.php` (`?view=graphs`).
### Fake session data (dev / BE)
```bash
# Generate NDJSON
php examples/crash_reporter/backend/scripts/generate_fake_graph_sessions.php \
--devices=100 --per-device=6 --out=/tmp/sessions.ndjson
# SQL import on BE (when HTTP upload times out)
php examples/crash_reporter/backend/scripts/ndjson_to_graph_sql.php /tmp/sessions.ndjson | \
ssh alpine-be 'mariadb androidcast_crashes'
```
API smoke: `examples/crash_reporter/backend/scripts/test_graphs_api.sh`
---
## Remote access
**Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=remote_access](https://apps.f0xx.org/app/androidcast_project/crashes/?view=remote_access) (RBAC: `remote_access_view`)
Operator console: whitelist devices, open WireGuard or RSSH sessions.
### Related endpoints
| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `POST` | `…/crashes/api/remote_access.php?action=…` | Session | `whitelist`, `open_session`, `dashboard`, … |
| `POST` | `…/crashes/api/heartbeat.php` | None | Device RA poll (`heartbeat.type=ra`) |
Device flow: app polls `heartbeat.php` → operator uses console → next poll returns `connect` payload.
Smoke: `CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes examples/crash_reporter/backend/scripts/test_remote_access_api.sh`
Prod checklist: `examples/crash_reporter/backend/scripts/verify_remote_access_prod.sh`
---
## RBAC / access control
**Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=rbac](https://apps.f0xx.org/app/androidcast_project/crashes/?view=rbac) (admin)
### Related endpoints
| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `GET/POST` | `…/crashes/api/rbac.php` | Session | Roles, memberships, lockouts |
| `GET/POST` | `…/crashes/api/users.php` | Session | User admin |
Smoke: `examples/crash_reporter/backend/scripts/test_rbac_api.sh`
---
## APK builder
**Web:** [https://apps.f0xx.org/app/androidcast_project/build/](https://apps.f0xx.org/app/androidcast_project/build/)
Docker CI: trigger builds, stream logs, OTA artifact option.
### Related endpoints
| Method | Endpoint | Auth | Purpose |
|--------|----------|------|---------|
| `GET` | `…/build/api/health.php` | Session | Docker/socket health |
| `GET` | `…/build/api/builds.php` | Session | Build history |
| `POST` | `…/build/api/build_trigger.php` | Session | Start build |
| `GET` | `…/build/api/build_log.php?id=` | Session | Log tail |
| `POST` | `…/build/api/build_actions.php` | Session | Cancel / rerun |
Shared login with crashes (`ac_crash_sess`). See [BUILD_DEPLOY.md](BUILD_DEPLOY.md).
---
## Git (Gitea)
**Web:** [https://apps.f0xx.org/app/androidcast_project/git/](https://apps.f0xx.org/app/androidcast_project/git/)
FE proxies to `http://artc0.intra.raptor.org:3000/` (Gitea). Canonical git remote: `git://f0xx.org/android_cast`.
No Android Cast PHP on this path.
---
## OTA updates
**Web channel:** [https://apps.f0xx.org/v0/ota/channel/stable.json](https://apps.f0xx.org/v0/ota/channel/stable.json)
Device setting: `ota.base_url` in `examples/settings.json``https://apps.f0xx.org/v0/ota/channel/stable.json`.
### Layout
See [OTA.md](OTA.md): `v0/ota/channel/{stable,staging,…}.json` → manifest → `.otapkg` / `.otabundle.zip`.
### Publish (operator)
```bash
./scripts/generate-ota-v0.sh app/build/outputs/apk/release/app-release.apk \
https://apps.f0xx.org ./out/ota staging
OTA_DEPLOY_TARGET=… ./scripts/deploy-ota-staging.sh
```
BE mount: `…/androidcast_project/ota-artifacts/v0/``prepare-be-ota-mount.sh`.
**Status 2026-06-10:** FE `/v0/ota/` → BE :80 configured; channel file **not yet served** (404 / lander) — publish artifacts + verify BE nginx `location ^~ /v0/ota/`.
---
## Heartbeat (device)
**Web:** none (app service only)
**Endpoint:** `POST https://apps.f0xx.org/app/androidcast_project/crashes/api/heartbeat.php`
JSON body with `heartbeat` object: session stats, remote-access poll (`type: ra`), etc.
```bash
curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"heartbeat":{"type":"ping","device_id":"test-device"}}' \
https://apps.f0xx.org/app/androidcast_project/crashes/api/heartbeat.php
```
---
## Auth & account security
| Page | URL |
|------|-----|
| Login | […/crashes/login](https://apps.f0xx.org/app/androidcast_project/crashes/login) |
| Register | […/crashes/register](https://apps.f0xx.org/app/androidcast_project/crashes/register) |
| Two-factor challenge | […/crashes/two-factor](https://apps.f0xx.org/app/androidcast_project/crashes/two-factor) |
| Account security (TOTP enroll) | […/crashes/account-security](https://apps.f0xx.org/app/androidcast_project/crashes/account-security) |
| Email verify | `…/crashes/verify-email?token=` |
| Method | Endpoint | Purpose |
|--------|----------|---------|
| `POST` | `…/crashes/api/auth_register.php` | Registration API |
TOTP verify script: `examples/crash_reporter/backend/scripts/verify_auth_totp.php`
Flow doc: [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md)
---
## URL shortener
**Web:** [https://s.f0xx.org](https://s.f0xx.org) · **Admin:** […/crashes/?view=short_links](https://apps.f0xx.org/app/androidcast_project/crashes/?view=short_links) (RBAC: company owner/admin)
| Kind | Path | Notes |
|------|------|--------|
| Health | `GET /api/v1/health` | JSON `status: ok`, DB ping |
| Shorten | `GET/POST /api/v1/shorten` | Bearer token required |
| QR | `GET /api/v1/qr/{slug}.png` | PNG (`libqrencode-tools` on BE) |
| Redirect | `GET /{slug}` | `302` → origin; `410` if expired |
| Admin UI | `…/crashes/?view=short_links` | Mint/revoke bearers, list links (session RBAC) |
Spec: [specs/20100611_3_url_shortener.md](specs/20100611_3_url_shortener.md) · code: [backend/url-shortener/](../backend/url-shortener/)
```bash
curl -fsS https://s.f0xx.org/api/v1/health
# Bearer: hub Short links → Mint, or php backend/url-shortener/scripts/seed_bearer.php (CLI)
export BEARER=BASE=https://s.f0xx.org backend/url-shortener/scripts/demo_curl.sh
CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./examples/crash_reporter/backend/scripts/test_short_links_api.sh
```
---
## Non-HTTP services
| Service | Access | Used by |
|---------|--------|---------|
| **WireGuard** | `ra.apps.f0xx.org:45340/udp` | Remote access lab mode |
| **RSSH bastion** | `ra.apps.f0xx.org:443/tcp` | Reverse SSH alpha mode |
| **Cast LAN** | UDP/TCP `:41235`, discovery `:41234` | Screen mirror (no BE) |
| **MQTT OTA** (optional) | `mqtt://host:port/v0/ota/…` | Retained topic payloads |
---
## Validation scripts
| Script | Purpose |
|--------|---------|
| `validate_be_services.sh` | Public HTTP smoke (this docs snapshot) |
| `test_gzip_upload.sh` | Crash upload plain + gzip |
| `test_graphs_api.sh` | Graphs JSON API |
| `test_remote_access_api.sh` | RA whitelist + session |
| `test_rbac_api.sh` | RBAC API |
| `test_rssh_api.sh` | RSSH mode API |
| `test_tags_api.sh` | Report tag API |
| `verify_remote_access_prod.sh` | Prod RA checklist |
FE nginx template: `examples/crash_reporter/backend/nginx/fe/apps.conf`
Regenerate FE patch: `scripts/generate-fe-nginx-patch.sh`

601
20260610_SERVICES.pdf Normal file
View File

@@ -0,0 +1,601 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 33 0 R /F4 34 0 R /F5 35 0 R /F6 36 0 R
/F7 37 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 762.0394 115.501 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 751.0394 88.37304 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 740.0394 97.26104 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 729.0394 123.037 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 718.6019 120.135 728.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 708.6019 109.2825 718.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 698.6019 133.4625 708.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 688.0394 71.02104 699.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 677.6019 120.135 687.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 667.6019 103.4475 677.6019 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 657.6019 164.295 667.6019 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 647.0394 71.91704 658.0394 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 636.6019 120.135 646.6019 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 626.6019 155.9775 636.6019 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 616.0394 100.813 627.0394 ] /Subtype /Link /Type /Annot
>>
endobj
19 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 605.6019 120.135 615.6019 ] /Subtype /Link /Type /Annot
>>
endobj
20 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 595.0394 125.709 606.0394 ] /Subtype /Link /Type /Annot
>>
endobj
21 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 584.6019 120.135 594.6019 ] /Subtype /Link /Type /Annot
>>
endobj
22 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 574.0394 87.92504 585.0394 ] /Subtype /Link /Type /Annot
>>
endobj
23 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 563.6019 120.135 573.6019 ] /Subtype /Link /Type /Annot
>>
endobj
24 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 553.0394 82.58104 564.0394 ] /Subtype /Link /Type /Annot
>>
endobj
25 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 542.0394 92.82104 553.0394 ] /Subtype /Link /Type /Annot
>>
endobj
26 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 531.6019 82.20004 541.6019 ] /Subtype /Link /Type /Annot
>>
endobj
27 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 521.6019 119.2875 531.6019 ] /Subtype /Link /Type /Annot
>>
endobj
28 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 511.0394 111.485 522.0394 ] /Subtype /Link /Type /Annot
>>
endobj
29 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 499.4394 129.725 509.0394 ] /Subtype /Link /Type /Annot
>>
endobj
30 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 489.0394 97.70104 500.0394 ] /Subtype /Link /Type /Annot
>>
endobj
31 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 478.0394 115.469 489.0394 ] /Subtype /Link /Type /Annot
>>
endobj
32 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 467.0394 106.149 478.0394 ] /Subtype /Link /Type /Annot
>>
endobj
33 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
34 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
35 0 obj
<<
/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
36 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
37 0 obj
<<
/BaseFont /Symbol /Name /F7 /Subtype /Type1 /Type /Font
>>
endobj
38 0 obj
<<
/Contents 79 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
39 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R
24 0 R 25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R ] /Contents 80 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
40 0 obj
<<
/Contents 81 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
41 0 obj
<<
/Contents 82 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
42 0 obj
<<
/Contents 83 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
43 0 obj
<<
/Contents 84 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
44 0 obj
<<
/Contents 85 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
45 0 obj
<<
/Contents 86 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 78 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
46 0 obj
<<
/Outlines 48 0 R /PageMode /UseNone /Pages 78 0 R /Type /Catalog
>>
endobj
47 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 exposed services catalog) /Trapped /False
>>
endobj
48 0 obj
<<
/Count 36 /First 49 0 R /Last 77 0 R /Type /Outlines
>>
endobj
49 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 50 0 R /Parent 48 0 R /Title (Validation snapshot)
>>
endobj
50 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 51 0 R /Parent 48 0 R /Prev 49 0 R /Title (Hub landing)
>>
endobj
51 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 52 0 R /Parent 48 0 R /Prev 50 0 R /Title (Console home)
>>
endobj
52 0 obj
<<
/Count 3 /Dest [ 38 0 R /Fit ] /First 53 0 R /Last 55 0 R /Next 56 0 R /Parent 48 0 R
/Prev 51 0 R /Title (Issues \(crash reports\))
>>
endobj
53 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 54 0 R /Parent 52 0 R /Title (Related endpoints)
>>
endobj
54 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 55 0 R /Parent 52 0 R /Prev 53 0 R /Title (Upload via curl)
>>
endobj
55 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 52 0 R /Prev 54 0 R /Title (Bulk / synthetic issues)
>>
endobj
56 0 obj
<<
/Count 3 /Dest [ 38 0 R /Fit ] /First 57 0 R /Last 59 0 R /Next 60 0 R /Parent 48 0 R
/Prev 52 0 R /Title (Tickets)
>>
endobj
57 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 58 0 R /Parent 56 0 R /Title (Related endpoints)
>>
endobj
58 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 59 0 R /Parent 56 0 R /Prev 57 0 R /Title (Create via UI)
>>
endobj
59 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 56 0 R /Prev 58 0 R /Title (Create via curl \(session cookie\))
>>
endobj
60 0 obj
<<
/Count 2 /Dest [ 38 0 R /Fit ] /First 61 0 R /Last 62 0 R /Next 63 0 R /Parent 48 0 R
/Prev 56 0 R /Title (Graphs)
>>
endobj
61 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 62 0 R /Parent 60 0 R /Title (Related endpoints)
>>
endobj
62 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 60 0 R /Prev 61 0 R /Title (Fake session data \(dev / BE\))
>>
endobj
63 0 obj
<<
/Count 1 /Dest [ 38 0 R /Fit ] /First 64 0 R /Last 64 0 R /Next 65 0 R /Parent 48 0 R
/Prev 60 0 R /Title (Remote access)
>>
endobj
64 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 63 0 R /Title (Related endpoints)
>>
endobj
65 0 obj
<<
/Count 1 /Dest [ 38 0 R /Fit ] /First 66 0 R /Last 66 0 R /Next 67 0 R /Parent 48 0 R
/Prev 63 0 R /Title (RBAC / access control)
>>
endobj
66 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 65 0 R /Title (Related endpoints)
>>
endobj
67 0 obj
<<
/Count 1 /Dest [ 38 0 R /Fit ] /First 68 0 R /Last 68 0 R /Next 69 0 R /Parent 48 0 R
/Prev 65 0 R /Title (APK builder)
>>
endobj
68 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 67 0 R /Title (Related endpoints)
>>
endobj
69 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 70 0 R /Parent 48 0 R /Prev 67 0 R /Title (Git \(Gitea\))
>>
endobj
70 0 obj
<<
/Count 2 /Dest [ 38 0 R /Fit ] /First 71 0 R /Last 72 0 R /Next 73 0 R /Parent 48 0 R
/Prev 69 0 R /Title (OTA updates)
>>
endobj
71 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 72 0 R /Parent 70 0 R /Title (Layout)
>>
endobj
72 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 70 0 R /Prev 71 0 R /Title (Publish \(operator\))
>>
endobj
73 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 74 0 R /Parent 48 0 R /Prev 70 0 R /Title (Heartbeat \(device\))
>>
endobj
74 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 75 0 R /Parent 48 0 R /Prev 73 0 R /Title (Auth & account security)
>>
endobj
75 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 76 0 R /Parent 48 0 R /Prev 74 0 R /Title (URL shortener)
>>
endobj
76 0 obj
<<
/Dest [ 38 0 R /Fit ] /Next 77 0 R /Parent 48 0 R /Prev 75 0 R /Title (Non-HTTP services)
>>
endobj
77 0 obj
<<
/Dest [ 38 0 R /Fit ] /Parent 48 0 R /Prev 76 0 R /Title (Validation scripts)
>>
endobj
78 0 obj
<<
/Count 8 /Kids [ 38 0 R 39 0 R 40 0 R 41 0 R 42 0 R 43 0 R 44 0 R 45 0 R ] /Type /Pages
>>
endobj
79 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 862
>>
stream
Gat=*9lldX&A@sBlnQaAMH6?gFOR&7NXq+DHg6;>E^Ic2$J94pI0BcF;&b.C!=`5G[nj&18I3g0=FSj6^aBY1*tXNb[fI12J@C&UDt\O4mj!N3b)oYRes]G'$B)6&664&l#Jsb8[0&fijpHe6BkBBqi'r>8=VhKC(+pFS*$AgG1WliU$fGb@#AFeTE9Cjor*i?Pq>6Uk-@j=9qf$C-=SoYD9qFP!pB'2/giOF8i(*Ya'<RA;7^SuT.YN9*(X7f<A7M=j#)3m6$b3c4TrV)iA%BZE_Yj:4qq_/(0_"ZfM(9O?9\iNVL"CA_&1obJN\3c0#:RVFJ;p=5lsLo?M7A(UBpi8DO=;,-$)rL#[H/s2H46Rh`fQ?[5ejEuoK9I,$*c\?X"N?GkBZklJJdN$CQ*gCAeP!U^hbC%QBIP>rG6"e$'i'bQAPAJRVd"!P>EG8'\+MR5Q0upoU+CsU*p5C6Ce\uZ0cMEbFau?`*=pfNcSESo8OjhfpgZcXup[D`2UYf2#Au5jaCJ1"l)'[<DO+B?#$C1aDOQIV*1Ps\q;`,)lUke=;9paPQ]ql>Ygd'FAJYY+JoB-\adl02Bus:lMc5cJ5;cYb3Vh@k&9G]:$7O1&FQ9D#iXhMXn#=>3h86hiAF?$;NkYoJgZ?3F0aXk-I*caHQAf,14O2mc`tagm1Jc)p3i>@LM7q!h5/^U^Y%i]r&L;C;0+'!^KXg*-JCB):,d#!C^iBj<88AF-J"7n(PB/Q2CQ$:EZ9u*5]hWT*JA/=U4X!dJp.d=V5l,I7\J,YC)+7r:Hd=M;n"T;MC<SNY$.P'U/oWZHcN!VeGHf08d<Bp6-!lU9G33GPh453KNiCVM#al3G>="D~>endstream
endobj
80 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 966
>>
stream
Gau1/?#Q2d'Sc)J/'c\0:)_(U%.(.=d^<8dAL["HUractj+UlQRo]L^4-6i7qQW&G-R=9FiH`b9cZ'5DG`_o0D?*-0*$,b9'T+C`E=bKW#*WNcCI!&=`_!tO,`Em@diaV?(k7=MK.s&t&j[)j/5.i*dY-a;!I[,HGEN1>$),uoKC0S@G(a;?(R4uC"f86$W3&(1i[5H/48*k\?K/<\%K'mt-]n!:P+I-E/%O_AWZ"RU)m`GnJKO'hpa>G1.UFc>II4h011/hAWf9?[YLH$$JE^8>rZ_-eQrY&k%nk0l$FFK2[Ut!H+G/r5K#Bp,jBb@1?!d2\Le;9Ri`7TreB:<Q\&8[Q9"8Oa`KtNg3L$YKr[iZ^IKb\aC(5r+mV9?H<gFm*n2L]qR'2GsA@WI.hIl*P9[=$\QsU/0/Pnd+!#MOCpuK3K]DTRH1r>Vt0eASn9Ard7%'Md)_Iq,_PtMgcrK4f-JbkXNbgupaSW=5:@k<r*#&A9:EJag@CemoqM2fBJi+7ac*6@+<AC&(_o#R<jUSBP)I;=3pP%`s]/go?O$@FoYV7rI=2#RK6<`Vke*Oop8Y&Dc&ib]!b(+TpAK"I7Q&pp*9^D08c2H]f.m5KmqkPi`jBDbN4%[1ftrGnh'U67J?cA/D3RTlVZ]H6p=^BN54JR*6,hTF9GkJd8W62W`Sqsk!ClO'oJ>.]B1h1DEUg0tH*7P*qtK8.V)9R\]X"5N]cgjZe5I/J^ZlkT(m<IZtWMCI8]46J]$Ia#IL)ilq-V3[+a=q`9/&>^Q7n1R!;B0itQ!;.W]aVfZ-T?m/c=%EJY[Q_8U:uf*$FX]KU%4MZ&$%JI@As]FUaa(*n_OT&]9:=Xi9%Ib[[n<hiehI&EN,tQ"(#"1D@=F]Sl*ac?cO*tL$@#icbY@TNjrfWi8JsYhghM:W@NMc0?NcE@8YS$a)$U0*UP+`1[q\I=V.P3r@.5+LR(%G~>endstream
endobj
81 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9$AT&MD_`PtL;Egp*cQ&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:ps&&=X(PCeL]7-s8G,Z6b]&CVFJ!om<]_Oaf@33/!:]XjY5-C*Ut>641HTb(JKc.+9~>endstream
endobj
82 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`Sh(gXlj:fa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B]'raq*pZH62`G1DsFgA+6hs6uHU'5'".RbXr2MH_7OtUjbJt$1U9V<CL~>endstream
endobj
83 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3476
>>
stream
Gb!#^>Beg]%Xua-^tX)p#u+:ko^(8[G1pOiBq%$KmRtjfa>.8+;']/)Rq*W7[kF\L-!;&,l#cWpW?[T.^^hOnJ5CdUO-9J1h>t8o_#0/n$A6;,-lUK@E=T/g8)K>k@]$"9_pYLgVJQl62&"k/qU@OAdYV+#pi_=TQ&:u%FBI=L6H/+JZ;eXS*/RE*gJIT-j#,'[l1j,X8bqWc(hL\n2,4hmc:uGkGk2i&KuI+=Uud8A:)#"m%>1/0qguWmn@Ksi.!]5$^VS+FO?<_"JW&pDpKUf]#sU#@l,?kr1l[[Iq)kEu6Q[@?(p@/.8bTfA-K+aMLBo,j8X0=t.&!ZDohR89"J&]u'/ZF%`W-BF+hF];r=]Tt(q*i`9[JW-0K(UN-'Xh;^][7DRglT0<]-R^3f5+4#J"Jj_m8HpCm<Mq6msu#S1*'DWJ"^p("_6_O0q\^!NX?4o/8RFRid-!mNS8NF#S9*c#q7*0LeF#0-0eQ#HN.0`a4lVoC%B1?SD4t_W1*Vb7g%TH6l&::n^#-:hg%FC,&^iJZ-"C9accV1KtOIjsUeEkmWnn';dGc:)"FP%QD^qVYDV'W-6c4OqH3:rE6jgP=I(^f*>-LFHj(B*^JB-C+l7uFjTN1CP6RaILWmE@#Y37M-`V0F4`%Q3ecsHCDEDMOrHu,YV#G^`a)HG"bG@-%<l`;9i04H6;;2:6M\f;gd.#_nXB4@8sS*5Bq'l3fUWF&noYRS%hAZ[/.&IOSa4sP.05bL?*-k'S2YL:=N-uCEh+JhNoKOYGLO)*G*>u^Iq-uR#DGZ`//6AFM[MXrgZUQbbKlJ'pq7;8Ose6%bU'KXimGR4MGZP2`or[6n=.&tH2LXab4EJdMPqJbDu&AG$\fe%Cb"La@jftU&kGZXhFDm_d?al[ZL!&ad$?g0kEfE!?h&@\BIVC,3%kG`$(*)5rYM<!7uie7g$:jt.YOY>G$)$tj)6<gr1i&LThYdWj5L6P)^bp$57X+rq<"CDS^,.&R27F:8a7jT:;/(6JBge)o@0ZojL<>%gMDQ#D&`e2/4PMI@AZo(3Hk[/1X%-7OF;A,O+07s-Vh>oa%[_90i'?shaR?ik[p5PS'7Fi,[R^hqK5&A;R+LG.1+bM&"V7\i_'(EA.'Nf(6HcSdYYV^cAG#ZW4g!UY8?61*0jo*PnPuGpGL&7+q0_L\G1<ZJ9R>%57FRPJ=qsuGq?KNiH\&T:XI(XjE@ag7YUVglHk=:0rTrniF<YH88jHMr^:<3!KGGpp?2)^GK6X!5At`SDiC[L=PSZZ_J+`;6-iuEA[sJBEDAU]s$$m@:^',j&J%oe:dDnf^^,Qf7D-OqY=HN?VPYljrqu]fM!/ebeTpS+eH5:)_dt_olN=Na26BTS:$EfHl9t:gI&r(l\NY\]eW?L`L@DGdU.ns9-_4i"8+<Jt0X^`W5*Z4E!,>t%V61-\PdrDe;rU\_79=QZ'iJ*D4+pRJj9@!OX+t`n;dTQ'7n+EGqeN4CAeDYBctgXHWD`*\X@-4B/8$V`*ogSA@LNik*[0Hh"ZN.DL\E1N#U1X=qb)r2&JVR7Z0f$o$Mo*L;MFK8;Dof[d*l`^jBn0@pm@u63(VbAe0DF"S*-Mp^[bk+AaL,3]8_4u3J1\a_B_9u7d9mg&/BUjd#V+lAkln_C9>UF$\o"Z/`*+dYg%PC^cW,4$YdQY:5cW7^oK0!n_uVRD+dR[92Zg]7GDW::9lgfLc=;Ms+OA/p1l8$QM`(gH$b])1k+HZr\:(u&.-i@[VW2f^gJ,2P1n_2.*tO'1ad-2>;RVn9/PDeZT-a%_tM+Dm#A^3\$n<Q""Fr>Z)H%4)l.=?Td]!"HkE7'\a\BUZbfGgaIk'X_dJK#GC9nLP."j*r-'7C$uj*!IpO)qV6)#)L2FBd_l^rFI`&3[C0!%B>O:H]i0HArmG4gGMpI;u\nT):q8D=%4<?!M6"V=`I@\Q0NJdIYNj-U;L\m:F8^a&^HUl(Ld/DV(s1V&\P\S(Rl_LlS'#`?jN$SVS9L?Aels1j@[__s$0DE\soA4J5<Hol61\q4oh,*R2pm.hpW:.0Dl:'!e",:6uO64Z);@&f]i-W:WB$IOM9("FHp!R@F8!7\6EXW?%72#Ac?@=qjchoCh3B41"qrATO@Msa?NJ!^qg-'<jhKjS,h)OhFI_R3V[^!_KD9o!M5pu&gb54-Jctbn,gqO`Lkgbc,*HL8;O'ZB\p%CqS;f?(C5Yj*UpeRf7Ja/Ju+&/X0!=c8=#Htm*Hs*86;q%Mb'T[3WIW9NcNfk+L7Xij/dt:Ik(L>#s@`#'</o4g"cJMnKR(5/5K6sTpB"MlL.VAG6K@<SQQ$#gin2s9oNbg?)H\MQa7/f1aQ,SL8EVKu*`>U4l:X7;0bI2d)Is-iY!/7)klR2T@c.-IGED]s_67S(D;C7AE]9`BRP>iT/c0J-"CLkfJf*:`Db#@t&SuNluN`M[]ZdE6l2]5U)L7kdM-6lN'6tIShdkVe@>05u`&6<$m\mX1(k".%3A)'QcLLDYEm&:n8b*LSM.B=Xe/LF."p55dP2IWN^Zl)_1lu#9W,MFYlf\aGjr;LV$A;ops%-R/Bs0fnb@Pq!6WtrNf<^UYs.aN1&M<SQ`8kuE@mfU<0:]+LVpr%Ajp/"5.cN0kaDg*o\-4<ZC5Y4Lafu27fr^JfCS(31KL6/SAn]X7Q]R;;m_GaBid`qFI)atpZK[_KP5rI$s(9o6-!@J/*a1H;[5Rs52I6<:-*,["8RP,aY;@cK$aG$WiSMagKMch8'*HM_g+i<onW1@+?>."]fXV+rEI*lWXj1a_""\bA[cjL&N.0FuSkGH?Yg9Jq`ZrT7nB*6jYW,I`@n<rtNFEMX]L$6f;rp6dF7HQeI&]h`Mkp@B@4n)RX:o/r/IU/=60C`lY:u`\/70TPQ-IVB;UUG=OiJVOmKh;.aPL"([3r/2Nga5U+?om8O]BNb#8F]WI=^fZ!!E;[r#D;mNOD,=+4V)s](Kgl+=#8ec7?bRa5@1ahg$#!7-.>3-J8sdbZ;T"jZ?t:S%N[Y'8F^K8qfuI\>X?h1T6j-j^[_4YX6!up&7Ig9(r!U%</<9>_c(Hjk_%,aV4G`,j;JQclHl$BksoJgLZrG4kqA04QS2;?p!n0WfM(`.P1.Hjl"#r@6p+H5K1e6^WHB5YOm/Z1?+AC>=:O^`K-cFCdp:ODc\S)Xd$Ch?-[#?+4`Q*JYO+l]j++EI7b5\)/IiQW0XNng(NuTRn\fG[.Un+rKI$&N>9>'jMUUJW\-%10l/,A'+@OR^EF=T,Fq)>!-cIoTc(E_B_9?M(Kdk^-;,-Ckm&1L/GtFc"A(O^h2nCL*>uua5kc-=\>nqZQ?JR`V^85MR=_79.nmNn)@s2_s@_@$>NN8M6lSC&APn2u$D["?ALhc1?g>@q,FqZ38TjP+X'qM0'U.Je>1l/-iFu#`h\I7,t"<84^7-ul2\nVks_UOh76rt'QP@^W/*39>d3;O`^a&JT~>endstream
endobj
84 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2971
>>
stream
Gb!;fD0+IA&cV3*Z(pGrja&*W^+2.6X-<=!cK#TcekJ5)FYp<.8[Kq0+XfG?2?1>tP+j@nPX*3*YN&2B&;ttQZWQPq,bX*<ao7nb!FCdOrL!Z5@5J'q.OL,2_:e&9o+<E"9hV`s9/FDhLnYbS'?;,AGEK+4PUpO;1@KfAoLR0HPPGf2,c)'9-l.c-.)Q0l4O1_W')F7W0@*B/Bc?RZW_2<=.l6S9(F;]9U8?;b9-W8_;P$YC,e@U:<1PA#0,T0KUMC]p&,j]#?SC9,SZjbh)$c,`rKDoR;3J4Qjk8rkrq;f;;WX?;@6+25q<O7$P(kJ*%%`IU*_dOMjZ>YThCD1#5JpkoP;so&#JFb0U'J_2/ms:7:>d#`*+N'Q8e:n1j)I(!@(Jr3)":08r#ICR>`OnDfSp[Cl7*8fl7)6ImVNI9)#OHUYG*^^@6,7]a6WufPQ^uMVLB5Z_MVi&$oOo7.mKiO4oBH,Aq86VC]r(+'uqJX!3_g+E#$&/pq.'1Smt;JnbXs1(KV`iL)g&.roM1^>6K<kh4VoY63;=#OS7F*\t[Xm(4%I&-@VJ*2._U=*8+&D$0$jpe!bDLa)05[/Tc2Xc#jlc?F\D]jXRb@(&33E/m.C7_<63REO-Z`26ECs/ZH7Ip.sA/3"C=l"f*u*R,2J>2tL21nHZD`LNAt%Kh&1Sb)3obA,h<"ib@>G"/2>ZdO2*b+Yepuh#Ye<I(2._5<6D:=-fG`Ir=VHer5F_gq8Q>\?>+/dF@j%o@!lud_od)I8II^lXNnaDR;Q`'0f/N?p'&M"Q=u<`^jb;XU[%ZF'-d7A=Q#Ud+Jj+YGQd5R!&(?I1"FOjAE\m<H#<,V/tKI?W,J.b0Xf4(q+.]qeKu-&7N/QYEtbm="PNi\E6_H'kSDR@Q'*g0YMd\HN2!f?IWWST`%@Yl.0Kk-`OK$ACrXjqgIH;]&/[jka<Mg+f5*krs3gGE\e)t3tP6*\6m-6fB\!+n-?"!7(#)/P,39u>f%A[U"*W%rW)hP"h;*hJ71X%`(Fe[B=jO=&.>fKI3VJ6>huUm:lM*M3A)_?V%69E;N4M3E\YbAL5J`J)[=62KYd#%A1@N/1q(k$ULXd-W;j"j=_;4Z\oSNt6Z8^Pa-gUf3P4\[N,<np?aJ.Y4acol.ndTZ&uk&S7e?p'Uk&LSF"j)i6p4<X$P('*62["EMo][V'g`nZ7;"W6oi^70XitoT=)?,F8FP#-g7GPdVPBLsVU,Q%^/u\$`*&(DFhCi/4#!mT$'Ah"jI<K<"9@9RNciSr_lH=_Mr7cV<4bGY&kK#(06)L72p)(0>e9NUkOTD]m1-)&Blomm;)1\d^L?P>S8ZDf!+<(MBN^4g9G.jbb7;Ic47?6a"4hM'7I"L>COno%:BV%X_T.7>'mg2$,%]GYY]T(,>F^u2H%D`J>\43j6^d._!%:5%d4hP'd$I*fb,W>Zl-<I3*`JNU+IYY3mfLCHSd#VXID!')/XIb#=S5;S,Z9pK#"WshYL!K^.m#Z)ZCV7,F6BFP0iS<RM+"1>hhcuM?&.CcSb6]cdcj(`;4M=e!6"*6Vb<jcqW$u,'WihSaN]lQ*)a3c#"j*j=cu3HA9K;'SMtXN9V)k0aW:<d2tYCs?Uo<KG$;=?V*W=0r-*Rb\@Q0R"R<aohAds[gm*$[E`6P,AcS0#f2frcCTTp*+_b@1c2jqNfIXY=fWTI,Zg>OI^Y4CdZAWPB+raZe^K5`c[I1gK^!_J6FbdN(J[nWChib1H`=l_CXsnH@)]O-HI@0nkkV!mEI`"!`n3qi[=SI;]3sma"VV(UhBK[1h>e;9FHrs?Yn)A0T\+BR];MJ[0Ue-mM&aXddg^iD+Y<7"^:)!Nnfc;mjS/#lgDm)UfP.0dpd^#p&*`o>N0VcDF:G&:-bZsk/:`M\l*#'="h@+jg1\&,Dc4C!k(=A:&!n2PfH,.(4\n#c+ES`Yubu2-sLuCP?El]jHa*jP7FI5PFNR,VjpFDoK:8u#NiSO:ql),1P>pAf\OB\ES?dd2Z=C.FkU%\tPHSDrZT8JQVJHs4GcB8a44/(NZ&-!VJcL7LGbF?Q<]2l"Or"'*&TR,s*%9GjB+R5X#M*'@lCKmL[n%I4H"4hhH'Ab";Oa97aXqIAOnC9(HQ5PM<?0'i%.sqt@AFIK%Idd(R^;uiS?O-4\"k=DjF):fs2Jd_0H=).MkbFN!($_N]e*?eZZ<6S]5JPc+X^"1W#SFa%GA;4H$hjs:[mkl'`FWrMa.(<VQ<"W?]Jt5$d!-^R"f!$FU,@l(Dd4:,1Gr/=4a!fB.e^p*,.A5Z..O2T4BuH!&PYB-8L2lA\C;!hF8UjsRYk)UI#9Pna3T5<0;7u'_Jm12+hHl:>W&ctSDFe-&LNI`4W)s+R!>dS`QZCpV*_1UQhWC'+u8F%Bf#c<DO$#\'t<@0a_JICqVU2+ln6i'jW^N_BDHn=m_%Ya;7cAtem"A'=N+X#)A10a7hN[h.ad\5H[\<*s0H27F?WLQ4BpY-i:cm8_tus+q;i++%P7JBW(aW1OsH*H&#dV'/OjQAb<hu5m_Qg6VW2J_h=Ku;U#s%].^g=cg&(cV^e\eJ;>uNig+(u38+)&'Uh`.:W3CQ;$G9p%>_&0sc1Q;.h-,7@"18sUp"'%ukcWnZ"Pl<'b<-RnI^stS>%%UCklMsh$Ri=U3qVt-$F=3l%mBW_WQ'>T]h88]c$Y_sD2>%&6m8#PB&<N_iFdJS@%H22/-Y^aWW5tnQY/(MPYEpdUe<.J(bMQuc5+KM(--"N9/%PbAn]"kCV=:&^K-7$]IX\'m+4(1`@NIpS7'o&5[a0bXQ0q=eSeqR/mYj-LJQ?3XkDk[;lWD[r>Snb(I3W*&)#lu_=<[854u(ORTC<.GJ(+;_=_N!7'CH=R);#u4ChlVK'kA-CsDm,JieV7LTRGUO<Hfj9i;rEpEgjE^RNFn;]?rUOh+MnZj]GJ/nV\hkTIgg/FM9VqsKaoE:b6IIMPf~>endstream
endobj
85 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2836
>>
stream
Gb!#^lYkfA'*"Q<JmT!YMhD_BGCX(`lt.>VY+*E`M?<ns[*@7WaHB0CH/.BXpDZ`1/Ba2Ng"l?5,S!O`AiMICAgi7)p`]IjaTA1%+Yl4o6mrs;,%>%$%<'Y=nDoWD;QdAIm.u`V,L>tcBa$CW#Li/bWFulqL'pDKHJ&!`oC6<SCEbK#VtYS$8<2>5_R&S62#N)_5:S;#_G;*uQXZk`'20fT`)N?41OkV?msKMFZV^PAk>'E0au$^a%$mb.IPbD=E?]"0A?^;.P&NHjR2i%Iq8Of1QjB#&H"O^9%f+;EaCQG.ZD33(qs1<W(T?BpZK<,b(6-gb%1sY1d+GcoCC4D8W-/+TiKYT^^SY6iR0YW@i[fr]"aZSlP"/q-d?HkN+P7Q(;BDUj6btu=R@#1#I6SQr7MuIP_IH\87d>1XJR=kc?Pt1e?Ym;>`$Cs!*`(61N+KIK\;CZ6OR(KN@FM-?@j,XlY3G`fK>mt^-mck%R,`af!N(O]kaYaGE:ioMOAnI+'&0Q@!Il\ei1-<sR\/\/Ycd)]MJaJ%Zu?.7\)PFp[_("_Kfr'16#OnrE]HpmS_>7*_`m;I!6R6sl8r]YMdk:[&!?^'.Hd7D/_e1O$'spRpMMG.,i]?^p#f=7FR.hi1cX2.*5;u<qmB4D7V\&!)h]<3ku@J4V4gaZ*%QcA=bbCNqMn8q"QcJ+AA>OZp.4JC%#U_8D_Tn9nUi%8b:Tk!]h1Ff(+dnp=YjurKJ^hdP18'c,[7IDK&4KRA?IMi4-a>*Bto]6*:ElKc@d0Y-?33L^c^FkE31^/)#T^GDT5MB[J(Q.1l_GW5@)Y7'hXZ<W]'Bdg9C6rVt)G$Q\%)rfQosb"M`hhYYRF:+fP3^s60[3R$UhH?``iUN$:+"-d;G]k/Q$8H<kAV3uL"=C(i:)9u)T^Rt+?j5laX@:8/"pK8LeJ6"*;A>n*fJbI.e*W9J%n:B0$j3kMPI4caj1L@#?g#Ld_05%I*NaJq)hA!FW2+953J]sD<Af,VBoiu/IZa'Jk\4Aa)S>ZsiNbYeG1-00mt\iGF?b[4p61*#4m.H2r$JT_IsY:4P8hP,Zt28G>ifj3;833lc!K15@L[rCI+ql@QrS&+=bC2e6>&;mULOh\1j)F;rr-Ur]cM3(sZ@lAT65\=O?&Z.<>,R5N_GS]iacFT)1p&:pj\R0:D\eE1":$-ioU^iD::$@K7bXM=%f%9udA?*0B'*hQ^r&DF*RdXG^6.pdIZ34'C/4C3=(Wc+hoNN44%@%Lg)@@TF?%fIX4l\M6L[<on<,bN\c#$7Q?j1m-"dS,WQgC[c@,3HHHRf9bJ$2J<5@sdcTg*D-8teK*@k._%URjI8e'67d_i"D,D\icmJCUX1)]e2t400Fc:\l+Bm-G-4pgVH92@i_'7*4L==mcQ2CH&aec5+)bDj^[BLrf4)Tu664b,d8Xh`tq@kO51=2PeY;ie<L1e?7B16gYd0(tB5+>>7@TX\HTE(Y#Sn[R9Fs\fGOPe"'=i0jRK._!6R[Fo_Q-$[RH!Fu7h^_SN_8aNY2Cp8t&He%WUTWs(YKcdgjtBY4bK.2NnSfIjVdq>:tX-^W</&CYZ7lKAMhY#aopaF+VTn%W*M.DjaHrm1C,>0Nf#C]W4/#(s+s.g37ZprFcparo4DdO7Fu6Fn`@4^aUf=uCJME#17"]tc.k#66Dq5G3)`J53THgYOU?"H#]mL\LH<O2-/A.I1KXrj;>P?[oZ-@QqPND\-m-Ic^;ORq%C$!L8Y\8C$lNL).,50LgQ?7=$aFoc[2;N?'_!eNs$0p"uBX6iTSIf$hALZbuFmTCfN(*6lAp-K8ltignp0M&s1`I4BDa`>bg!Q"OUW^sY#meufK=`;ub\SDV<E^;CN\n#*N:i6^ehVB#Yqe5_"p/t?8IXVXn68`(jN,[&dUDu7;],&/$i\.$T-#"/D`,5jDbDpn)W4Q-l-!$R3\E<W;XOnD$PL0>neH=;.0=sk##>jDO"Rd`58?Z05Y\f54]li,"-7r.);$ek9$HUYQ8mjr0)P@+Vff$5J$"pCFWK)R(`S-c,F+Ie$=H3r[QQ2ME(F]/R"CJj&&X4N'j0@F5bi#H&&p>u"ckek[$mFQjHL!K&oomI72d9X"Ua/p-LXDpIY'".gj3Rd5O;k%0uS^UqCjj$2l;<(Q4FK&cb[6NLji:\(@H"F:(70q<k@>15=(s3<(Y[Aoq'd`O,AX]LDggHs`N$+0,EB%A=Jpj<OU1^N,!`QmCXbf@P,Gg,!BcYNnB$FRDX8QB<FB*8baYSeA52W%Z7M5\>8f8b!%K>WH$,#]o%.Tm'5u5g,9nm4?KrSM?`B<,lW9daaB_a6KbZPqW8CN'\9d>eE-m(3"ai4q5hm+9cdoo`$YgO5nl_Zc(;MO^XbIKkd4D#HQ/Oe1E#pd!_Y$FUO>OUL[S+2fClE.s1EBl(a9;hYk9=O7>=a#!)ekE$=m:4n+%%Iaad;U4E)6KTgrtLn]K+Br'AnMHnrV+XDb_!gm,DTYkhPqEG:7l,mTo?F&j(1=Vj8M,V^3?8ZgE=SFIb9RLGKXt?Aol&AX1'snhKPf3QN+H.r%Sck-7,;@0u:F4Za\)`:`:-ZjC4\>Wr"s>Z''[1K1=.Y9al'AK<RVji.C2kW]Q^u6h#n!'WZaE0E9)^4aM@O?oDcG9F*.8b+DMp5LgG*I3g?\%Bo2n:s35(6Yl4h4H5N5>e'@XAr")p<@0%L#\`sj#lk$.$[P=foaP(MbBWo\MnBa<6P)ld<GOWo3G.TiVUTBZ9*J)l".kP]e)W&]Hf*qcfX%9AE`)FLh;-8H":^U_^"W0#bJV9mHiSBC+`L1+<Le@H5Q1j/FRhZ($P8@~>endstream
endobj
86 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3359
>>
stream
Gb"/*D0)1;')q<+dH.C?31G_jiC-:AW@ce=a1IZS7oF6nKBcDAN?f.T&/a*jVR!u8H9bkk5_,A@D:NLFYlce(:\W(T5>Mef6,AU#s)urnk@9UW<)<m[(SL&V%c^W03Bm7\U2G>CL1FJBU-lD*8W)*JnMtF*#LuM6WkiohWZi>?EVYKB-0-NF-X=OALtd:j!E"[+;9Y'#oUj"q6``?:Gm7?:Ye>aZi`kuU+u=,[`LbiMDK;9t4e!##h!LOf_>,DL/Mrjj$[9,fl24%j<jqHq%i2<<;j#gCTD=G`4lm7&n<V@XqBoo:H:%n_p?+9&4G=(b>mN>Ec](t5LYQ1f%ZfNcgsXPTg^ENKde,iFK#U%99_=]^F-l^lh`mdN>Ub(.-fl@t8ghW?Ln]+4`0>Jm[B)`8k#='VKV\2dY4E<N:!oUO[5j[=Vmifr-Vk\?LfdQa>Tj]#EcA/Y7V@(K_e20P&_KA\:C7R6R.&ii)N/?1).,Uo97J!:fB7J_.]h0i\"b[Z3"Mat/E8-'Bq]qR/?s.CY%1cd+`lh,*V!hTZ4GIDZ4ks>MFQ7db,a%8;QpKHR&"gC]2pSQo]t`.)jmo+4l!Sq<AYs?8^Up(';OujqkH]`fEc\CoGg3/At4$\a2.Wa0[IXN.`g]F!UW6f(IV#i4pT6$"q04n[*7BVgk9dc_R7WKS_r(nI?L%"=P2HJ'4MfUq!UPJ`F.V?64Z-)#Wi!:GhP41nDo2sSGKlSNe`Kjk./UZPWqR[Ifbg*3m'7/c5`F'3R'[/gAc1C9K_^7#-P^'I=@W$Pc6ab>F\0qPMA.rqR%I1H`WT4s/D0a<Q>=X,J0od]t/i6AfY%ZOMM,;ffoKR#i#+uUn[lpIU>,"VFWJB!J[T:<(`$:1d_@:l^:As!i]ssT!!rRp[KuNUDl@QW5O?!-DQ[>iWg/H52.]e;:\0n`T^4s]7%aC[;r\3&TY-pSQrgSW(WYi8[?03ZBqpUSC>Dtjh$-j>@T1V(N56OHV#`cKkLUe_`UEVqr+T-f_i'"*`Q?PkRFnuH#9`%VV6>Pg'aJ``Q5K\G>JZBS_@-BO=o@Fe$+;bm:G_p4fhSX!O:?n(uMTY5WpY4^rl8Ihde=Hf<\rnc]fKT"[bjS*J6\W_Lsgr42(a\LR?p.5`X'kp,[Eag;_91D6-,9[T.F[[U!eZ9H-Z%"7^]6*Jp'l'e4^mlm)11Pc4Kps*Zkh/CN&X'oIKr02F9Ihka2!r=B\5b(ERHT]GPl41CK1I^pN]@PC73)Xo4n^bh-g^oOgCK7C$)YL3Wfau/*Qg45;Jf76po"!P@rj'liX`HH(dWc4)/&kUPoZ^A5.*3,E0HYj8k'67!GIBett?;nRIo?)ce)Q'<PQ/3+9;e07K\Wi\]l>$&!ff,BS?r8WY2U?]O\"1=>ohKhgDAK2"7PHbq'?EJBP'O6RKLukcR.0+ObqG+b)3t*g@*$FcQ%t=pmAq40ic=QoG*+qc.;%QZd@=KYS#^bFVFue\p*rqN5r?^9G%aSXitSsS=@>pJ#0?HE4H=m!f7A]M+^Pek%K[F*3fn!_K<]hW-]b*DIi9sUc?R]pe/G?QR9-q*;GL;pn6C8*%d)""a!E&9j'.:D%?f5i=gOO^+\0NAF;EM+>$"4Ygi*r6o:T.KD&l2Hg7]f?6:;708iY-]JAWEbR7MhN2'#am8SR>ikCa.-?sb=7%H_HXhg9Sq./\2Qd^p=#/Q:!g5=PpEL?F2I4ud@!f`"%<41%knKr>+#.Ett`A`QP`S+V2Zf(UiLDie=e@?Z(=T,lY/ID\%(r^8qq9l9?L-(0N,8`%Rl0/%=P,-0S#U7?9q9BIn&X991En"k$;>>jl>"S*tD`qP%4bVj[nn/Qm@#>rG9VUTdRKXuIO2A0<fTc=m=;Fa26\h['i2rgF^+UoSA1:T&'D;:4RDk1CjmCQd/&D'<bjZ<8Qb$Q?2Zhu9VLF,"`DcYWSG4XkXIKA)BB:hNU4)@YRS1uZH/3?+mGCFf[4WHhgLNJOijAZ6ZG3!)hV26Fdo7opcl:!(8$7U9L47q6XkID80gjNS(]6*unMq_Q>Q(hB)!VCG0[uafq:dhaIpLV_MRo5D4o^NscaD9jmE_(+gf>B/"!7>lWOJ+psGL@-:JU:#@nD=t@8kP>-Xdg$S'gOTKPB:l\!:nL19+ZcQ'p+`W@MDWa'G:>)A>6#[@]J]mRr1Q2R\=rd&d`^s_6"4s'uE/*XH-TsnMT.F`qbTh\/*Jj"R)0S%]"9"b@(r'$nmAh`$U-._<CGY#u`621$*k*\n4kq!%E5UIPDh$XtN;o*KdJL]KhXLZ;5Ju2U03o.9`5\9[t#K-B>T^#M?G]CQ`6A;VaW=ZOSPni>^p'Y/a)K[ti_gJLi*>6mk!+2+4F6"mlk^Em@TNBZaK)Nne[#;^;%A/>s-rhdI7p`:)Rhk3n*I`(/k4Tm_"(mislK=g<qCS.%9,^cNHa6_s3l@ttL1@s]aKg<r_ikj*u#/+R</TZVN'H(0%u!OaX6GkMQs(r7:%kBFQr*0%*U>XkHNq)s9,^;G%ek"_,7Chmc@Upl/--jZ#Q03HnMmV6)d^HbT2X21:fJA!rpH2mJq4MJ<S,3$>>@bf``_/hPU(ts=Dg0akL7g?RFfa0+Ub-`1C/4<>!ZT:QTQ=obP+@RBbIQ&=cg[UrX!@DG1&51M:FLHHW@UFWE*7OkD7D(9OG5@f"!#a\Upjh,%O+EG"^i^d(mlb:/bMmG]=C)<'&&Rn=?TB\;$_Zs)#j(8sgK]TLM2k-;V]1nWLhL'^,!E!<K48@3>!f*9gR^K&%Z.X8Y8/osHE=4$.n-JZ.Ws9F;4MQaDL-'"i:>$6NSOOmPc$!dBjDV#McWN'(HC-,IdEJ@:MUnODj)2,V_^A.Gsd_XPZl[0Q+CbdbemhW:mA@9KhTeeb)O?%gOp+N60-c$OtM2;AVjKmgY!F-\`N9t0\ljUN]48IOd[<f/d/#(6j3]7-4kD+6L=e`)BE+J(_bW>Wg&_hs17$H/MCT;&D-m(O3_%XUjWW6=+7%REc,\aqidB*!:El?VfB!/=o..J@o"FDm$ZO%@l',f)Wua2RYb\grKDe&dAl`PO+@-0lO/qa1]$h,o6Bo_?8c.Ra-?VaMABs+F+3GbS%RU^]+'@R2%*0]!UU=g#2BU3ME2iu??d0K'sK"q&Fm%<R!n1#Nb7m7">%\SiFU_rfI,:&=(t$5jng_*;I4*P,qQK\V"][KrT6XPQ$R/"f'hj.D8/aIS?=SFESC7HSbR2M6'DYWpeFe@Dd]LS2ROUsO=it](rn,?jV?G'=mZYZXkop<3t'c!#MP%3QYe=RX5dHP;#L3@[c2;bZu+c&DV,.K'@js27L[C8<@]udbQ8Z`*BeB45F-f93<~>endstream
endobj
xref
0 87
0000000000 65535 f
0000000061 00000 n
0000000160 00000 n
0000000267 00000 n
0000000379 00000 n
0000000526 00000 n
0000000674 00000 n
0000000822 00000 n
0000000969 00000 n
0000001116 00000 n
0000001264 00000 n
0000001413 00000 n
0000001562 00000 n
0000001710 00000 n
0000001859 00000 n
0000002007 00000 n
0000002156 00000 n
0000002304 00000 n
0000002453 00000 n
0000002601 00000 n
0000002749 00000 n
0000002897 00000 n
0000003045 00000 n
0000003194 00000 n
0000003342 00000 n
0000003491 00000 n
0000003640 00000 n
0000003789 00000 n
0000003938 00000 n
0000004086 00000 n
0000004234 00000 n
0000004383 00000 n
0000004531 00000 n
0000004679 00000 n
0000004795 00000 n
0000004901 00000 n
0000005021 00000 n
0000005105 00000 n
0000005183 00000 n
0000005389 00000 n
0000005810 00000 n
0000006016 00000 n
0000006222 00000 n
0000006428 00000 n
0000006634 00000 n
0000006840 00000 n
0000007046 00000 n
0000007133 00000 n
0000007450 00000 n
0000007525 00000 n
0000007626 00000 n
0000007732 00000 n
0000007839 00000 n
0000007997 00000 n
0000008096 00000 n
0000008206 00000 n
0000008311 00000 n
0000008452 00000 n
0000008551 00000 n
0000008659 00000 n
0000008775 00000 n
0000008915 00000 n
0000009014 00000 n
0000009126 00000 n
0000009273 00000 n
0000009359 00000 n
0000009514 00000 n
0000009600 00000 n
0000009745 00000 n
0000009831 00000 n
0000009939 00000 n
0000010084 00000 n
0000010172 00000 n
0000010274 00000 n
0000010389 00000 n
0000010507 00000 n
0000010615 00000 n
0000010727 00000 n
0000010827 00000 n
0000010937 00000 n
0000011890 00000 n
0000012947 00000 n
0000013177 00000 n
0000013405 00000 n
0000016973 00000 n
0000020036 00000 n
0000022964 00000 n
trailer
<<
/ID
[<82e09bcb7eb13164861aeba124022201><82e09bcb7eb13164861aeba124022201>]
% ReportLab generated PDF document -- digest (opensource)
/Info 47 0 R
/Root 46 0 R
/Size 87
>>
startxref
26415
%%EOF

View File

@@ -0,0 +1,258 @@
# PostgreSQL, TimescaleDB, and platform DB adapters — comparison
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R0 |
| Creation date | 2026-06-20 |
| Last modification date | 2026-06-20 |
| Co-authored | Cursor Agent (project assistant) |
| Severity | medium |
| State | advisory |
| Document type | technical |
| Supersedes | — |
| Design review | — |
<!-- doc-meta:end -->
---
**Document type:** Technical advisory (not a DR/SPEC)
**Context:** [repos reorganizing SPEC R1](specs/20260618_repos_reorganizing.md) approved; `ac-platform-db` planned as migration-only repo; production today is **MariaDB on Alpine BE** ([INFRA.md](INFRA.md)) with **SQLite** for local dev.
**PDF:** [20260620_postgres_timescale_db_platform.pdf](20260620_postgres_timescale_db_platform.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh`
**Related:** [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) · [specs/20100612_1_scaling.md](specs/20100612_1_scaling.md)
**Documentation index:** [README.md](README.md)
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. When SPEC implementation can start](#2-when-spec-implementation-can-start)
- [3. TimescaleDB fit for AndroidCast](#3-timescaledb-fit-for-androidcast)
- [4. Platform DB adapter layout](#4-platform-db-adapter-layout)
- [5. MariaDB vs PostgreSQL comparison](#5-mariadb-vs-postgresql-comparison)
- [6. PostgreSQL vs PostgreSQL + TimescaleDB](#6-postgresql-vs-postgresql--timescaledb)
- [7. Migration effort (MariaDB → PostgreSQL)](#7-migration-effort-mariadb--postgresql)
- [8. Verdict and recommended path](#8-verdict-and-recommended-path)
- [9. Changelog](#9-changelog)
<!-- /toc -->
---
## 1. Executive summary
| Question | Short answer |
|----------|--------------|
| **TimescaleDB worth it?** | **Yes, selectively** — for high-frequency cast/WebRTC telemetry and rollups, not for tickets/users/RBAC. |
| **Split `ac-platform-db` + dialect adapters?** | **Moderate effort** — clean architecture for SPEC Step 3; **do not** run two production engines long-term. |
| **Pick one engine forever?** | **PostgreSQL (+ Timescale on hypertables)** is the better long-term bet for analytics, cloud, and schema rigor; MariaDB is fine to **keep through alpha** while adapters are designed. |
| **Verdict** | Design **`ac-platform-db`** as dialect-neutral migrations + runner; ship **`ac-platform-db-postgres`** as primary target; keep **`ac-platform-db-mariadb`** as a **transition shim** (alpha + parity tests), then retire. Enable Timescale only on **stats hypertables** in the same Postgres instance. |
---
## 2. When SPEC implementation can start
**Green light** once all of the following are true:
| Gate | Owner | Notes |
|------|-------|-------|
| SPEC R1 **approved** | PO | Done |
| `git://f0xx.org/ac/*` bare repos exist and ACLs work | PO | Done (per PO) |
| Monolith `android_cast` **read-only** for day-to-day dev | PO | You are setting this — avoids drift while extracts land in `ac-*` |
| Step 1 verify: push/pull smoke test to `ac/ac-docs`, `ac/ac-scripts` | Agent/PO | 5-minute git check |
| Write path agreed: feature branches on `ac-*` repos, not monolith | Team | After R/O freeze |
**Recommended first implementation slices** (SPEC §9 order):
1. **Step 2** — push `ac-docs`, `ac-scripts`, `ac-platform-web` from monolith history (no runtime coupling).
2. **Step 3** — extract `ac-platform-php` + **`ac-platform-db`** skeleton (this document informs dialect choice).
3. **Defer** domain MS splits (Step 6+) until platform Composer packages publish and BE can `composer require` them.
Monolith can stay **deployable** for production until Step 8+ per SPEC; R/O on the legacy repo does **not** block new work on `ac-*` repos.
---
## 3. TimescaleDB fit for AndroidCast
[TimescaleDB](https://github.com/timescale/timescaledb) is a **PostgreSQL extension** (Apache-2.0 core; some features under Timescale License). It adds:
- **Hypertables** — automatic time partitioning (chunks) for fast range scans
- **Compression / columnstore** — large retention with less disk
- **`time_bucket()`** — idiomatic rollups (hourly/daily session stats)
- **Continuous aggregates** — incremental materialized rollups (better than plain PG materialized views for append-heavy series)
- **Retention policies** — drop or tier old chunks
### 3.1 Where it helps in this project
| Workload | Current shape | Timescale benefit |
|----------|---------------|-------------------|
| **Cast session graphs** (`graph_sessions`) | One row per session; dashboards aggregate by day | **Low today** — row count is modest; MariaDB indexes suffice |
| **Live cast heartbeats / WebRTC stats** (growing) | Sub-minute samples per peer/session | **High** — append-heavy, range + `GROUP BY time_bucket` |
| **Remote access events** | Event log, time-ordered | **Medium** — compression + retention if volume grows |
| **Tickets, users, RBAC, reports** | OLTP | **None** — keep normal relational tables |
**Founder pitch (accurate):** the win is **fast analytical queries over large time-series** with **compression** and **incremental aggregates**, not replacing your OLTP database.
### 3.2 Practical adoption pattern
Use **one PostgreSQL instance**:
- **Regular tables** — identity, tickets, devices, reports (standard Postgres)
- **Hypertables** — e.g. `cast_stats_samples (time, session_id, metric, value)` after live/WebRTC stats land at scale
- **Continuous aggregates** — e.g. hourly `avg_kbps`, `packet_loss` per company for graphs UI
Do **not** run a separate Timescale-only cluster until traffic proves it; extension install on the primary BE Postgres is enough for alpha/beta scale.
### 3.3 Risks
| Risk | Mitigation |
|------|------------|
| Extension not on Alpine/Alpine PHP image path | Use official `timescale/timescaledb-ha` Docker on `be-data-1` or managed Tiger Cloud later |
| License on advanced features | Core hypertables/compression sufficient; read [Timescale licensing](https://www.timescale.com/legal/licenses) before depending on TSL features |
| PO single-engine ops | Same backup/replica story as Postgres; not a second DB product |
---
## 4. Platform DB adapter layout
Proposed repos (extends SPEC §6 P1):
```text
ac-platform-db # Contracts: MigrationInterface, SchemaVersion, portable types
ac-platform-db-postgres # Postgres + Timescale DDL, pgsql migrations, pg_dump helpers
ac-platform-db-mariadb # MariaDB DDL (transition), mysql migrations
ac-platform-db-sqlite # Optional: dev-only adapter (today's sqlite path)
```
### 4.1 What `ac-platform-db` owns (core)
- Migration runner (ordered files, `schema_migrations` table)
- **Portable** migration IDs and naming (`001_users`, not engine-specific paths only)
- Dialect hooks: `boolean`, `json`, `enum`, `autoIncrement`, `timestampMs`, `upsert`
- **Duplication wire** (switching engines): export/import interface
### 4.2 What dialect repos own
| Concern | MariaDB adapter | Postgres adapter |
|---------|-----------------|------------------|
| DDL syntax | `AUTO_INCREMENT`, `ENGINE=InnoDB`, `ENUM(...)` | `SERIAL`/`GENERATED`, native `ENUM` or `CHECK` |
| Upsert | `ON DUPLICATE KEY UPDATE` | `ON CONFLICT … DO UPDATE` |
| Migrations | `.mariadb.sql` or PHP builder | `.pgsql.sql` + optional `CREATE EXTENSION timescaledb` |
| Index DDL | `CREATE INDEX IF NOT EXISTS` (10.1.1+) | `CREATE INDEX CONCURRENTLY` for prod |
| Time helpers | `FROM_UNIXTIME(ms/1000)` | `to_timestamp(ms/1000.0)` |
### 4.3 Effort to maintain two adapters
| Aspect | Assessment |
|--------|------------|
| **Mechanical** | **Medium** — you already branch on `sqlite` vs `mysql` in `Database.php`, `GraphRepository.php`, `LiveCastRepository.php` |
| **Ongoing cost** | **High if 100% parity forever** — every migration written twice + CI matrix |
| **Sustainable pattern** | **Single source of truth** migrations in PHP/DSL that emit dialect SQL, *or* one canonical Postgres migration + MariaDB shim only until cutover |
| **Duplication while switching** | **Hard across engines** — use one-time **pgloader** / custom ETL + dual-write window (days), not perpetual sync |
**Recommendation:** treat MariaDB adapter as **legacy bridge**; Postgres adapter as **canonical** after cutover date. CI runs integration tests on **Postgres**; MariaDB optional until retired.
---
## 5. MariaDB vs PostgreSQL comparison
Scores are for **AndroidCast** (PHP PDO microservices, session OLTP, growing stats, VM-first → managed cloud).
| Criterion | MariaDB | PostgreSQL | Notes for us |
|-----------|---------|------------|--------------|
| **Maintainability (ops)** | ●●●○ | ●●●● | MariaDB already on Alpine BE; Postgres is one `apk`/Docker change |
| **Maintainability (schema)** | ●●○○ | ●●●● | Less `ENUM`/engine-specific drift; stricter types help multi-repo migrations |
| **App code (PHP PDO)** | ●●●● | ●●●● | Both first-class; rewrite mostly SQL strings, not driver |
| **Flexibility (SQL/features)** | ●●●○ | ●●●● | PG: richer JSON, window functions, CTEs, partial indexes |
| **OLTP query speed** | ●●●● | ●●●● | Comparable at our row counts; tuning matters more than brand |
| **Analytics / time-range** | ●●○○ | ●●●○ | PG better planners on complex aggregates; **Timescale** tips this further |
| **Vertical scale** | ●●●● | ●●●● | Single BE VM — both fine to 100GB class with tuning |
| **Horizontal scale** | ●●●○ | ●●●● | PG: read replicas, logical replication, Citus/Timescale multi-node later; MariaDB Galera/GTID (cluster0) works but ops-heavy |
| **Cloud managed availability** | ●●●● | ●●●●● | RDS/Aurora MySQL vs RDS/Aurora PG/AlloyDB/Timescale Cloud — PG slightly broader for analytics extensions |
| **SQLite dev parity** | ●●●○ | ●●●○ | Keep `ac-platform-db-sqlite` for laptop; neither matches prod perfectly |
| **Existing investment** | ●●●●● | ●○○○○ | All prod migrations, `schema.mariadb.sql`, cluster0 GTID docs |
| **Timescale / time-series** | ●○○○ | ●●●●● | MariaDB has no equivalent extension in-tree |
| **Licensing / vendor** | GPL / BSL nuances | PostgreSQL License | Both acceptable; Timescale has dual license on some features |
### 5.1 Other bonuses from PostgreSQL
- **Same DB for OLTP + stats** with Timescale on selected tables (no Flink/ETL for dashboards)
- **`pgvector` / AI extensions** — optional later (Tiger Data stack); not alpha scope
- **Logical replication** — easier blue/green DB upgrades than MySQL dump cuts
- **Stricter constraints** — catches migration bugs earlier when `ac-platform-db` fans out to many MS repos
### 5.2 Reasons to stay on MariaDB longer
- Production already stable; **alpha must not hinge on DB migration**
- `orchestration/sim/cluster0` GTID MariaDB story is documented
- Team familiarity; zero new extension packaging on Alpine until you choose Docker for `be-data-1`
---
## 6. PostgreSQL vs PostgreSQL + TimescaleDB
| Layer | Plain PostgreSQL | + TimescaleDB |
|-------|------------------|---------------|
| **OLTP** | Full | Unchanged (regular tables) |
| **Session-level graphs** (current) | Indexed `BIGINT` ms timestamps — adequate | Optional; benefit small until row counts > ~1M |
| **High-frequency samples** | Partition manually or suffer sequential scans | Hypertables + compression — **large win** |
| **Dashboard rollups** | Materialized views (full refresh) | Continuous aggregates — **incremental** |
| **Ops** | Standard Postgres backup | + extension version pin; use `timescale/timescaledb-ha` images |
| **Query speed (simple)** | Baseline | Same for non-hypertable tables |
| **Query speed (time aggregates)** | Good with indexes | **Much better** at scale (`time_bucket`, chunk exclusion) |
**Rule of thumb:** adopt **Postgres first**; add **Timescale** when (a) live/WebRTC stats storage is defined, and (b) you expect **sustained append rate** or **90+ day retention** of sub-session samples.
---
## 7. Migration effort (MariaDB → PostgreSQL)
| Area | Effort | Examples in repo |
|------|--------|------------------|
| Schema DDL | Medium | `ENUM`, `AUTO_INCREMENT`, `ENGINE=InnoDB`, `LONGTEXT` |
| Repository SQL | MediumHigh | `GraphRepository`, `LiveCastRepository`, `Database::tableExists` introspection |
| Migrations | Medium | 9+ migration files; idempotent `ALTER` patterns differ |
| Config / deploy | Low | `pdo_pgsql`, DSN in config, Docker compose for `be-data-1` |
| Data cutover | Medium | `pgloader` or CSV per table; schedule maintenance window |
| Tests | Medium | PHPUnit against Postgres in CI |
**Estimated engineering (order of magnitude):** 12 weeks focused for schema + repos + CI, **not** blocking SPEC Steps 25 if Postgres is chosen before `ac-platform-db` migrations are copied out of the monolith.
**Duplication window:** dual-write (application writes both) for 2472h *or* read-only monolith + one-shot migration — dual-write across MariaDB↔Postgres is **error-prone**; prefer freeze + migrate + verify.
---
## 8. Verdict and recommended path
### 8.1 Decision
1. **Approve PostgreSQL as the long-term platform database** (managed or VM/Docker).
2. **Use TimescaleDB as an extension on that Postgres** for **telemetry hypertables only** — align with cast stats / live session analytics roadmap.
3. **Structure `ac-platform-db`** as core + **`ac-platform-db-postgres`** (primary) + **`ac-platform-db-mariadb`** (temporary parity for current BE until cutover).
4. **Do not** invest in keeping two production engines in sync indefinitely; MariaDB adapter exists to **de-risk alpha**, not to double every feature forever.
### 8.2 Phasing
| Phase | Database | Action |
|-------|----------|--------|
| **Alpha (now)** | MariaDB on Alpine | No forced migration; extract `ac-platform-db` with both adapters sketched |
| **Platform extract (SPEC Step 3)** | CI on Postgres + Timescale Docker | New migrations authored **Postgres-first**; MariaDB shim generated or hand-ported for prod continuity |
| **Pre-beta** | Postgres (+ Timescale) on `be-data-1` | One cutover; hypertable for stats; retire MariaDB adapter from CI |
| **Cloud (scaling SPEC)** | RDS / Aurora PG / Timescale Cloud | Same `ac-platform-db-postgres` migrations as job |
### 8.3 What to prototype first (low cost)
1. Docker Compose: Postgres 16 + Timescale — import anonymized `graph_sessions` + fake 1Hz stats table as hypertable.
2. Re-run graphs dashboard queries with `time_bucket` vs current MariaDB `GROUP BY day` — measure at 100k / 1M / 10M rows.
3. Spike `ac-platform-db` migration interface with one table (`schema_migrations`) emitted to both dialects.
---
## 9. Changelog
| Rev | Date | Change |
|-----|------|--------|
| R0 | 2026-06-20 | Initial advisory: Timescale fit, adapter layout, MariaDB vs Postgres, verdict |

View File

@@ -0,0 +1,444 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 18 0 R /F4 25 0 R /F5 26 0 R /F6 27 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 762.0394 124.373 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 751.0394 188.853 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 740.0394 168.389 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 729.6019 166.395 739.6019 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 719.6019 157.2375 729.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 709.6019 90.52504 719.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 699.0394 150.621 710.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 688.6019 181.3875 698.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 678.6019 150.1425 688.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 668.6019 173.91 678.6019 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 658.0394 185.285 669.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 647.6019 181.41 657.6019 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 637.6019 190.575 647.6019 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 627.0394 210.861 638.0394 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
19 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 616.0394 202.517 627.0394 ] /Subtype /Link /Type /Annot
>>
endobj
20 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 605.0394 166.189 616.0394 ] /Subtype /Link /Type /Annot
>>
endobj
21 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 594.6019 100.95 604.6019 ] /Subtype /Link /Type /Annot
>>
endobj
22 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 584.6019 99.29254 594.6019 ] /Subtype /Link /Type /Annot
>>
endobj
23 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 59.68504 574.6019 179.73 584.6019 ] /Subtype /Link /Type /Annot
>>
endobj
24 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 28 0 R /Fit ] /Rect [ 45.68504 564.0394 93.26904 575.0394 ] /Subtype /Link /Type /Annot
>>
endobj
25 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
26 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
27 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
28 0 obj
<<
/Contents 58 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 57 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
29 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R ] /Contents 59 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 57 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
30 0 obj
<<
/Contents 60 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 57 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
31 0 obj
<<
/Contents 61 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 57 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
32 0 obj
<<
/Contents 62 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 57 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
33 0 obj
<<
/Contents 63 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 57 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
34 0 obj
<<
/Outlines 36 0 R /PageMode /UseNone /Pages 57 0 R /Type /Catalog
>>
endobj
35 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (PostgreSQL, TimescaleDB, and platform DB adapters \204 comparison) /Trapped /False
>>
endobj
36 0 obj
<<
/Count 24 /First 37 0 R /Last 56 0 R /Type /Outlines
>>
endobj
37 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 38 0 R /Parent 36 0 R /Title (1. Executive summary)
>>
endobj
38 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 39 0 R /Parent 36 0 R /Prev 37 0 R /Title (2. When SPEC implementation can start)
>>
endobj
39 0 obj
<<
/Count 3 /Dest [ 28 0 R /Fit ] /First 40 0 R /Last 42 0 R /Next 43 0 R /Parent 36 0 R
/Prev 38 0 R /Title (3. TimescaleDB fit for AndroidCast)
>>
endobj
40 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 41 0 R /Parent 39 0 R /Title (3.1 Where it helps in this project)
>>
endobj
41 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 42 0 R /Parent 39 0 R /Prev 40 0 R /Title (3.2 Practical adoption pattern)
>>
endobj
42 0 obj
<<
/Dest [ 28 0 R /Fit ] /Parent 39 0 R /Prev 41 0 R /Title (3.3 Risks)
>>
endobj
43 0 obj
<<
/Count 3 /Dest [ 28 0 R /Fit ] /First 44 0 R /Last 46 0 R /Next 47 0 R /Parent 36 0 R
/Prev 39 0 R /Title (4. Platform DB adapter layout)
>>
endobj
44 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 45 0 R /Parent 43 0 R /Title (4.1 What ac-platform-db owns \(core\))
>>
endobj
45 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 46 0 R /Parent 43 0 R /Prev 44 0 R /Title (4.2 What dialect repos own)
>>
endobj
46 0 obj
<<
/Dest [ 28 0 R /Fit ] /Parent 43 0 R /Prev 45 0 R /Title (4.3 Effort to maintain two adapters)
>>
endobj
47 0 obj
<<
/Count 2 /Dest [ 28 0 R /Fit ] /First 48 0 R /Last 49 0 R /Next 50 0 R /Parent 36 0 R
/Prev 43 0 R /Title (5. MariaDB vs PostgreSQL comparison)
>>
endobj
48 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 49 0 R /Parent 47 0 R /Title (5.1 Other bonuses from PostgreSQL)
>>
endobj
49 0 obj
<<
/Dest [ 28 0 R /Fit ] /Parent 47 0 R /Prev 48 0 R /Title (5.2 Reasons to stay on MariaDB longer)
>>
endobj
50 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 51 0 R /Parent 36 0 R /Prev 47 0 R /Title (6. PostgreSQL vs PostgreSQL + TimescaleDB)
>>
endobj
51 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 52 0 R /Parent 36 0 R /Prev 50 0 R /Title (\376\377\0007\000.\000 \000M\000i\000g\000r\000a\000t\000i\000o\000n\000 \000e\000f\000f\000o\000r\000t\000 \000\(\000M\000a\000r\000i\000a\000D\000B\000 !\222\000 \000P\000o\000s\000t\000g\000r\000e\000S\000Q\000L\000\))
>>
endobj
52 0 obj
<<
/Count 3 /Dest [ 28 0 R /Fit ] /First 53 0 R /Last 55 0 R /Next 56 0 R /Parent 36 0 R
/Prev 51 0 R /Title (8. Verdict and recommended path)
>>
endobj
53 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 54 0 R /Parent 52 0 R /Title (8.1 Decision)
>>
endobj
54 0 obj
<<
/Dest [ 28 0 R /Fit ] /Next 55 0 R /Parent 52 0 R /Prev 53 0 R /Title (8.2 Phasing)
>>
endobj
55 0 obj
<<
/Dest [ 28 0 R /Fit ] /Parent 52 0 R /Prev 54 0 R /Title (8.3 What to prototype first \(low cost\))
>>
endobj
56 0 obj
<<
/Dest [ 28 0 R /Fit ] /Parent 36 0 R /Prev 52 0 R /Title (9. Changelog)
>>
endobj
57 0 obj
<<
/Count 6 /Kids [ 28 0 R 29 0 R 30 0 R 31 0 R 32 0 R 33 0 R ] /Type /Pages
>>
endobj
58 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 961
>>
stream
Gatm;9i'e'&A@g>bRkCDrcW=8AQ``d@A2)?\6+_pJsrL)\01eL3e9129@Gk(7;^Q,n'.?bYqm\Vh*pVtAI<Ai_:9P,66TM5iJ9jC&)X/8T,IHpaT[iY^`8S:#d]USS/XpWTM.%)/<j6?J<Z]M-W$mWNftI".#:b;JcZNa#YM,t$8I.3OD[1<iTsWPg3$g>&,2D%-)i>[H=Li2/jX,)CcC(NnfieB)pkkH>#U[T7&bcLSo/\5P^.E^d0i>Y0^fXd4<E;N<A'FWdGuK*#pk0%]q5["iZfC?V<.WleV!aM"HJDlnZ,Sp]^M[-BGCm%5h0&m'4g$e_mq6`H5I`/Q`Pd1iKuu#$'d2gkeb.#_JrW.IZD3Nbs=31L]2^_@O%9),\DFm3LiF;:@lUW]_jT>Rq0GkDnVJd"EeG,IlF[NZ]?<HoqQt]e,<=LM`\!jq^sUmph5X"5nM():1=.(2`oJuDPO7<2NS7R7Kb"(JCcm[S/3DjR6[=r4G2sbc?/l"G/s?a94j,[*V)_\:5Yi-j9CiE+NHiYZ*tD($DX^:$aI/=ebr+FIftY.S7=RW6cqc7,OT\g+jK%XPq7>=n;fCU!Tict51aq2Rh>O?!r]9@Y:I+hS'8aufs\t/bgM(qbG;L+ZVT`dfgp%F(?X-]M*:ia4J^u_SXMOkV#tbK[lXsZW`]1U:U^YG]FDBqQb0V.8,L"@U"V_]29Y1,mA%qQiSneHDX6"(':X$\_;`^GF_*omH^eioA=^8ToP7pm7[X+Go_rs)QPg./ikfiD^&-h#fBgB/TbU7jL8+OTJOM_TgB4tW(q1j#VApbZ>06K"BZR3&Ys$$/.,%,;((,7_\3Yfh:tT/T#GN,ill]hki#Mr=&?LLl/R;XbHN`<m@sKT[iQ'fKLLVpN#UGQY+T%HH.8G\^d(f<9V%Cbp<iK(^Jgj(hGMlDN"<g)0(Zp[l(!'/G_!E_]3"n@~>endstream
endobj
59 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1027
>>
stream
Gatn&bAQ>u'Sc@-MLV%.0oSZZ]BeO`R\HC1cu'6pN'uEj8>8`)EV&leP3mfV';S;E#d=KuEk_.;X#V@bq".^5^o%MU2]Q(q>(J%P61l,Ie_*0"r,?;3ZE-Tl"F6TF-nZ0ZN\7Z:&9/V@`FThH"^%@R1?#J'1O:GKDMCZ[3jKn=2M;o!S6,9q<A5rQVY@+bmK9J-W.#V$J"bmKqcWP$qsiN*7ko;1-gR;]qHm6@]m`q"gTHb6.0(J>k5l2'IrA;IgVrh3F>pS,,8$a'VFN4b3%qBE+Gu)fLgl*2GKSIA<EGfsdN0oKnU'.qUROp@6?'ltEHhQ$dN_UQT!"rdR@*atP>r`oNG8\'h8=0<_dL_J<JN9u\X<R9<n:T@;NQkUVP"CHHY*'SSnN#5(gj[t#Ul@IHqsdXQn8W^U6V<*U]Y1dCudkLUQnVe)\A78GWX#_+s^*]8.SjQ*"H3qRsUN.n$"pXNE+.4!EegSE3Ec.4]\>WWbN'F#3/1Q!1%G=Q5J4('#qg.FAc&L)2i3ANmS6VTJW,gK2RFgUt(X6kL!^HS6oKCqrKUCKPmFjc@h"<]Ue4CO]'>s5<X-/gm`cYPf(Y4mY!1m.@qJMi2`"dLDp=cKAd-dDiWPZVI(hmJ0iXS,+#R`E'4`gYGX`5J;Q7FRAb/K-bZtc&ZYP]^-"X&1ne10`6TK-MDRD"GlFiZO+Q(T'[Pl0_=3esNM^#*qhFqN<<I(BNG?H>Zh]*:[!XsYC["ns79lfq3uq4[Bed)>)R^ULFMA;>T$M79hKNlf4'FSUnP=e3`Q5VFZWa]1<on^"Ju"g%.mZd^bDi<dDbZ$5e?@mpihPJ!DkOGrE!)6!&MGr#+2<:L4%`!.b[.OD#GSgf>?e/c+<lq:R>C9cI3H-_Ir#L:c2HI<7'j&4;MZ<QL>uCFXmR8NG/c_)Mr*IY#*2sTBJe,b7tGX6@2S=!Af'i<j8.<X)e0oc8I:\1SECc=r8)ek?.>$KL]9F`rhXV[GX6nSQC2c"9YR)+>TeB*#A=&;GC:Ds~>endstream
endobj
60 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4285
>>
stream
Gau0GD/\/g')nJ0_2iIQbBfN1HoRongY5*LUe19;"UkO@6sJHfU+0&9]YFH#Zm8;MABVMh'\cS!71Nsb1XA2$238;t%/"hK6KllCRX?iO@A:dG12P3)O#Q:oj4UUlUDR],Rd3&HNP)6kRC_e%G@s-$#Pa>A'a&oU_9>BVR&N+0EJ4g,rk*e.:#sJ4FnI+dU<th+neF_!&t>,5I"rdC<._)H,P4NY.%p[%&J!EWZYY8+UP&r,dImJ!iU6<fm%+"8n\(1d7ur2'h'\k(*KffJ-%`de4ioIB1(Jf?Cl&i%(JU2dMM(=+K[VbfCD'sHiZe\Rl6@:-dhmuHb?!YBhOY<c,makik%I2M)&AG_A!Y%P[FPM58]*ZP^I<a.G_7Ct(FZ(#IDZrAf:NT_\VsIUOA\$4:S6CbbsoHrs)NUgXhraQoKl!rT'YFV%2G];)$l:4-7ojn[5L;ENa6qVaj+mMrFJdpVJ`uVC[+]gJ$I3'IhHil3(QH/_YI-0?nDg(BP+gs\gL.6&C;g#fJ^ejo*Fk/UT*18Gu!C=N8o.&Z@J"pH]:Mt2J?<5p\oL@if\8!P@*c3^Kc6&Hs6+h-6eWC-ZFkuJg9cLdPV\G[I*+$8ch12F[Obd_gF52Ua!Sl@DEH%Fj9Z&U]4k/^8>+ph*4/KWr_UOA]VQSB"<!Y>i3,dU!Knfa9>i_`:MLdB:E4i>q(Qf+NJV>lY3qK>)n:nC[12VO#50;VmZWTT-`c=HQ_QP1C_f%@lW]j6&YIkVu0JR1%![3OEN]T;\Rl`rNlDo2nQe=bj=Z'+dOj!@XD;U#Td1@Wi0cPS_#Ri6FMU/G=j`hSm)[R9iB4iPnT+*jFk^Ld]7`D1,V%2DjpSI@Mo=7KYNVM[6T-Ql1kQB"f=A$h8_8r5VRQd&L'W^^BX%$n+YmWl,[2[O8g@#*,uUT5^II`WK+8c`*(R]<7Hlj1)_f$ngc(ZYX&<P@e,`,miS`pJBq:b5^(%:F`'\eMrQpRfk&$a1C+m]'8TUJLPRXps)`P:++Pq"27c-&TrgGQ0bo>(Q<EYD!t$KCH\V``;ZUnJ:pY)3XQ=@O<$<6D"\9guPoR^F-O=?N"F2L/\NE!I23jIL"A+_uJQhW"9IdM7>F%X6I:8BaC71D691Z*';IHdM_ff+:/Ds9!p"MR40P#*"X<ZgSCQ].RWRGQurK'eC[L!Z=4FhtnW&c^I5"'q@14eUCi<@)ukm)_D,UG8p!]7,[L&=B91T>Yii-5?!$SEF;/.^('HUGN,a9n]$/m3@K9#q'T0'&#rCYS'@JE]IdZ$]]Nit7=##e/oAM'to%3[/eM!Kb1_&l@+%1Kj^VB)41&65k[?<p=XH=DhacUMZ62-1F1\=R6t<@rVjr$->U?i2f8,*0)HB1B='U`$:_:1AB$V/n%c#,&'MXQ::pg9-Q!M[*6qn<mndS1?5g>61T\-Fj<8%e_VJ,=rfMc_OR>e$R5Zi%,CJ[H=[.<;p^I'\k&Xo2V]4T1eZQ'Ee+Ct48b\^-Z\3]23(`@'k`<>9DC[W8=+u-#QFVl9_2!)L?$tdH2mOGE8aFn<E?`]0-=]rTO-mU(OG,nH>R&,8C'[:lI91WA+^2u9`5\gQEU5+I/Zk@5UWrUn?dUPRa\D)bO;Hd;N;iI:t0JZc#^St,1D+.VO#bn<?9Ph#+Prf9b%&cSte%E?)9:o-UJgq8CQLUi:-]#I4'SSBm-rga")_E)$(%Q^^.Po<"oECg^?DRK%NE/h$)^<p*)"^)<!uL(No<mi+u,c2;*ujB>ZnCS9:TBbRo6gqaYY4lb3C]gRTb,0s>j>Z6+kQqT1/X.i+r4155n-PG_461@aa)ZA5>nZKbB5YRU%Q1Oj&3#_6hZl;RWejP3CP-"kY`aX)X]>^PRq#IeA'0\5W2c(.&a2,!d_%8bD9#'lcJC6hhHS4j^pl`8%2jTGgPp[-0]QN:L;7$1Qn7AG7?]Xm(-pj#'+7*k`XP^*&+;MgLt+VUW_gB5ZkKl$LN55/=L)bX:<j1-YPP(X^JiKGh6g'V`.AO^X;.(T%2!'?J&6bF9E2O4bW8CkhHml:?#o&$+55?P'D$3sfhZ\sV>PcMq6Y<bGS1,HKQ>EiF\QFm96$NNH2'&1kfjSJk+*R1>_pO:2FV4<+0/>A'I8!N-uR\Vs>8%rHYO:M@#65@n%9Ap:.DQdWbo6m%gaFY3jDEqu.P\h9,EXDMM1;o-Lcg?Zm1s)FhLA6=me-c&uIs6<]=Sk;`GnU29E/lUP?kRoV$\DB`4U:`l$h>4UhXc>6`\#(O6e@X]=;@`T5e]'M<QQK%Wr^oEjEWBpZMqP0gnFsciifN[ImOHL1uYuR%kIa]JNY<0;9/K%'B^;rSF8CU^0ePR4B?e0:K7GBOOr?k@Q')MZ`GG]a4_G.MH;t-`3XQ0CC'aMH77\F<Y!^1dgYsN7rA()k.7*'oal^=?n8/[65$!rbojC#l2r*VTN3gKbnUCuAu=ri&^*M(c//,GoUVqP>OCO>W('uj6egqba@L%5aX/7DN1(cBZk4>W^?F2TNJC#)9Y_%hCNqEd[]7V9Jn-+KrgmXOA60!>/5Kh=A+je*X3#CQ_1pgqT%U5/X&F@6dQ=hn1eQMpIQ4GOaIc$t"M!*#UbZplG"QHm^Sj+6#kACOE0uM&kNrakmA]<</;cKa-?mO3(i[:"#K.p\biN8pWcUJLhB?Ub(mV^&=)8$X!.nmAK4hiMgl^ajR"B6K2"Bh:._f1k$d*B5Ptn[!%OI@\#?]ZM.W#"LhGRe(Jf`RO2%u-0AfF;hFZ!!2K^r.e8jWF],a#9h*#C**0L\*tQn36]m/ho\a^3T.c<qHHb[VDNUP-:Y!oR0IY.(bCC!P(Q9CM%YA4eEYS4LVLO4AS9$bG_Q>]):m<=.%`b%FHic0AX7\7P!,dfr=QeP_A`LF>D#?FJ,tdJjjW3?hs3K0#0,j8s<*dB!=?*/k@FTuKd__"g^74Mp;@fguMGJ7N*bG.3d21dA_#R/uDPCN'7@i'?U:&*F#>[\*Y!_)]K&:i*6b(1s<ab*-HJB3Z?k!L?'&#FA7^^_uB,dC\djG.6#Y]H%kp5hj^RBEE@5o2QuUbheI]Ru8Sl/]hK.Z189F,m#nHj"0Z>co_&/lYeOKGt+WnTB<]Nq9;C:/kfPc)UcJJ7CNg:&[=HV2j4g%GQYq?<WS*>*cbE'"D%4MgAs."j=H)H["Pcl`1k;gBG+i[C_\2I'i]E;VLJX=g24!+3f++D0;K1N1j<;rA(QsXE92a49`_ea,D+NF&K)n41glf&o>/#A]IJWB&^[-:6]=[#lLVk&=WF1'OW#@IiV*L\^C]$N,OSP1U)@t\8Uj).#Ji;BEQYa2%k8M^lq_YpcW.9oL0=9L,OG!N4A%5Qdl26]Nd$]i[[ag3p95%LH^$M`LX_Ru')!^6m$Q`4m-!VS?fbE;5pVM1H@W4NMrN2%fO_HN8I-U22m^VsSE[it$kaX]&Pq5Mg2#iq7]e)>L-67n[>,h0(H_Wa@$c?NGacRe=P%ZH0mn4j6)30;dLLV$fWPkH!em"3F,%$*(,O@NC3f+0b7OF*R&V]>MKcpKM]7aI>qt]YAg(`>\kEk`*6PaPE)F%5.pA#9_Kc<\_t1hK^s/S@HTt3jPtt^(PB8C0/&39K+re$*<*Z:tRBD0YRXqG<gMD[Cm3,Q%)J%2^4ti<_N[732PsnV<Z:SKU!Hd5`b4mnIY!o_b<6p$V"W#iC6,2:6rdKVdrNY[bmm+nng3.^C\0id2WkgtD+Ve-I$lVC5%U,c=L<jmP'XQo3!i]Zu^)Uf3mH?dtoLhb$V"t!I"a(W\L-7cN:aKD?[033c!aRiu^EtFfp65QYDMao/F\V3Baa7L[Wjp4VWVf_ThqgKP*ER.qdsNj8IkMaUD:PZ`5RGV7!"q)5IhqY`s/aa&Dl$j>%H;DC%O5A^ES^P.D&TTtA\sp;0:^&L%qg*!!ru!]"[#dYY1:2IEKF\47-LLMMq>WNX'lp8j''.X^7jlSqbqFbIpq)032IgTj9;.uaQ5:oOtCP!3`)"d*Ojm*X]aL<"&J]W/#\N]>2E"QAXs/3gBsT:DqEP6g?EGbQb6:5FM2GR/TB"9MhX[\,K>;9Rl=0F+aJ_`jb!qNhKb<`V/afbKq$PobWZ0uS*3VqQc8u+_:!13Jn8B@T;/F*U!D28(-c+GrQ[u=IBr$?@o^YZ<A".Y+@I3U7k=U0h6tPDJ^=EmKSD7fMp6Nm483!'I:Z3IMRhZsE_@RK\a&$R#6rR<gFt`+[=@V;Z<DIP\HQ$:mEi+#c1%FX1*+Uo$t!jpA-"*!)sa0'rrKHeaYs~>endstream
endobj
61 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4261
>>
stream
GauHN>BAN>&q801kc1be6P:Vc6_L7#K**B-(Lu%S]i7kj)G:0G,:'BJZK$FaH?;Le&e[0:2jJOHRH^=-mcj_^HR'0mNthQMfE6M)61;,_OpnB#8?/?"C'KmPRt'9ca\9)qBZ%Q__I,>;qMMH83ZQ.nkmUfJ$[l.E5na">=[[;O.>lLpIg_gT1`4#4=T+53E]i"MqVkAEO[klFa[i_=")D$\!#;q/\u$!jBZ)"oHO(Hs_*^K]*\N.hLKStM"T]&6bB4jWo*Y2Uq3H"(:W*b-qJDW_feD]DD*?;'+8#)@21gW0fYfLq0F/(rdqI.o*>cb7pnp_0K:sFe:m$]ALKRC9L&mD'eu)`h:'kRrL;*UaQ4\Dr5(%_Lb3Q4Qo/pK0U[5g?K-M/1(bjaY8":CJ\3=_Wip?fWT=,;g2Q4%t0pP=3'LYMiL)R.b:C1_$QGqmoXLqIrFF^%P4lW6<@"kHhBs6h`-lKo4"0Uo#`G,MHa_#$#i;)dS)NMd#!r^EC_p&B:#Aq8b!WU8b'^&SFikN:,#qXk^Te::M!7Z#+m4OP.Z+V1B4]4aYHP'0/M#:L*"JdMi!ZECT1Q3V(K?et/Q\cn7E+]@3Jm(TVga8j]BEL!07M(?\dT8\1FA3_`5oB6*?'UN6)[D]=7,4CeaHEVj=&Qi3Y^)!Lq4D1iIup-DbK1GA,,[d'B<k(m="PoK7;f#*7ik9_/XF#JP'^*"YK&"aAQ1EF0$fK,cI[9Mr+a1u/$bh)g*LiZ/8!jp7:RfEc]d:a>CRJiCV`um1S&%\N0f/9n$VOc%dh5"@.A@?BZOC7BYnr;VcF!:Agp<AZ(>:N6.$tH!t$Zq<u/'a/r$4SZ'YF4I-g8lG)PBp0QthYq0`&c!fs^(&k9H1H%kR^L["/^K*O7'mVe6-aEBoa4ZQjGg>aY*X60h9Af3"1b4eHN-fPuaEC1$?I9M+<1V)M!4M`ZdE/EPp`N-4)`5j!DaF5,i+:'3K;[7`<:Pj@9Hl1H07P/8B$0[Lh3%^>?e1krnT4,-t2/aSuFKUuH\%m9a"/aA0>G1;i&C9$GQ=NeM&Hc/!omH&`RF)Bu(EGC?15Gf_\e@1e(bQ$dnL*c#c-Ti5)u9a<rr#;'aof%)62B^"QT+,bT2T+-5I`9BZ=+U['RjEe[<S(*Ro)la*GoZo^X<.)VRZa*8dE^Yjp(<**fsqrQ7h?bI3b>3Ws_]A"qZ&&7=LnA6=3QS'B=t5N`Z&h=\b_2s3>s(<Ib6Z>Vt"p<u*NI(Z@4AieCGo.l3KGJt=a#0r6kFB)!N<+.\,SH>\77)NDl#Qne^IAcU#nA;#b3*GD[=eo*c0DkfPHdV\opnG1Vh'?6u]5_N%EXa[i7N8&?nO!$]C(gSeW8Cr6B_LFcN7uKPc=RZ?//;.\!oLp8>fnL=LLmQ,Am`FqIk8rXj4?%F/^sJuiGI#/]l_Ru:As$MopGia?*m]r'2Yb=IIEFM,^NhNN]a$W6UXQ7)J?[0?l!]$m)ub]J7\UmFqZM<`-b+V`0`IoiUQ-VFZ4Qr1,]>h0S(Jh0jZURJ0XW4fi0r`g563$FGZ[WPKN,";3`Q>Xe!4+3mqZAjiG;>g$539A_n;`Qj9qm0Y]-lhA)O]2:";Ml?91FG*n>fa<UI&>\rrI!a5'K9:Y.P)cXWZ&1$U1-#U148.&enUh]n>$[<k?CQLMl8efqX9jXk&5L$9HG0T6t]g%2GYmZ^HoMUFj/Upcjj[<UVd"(4(WW*r,/#F\frg`0Oh=4U]YXYD"TC+r<ph/DL%b0\:RD+^=q/Fi2ED4EtaE8NL-KZouJL/7OL>+-d.BTZ_TJ('e(L)qh;7<I`dr?#i?j"S=jOek0ErfJ5'42J8BN#ajJfG4Ms+II1i:Y`;Mr!0*iM8*plJZ;AHnM,(NA'Dk>i]r8OUbS'4F?.CN+d:E``_:EO>`_Nl;BoIiGQD*\H,$Q-:kpF'IoRhPJ_cuJ]k4(S-AVb$^OUDEDNfTQ&,eq&OApmakHi5`6T'qug$mqd["*dbi5K-cHaE(GWf'hIFo19"p\K>]q_4S%mNslVq5T[B*dOX)r6'1Gq<ugQh!q<5jE,nIl1[.&NZ\TTU-kfC<&DW.WS@;jE]:\'Q+#'jD>D@c5.S:39u]\9F3r5rdGlJ.h]-:kj]S@^PL6Q]P0CXbY+G)X48hjnXXNr8P],_=^0f*],Er9-3r'0O(M!AUZUQQV27f?.7U@Uh0Z6/6D]?5Q6[hrWX.(@/>VE;'[-aYl#mm+<&5QA0!j=gOL6c\bEAd=Vll.BABh0!1pY[B4^l^Y69t53kJ$(-ijn*_fAJM`oeHl\T0'-&H@-X-O4&9ga'p'&HYCP[je+n7Z-VQbBr-!caRk@dcZ:*7a"?;Pe]l2q=?F^Y)m*Ydf*I>i\?fc_*i]/[1r5c/W:JT)iB>'PaO=skhcZciW&,5\[T<%5$ADPoD4\6@,2KCpL'ltHPoUBh5Wd2!*=s5H?Q&+q1<RE:c='`2UOVnp,:dZNJ5hqYE^oqh[d9:Df^X<.nYZ4hA.W\10-.XR'>P=3H9bFd8'7;e:8;:o\@#?QDW[MSh$,<3Kfn9aK\,ql=L?!dG_Ob-JJ:E,5].%?,mdJ5cSD-:2.[S(cpcQd<B16P?T*nqu)Ha.K!mB\hNMAteXga/,"k7kCoCR?"Od2q"U!-`i=k\37&oJBUM`@=I45sXoC:A,('o@f.B.oUBr=(Wd>gAs2(uC[0EC1j,Db@[XUJDk>=4,WVX)I+m:I084;unOGZa.g=0QBc'GpED*]UdXWbeNXbp)VJ//`&7]>&qhpL@BCT!`lS/3ek'-jV;l\lYKj5/2/c/C=\hWja6&e9)E@?G@9s@\+hb(Ltc%]h5=$T3%Jd-&%puG@F)H8;=:>5K-J)L:!n/>hl9J\&?dSH8mrSAg`&_ZC6)HA:F*f4##\G_]`9(6Up5m4<"QR5Dt"5`0L&67UcC^\.[Ogl*7"8tb8E!?8YLLs:ek0m2Tq&Vo?R$dU!ZdmLuUskn5)Y?M"?$qq[Rf#4t6sf%i(%b\PFUMQ[#kn`EK3@m4<_WZoZaK5Bhj(^CoE,@eagPUrBN0)[iF,.d_9#F""7=%og!G9?^o+m%Jq2k3d6,1O,N`h$f.rjYf`iflBd\)%j&D?38tTi:U3jL)sncM3eL,=2?=,:\.[VPhH/<_o-:XPp\C>!2SSX\Nu%62@le:"/M(fO@h^i+ADN+MiD6Vdg/CiCU'#'kh1Wpd3ut#X'@9ahGM2HA#qL39DrhZ0#5!6TN&kh4l9.SetuY\nG$L]gN$`r,Es]X'a1h3>Q&^r\Y4Q.U]PanIMd>97ap,:&An$RrumOFq@57SUMZjJaqE=Y)u,Y#4uJ&JCRPpL-HNbNf5!iTZO(8&BDY$(^Se0PNa=Af-g"XLdBf/HrmFUZ#1`X@C[b!RkX(h[/9iQuV,G!M9]*6O*hn]'n[o:8(uM(ZjW99T:Dgm!m)^C*c;oiRf_Y"l&&6Lr^ao3YVRs,2+R@0!V3^XTmV>6dfasrT;@NOAAc9&DFX`b\LQPi;G%b3!7e;dj$(TTP<Jq3;2=1H)?_%95ke7i&9?Pr'I)KNKJ,EV'=##d@_@RH($2*Af[!3O354-8\X2bM2!:rQ`]nH==7ILssZETjT[a/)%l27>'ab7d2ATMh%m^QSBU<7LQ:FfcIMG`iKQKN)=Z(pbI7BdA`T%;a/?u<A`Rd!dibh.rIl")S*8^4Q=K(*LF0uHi`\#p^dQCGD@[rM]sI_(jXi*#6O^6sgQo^&sKfIGl^"`4[C't9CL8^-11]6HL%o?BV&.\NClM]%8b(1!J;q([Nd>8qaiRt/8_kJ/Kl>SL1I@3=4IlLI,[2XU8g:K9L?m#L,6k916gkonKaZ&Me3IZ0!kV3ZUlB4_mV$P?BW:\bBH9R:ZmX(UXDEnLpH.+C+j&d8-OBq/f&d9C#,RL;L1g\8c-S8Ah8?"b[bF4p.X"J9Q-h9*A2ZL=Vc=ki/R'u0cAXS;gdcdr*[Qd1jLNu).e!++h/?KY[]\+?XGS^)-C-?P4-c/31li@k4echQgEM`84B^*c+ZDS`S<r"JWn'KR=>h_iQ-JUd]+s,/uMR_@WN3D\2"ar4m>DWh2"4Qnd[cO:;tS:qGKK;)GJIXmb)TfBro*`[9^jF9YT6nQta<2j+F;&]ifC1F)oB0Q,+ntC)u(Fgg/TlQkBH;9oPECiT^eCY&%AlSVHfQBp1,o]ob2-+VEX\(h+n['Pd$rjTW%E^mGYWU&uOa;W&F/I>d#(e\MUlmr'QD@rRFa$gKJQ>T!]_NWO=`&T~>endstream
endobj
62 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4017
>>
stream
GauHNgN)%.&q0LUoV40u,_?h0MJK]D[s5Q$-]Y=QmXqR;+;0hsTh'n<j`hj?gUG!\.>f<\c]lV=.=c*)]?u''!!F#6+n8oT")pGZnL"B+L%u@;513kg$Wlq`+"&?$3LPmAN>Q'e.fM6Qn[5l,re.<c%m-bF$+_i1WC3;m:rD`>/:TYtbpJ<.eY2u1]nN#8[o>55X#dP\QhopPDBfh4BR6^71/TAgbh4hA?E+F8lV4E@W/IO>Ph6,k-+/mR)bJj?OTqgL8aK?(L#f\^=#N$iRZ&qa!t<I;lFmPEi\1SqCtLG;hMT"!q&_<j7?7B_IgR'HR%l)k6:XnK,\m6k;ASW1Z&jP"`Lk\p9qkW^*DMbif2]8:c=D,5Xm+t==4$23Fde,h8lVV-<)PbX2E)K.?c.I%_*SE4l?OrAi-Sl-S%rT"1IVi<C)+#h'o\bO3igb*K/oBS>L[67facA8!=,lQGWs8*$H8HfHd.>g_`cf(7=J_i3XBl%QpeQ%F:uu=+1J^Xr/F#NG>MI+OcWkJK1Esd\I?cei3tdm;H4AG^0&;$nJDNr[j4h8lZ9:8auMRGfKhnH^=csu1=Q)'p/aJ#rq&A$h6u-m]]J"n/c6BYHWukF:2:HA9cGX2J,Ks(2mr0Oi8Vc8+2IPNEX1^W9j<;2HQV1_&J6*dGId(IM-T!J4l9l0@UKI;iJrX&]faVf>\!MiNj%V)Qgci9gk]H!jlmdJqr)`/Mb)o7rI>F1N,E"V4@n*$HI.B37lG1^7p!n=\ttE[(QRg;rGi6Pl6PeWj2@DmmI*Ne['pu;>X"1oU<jE"gq>h!]nL;8kDQeHX8D5QRHs]&Qfj&Q6"1lQAP2oO]?Y.Cg_X(G5.*+"/c?5o"pFLgm&+547i18=4u;Mq:2hYB?sp;m"R+0;4Je7'^Ei%@m?2OW1!SQGj55e!)0di5O?fok_@D,4h@/ejOT3,0=T]9CE^1F[Bd!?3'KkMK'S5"B=DPo"#^nf/ML^CRAtet+35q\;*4bq1E.Rfo*,j$1fJ$)@G)K+)hInG&*q=!=)=6`!HukXqE,Q8R^lT+s%lV]_N:RMYO=@VW&rpS\d*Y=[(OIl'-`KDV&!/*(,590SA,B;2E6rnUOs5"EjToLm_tM9pIA)k':1ibV&?hCI'_6)5_FEIOJd5CV%\=3t8*"J`dD\R*!^?%hTa9>#4K(g`4\^:h=E8N=j($^sch@+".0W-',,MTfl[)SbS3Pk^(K&LnELd.DoG2K9F+NGA9ZkJW=u;LKXLH$kAR.PQMZn\b>]pInk!0mJ<jO$(mqsli=]ocp_dLTbLr.0^rsO>V3#PFj;`>!359I%(7KtuqFG']788'"[*q>_iRue*T6d`&9)jL89fk&JKP5ZmhiG[SKk`mKBbaKfb;ehJVH';,D:d/JcB&t>FGXnqCde2&TWfp'p#gj=cPj?R9MIidP/9*8pSj*qVR]\#5+kuUDXSlqZKlgf]R2WS]6^s'/PJk80\H=SW%^e,J7B</@O_GGZ<1(c\b,N5p&bm"%rIs]/*:s.boJ]VAqHb6sj=BLdD3@[o:t#_!nh\Bfe*7(-#\'BqqhQ0rNte-&K9h0<?Z,'$n/f*4K'2YZl'@RT`VttRJ@PqaYUG>e&'+MPKAJ!-n\1'f:qdprVL6)HBk[+9GD@!4cUbB_HS7!E]>86?;bZ4ZZ"Ar%B/`XShkda#Uk'W/A1i)?/.HTAF(^3!QW-e\+m]$S3s9T<!9sc(r6"S_rB[Gcj-73]!H41E(J5E\j*DR(Nc[EC@UjL=k"2UD!J2)V8Wo!XBbb/KRfKN\ljrI%C/!`K/=*oC#TZS'gHrN.O?27$/&oc;;WsXg/hG"D**4#hEgb6h>[T'[YE-8j*X=nB+FI=0^VJ[#e>_=o%Jo-C;*fm@1_-ZiMFQ3DOh3+G'<^f+>56*H%\!)pa1=bX)d0EuW@Re0(HIsE1"NFj\GQO3CV0c#LcOIVJFh%Qd:(1tqSrEg&#E'XA--9/CrR2h?*\V^Xtq)>$kY/NE/D"J=W&s+_sGLG6h-\o%Jno#ELjU75D;=)k[Sc1>QcE*iN;Qj,uG[HS@O3H0dBIR0"grk=DQea<-%5-hc#u;VNM*%g).,MUr/(Sfgs`g-U5j4Zh>J8kXni2-^W0@eP8T1/-fBcIX5@3Q%F88R:LJ%VAc7t_ap2qR\@PgMbeV+A/a`,092VRV`dF`5+\3#of6uM1n,I)GcDc=Y,NB\[rMW(p"2nn%Z]:k17!-@:WCnMP;.)G]g?(0+r`6`,K3(B*&7#tMi"2:YgUn[M2n)##U+&F>99X>6ugf#>;jTg..+7i3fR@CL-A&/HWX/Da8P.'CO+uT,DHQY.WT6j6H0d;W56tsiaPMZN*<,g*Z::VF6Kn]Erj`dYV*.IHD:-jYbEhi&DnZnfAXOpm'&c7T$;`D8o$`fr9!k<o=R8R%?DADbd!Xf-`@6SfT1iGQ>/@b6iX]F8HJ3-/==JDNW?eeY+$c%d!nO/L&Z5h-iB%\[91FbXgB%._49&%\.!@obd.j\:jFnP_Du03kTm-"*7.MA0n_h$LNU4e?n):#/XMhGGru,d,c>X!XF$i+C]`G#r9I"k_R(M5+!3h)M-D;2Y+7]8?9mTDm-I58)AofplFtKb0!qYE7R78RG7me`]+A&;.\/fOhmX)BQ5/<j@gJqA1c8XLmI!Z_RFps(^$sF"?K=TrMA,qXh)_,B15Y.,93?iG(HT9Pd:_ne'Nk0-c^$I^dh[]B,/5NcXMblF[SMhM9)s%V_?=O5hC1hrdoM,TOla,nrh$k@Y%^%l3?,PEg"+jX;RK*L?R(<PS-LT?+,K,8E-)EBSKq4W5g#LQi;0[lqi;b]//"J"R%DlSKLjSMS!(QeKh6WVp.6(F*Pi$Y^7Fa\:7V;E`7:`h>4L79jF.nJlh$WAh<h.3/m$tWCO&u6s7\s8[?9*)DcoTepC]88.n/4tN@O5=k`REh8TI:!Y+q,j9fV(*@l"?p*_=j5'`rQE2pEi%m&XT,YoG2Mi)<D(<lCBE]W-p2M,;bQ,d1f"jDk\]nZ8,$boV`GC!%]@MO:Hh*jYLQ>ck$$027T[/'T*aC#5QOC0J#-=6nuD6R4\;-&?]N=a[<7#LrGL;<L@<Ye>-F/o%S[i]D>^NSSA_):p@[X/>uHH3^UZA`Nfp`P_TtiFQN)!lJhJ="np!ao2:-3O&Mbqb'\7r=#($1Hur>qm&<1+CJMqhAfFu1P:9LaBkJfn%kkSR,!CK:qIttC5YT?+]$K\MXa/kBa7Wq7[fVgbf1-'VWAhE&eF+1mW*o@/MRA9<)gm7UeB(_f90PH:T?.k(LLl*<Ko(t1r%,$bhD:#<G7.:"eoa%eG_iZ#I"/rZQfZlAg/_b<d9DF'5h>UH8#qGmi+&q+ep&5A,h+`@[HCG59)EMcX_f>dZ&P;UDX>(H<471nT>&G]\Li7Pjh*0iU<0$7%4Zi;]::8)+PNd*-(ZIgYE>TW544pDt>WshgE._\@T<+.GK&m?[]rRrotRaT'*"6:oX%=R(iOiq0IM_hg*tA0;Ykl*ooV@'@YTNFB?9gop1]l"RE.G/ET%`';W*OXsIRVXQ5K"D_X+U:bMIVZ)`7tHFH/4s7'S-^#Ma_3ujCCM9tE@LUJ45BGfr[.l"d`FP&uON8AR/1`"p7p#>=dl(DnX\FL9GA$\nf9/NlZIr=FN%s=uY<+:'TFKNEBUs*UIo83`)HuJ8jlRt.(>efCWj7-^WXoJ;7(&<m]#IaYZY\L`dG5:o9`cs"?gP>-o7l!_d;61:g3r8)CO]N5.++RRXn@)]Equ5W"%fT0*kQ)^-h"WpJh)/&P(QZ<3QK=4"GKTf/G#Z1*/l/40-SISR]$>3bK(b[)9l$=VhK%CX..aXjAUg0AfDBKX,Clpn03f3]'ftX!c0HimXa7ZORrka)o8\/IkgD^/8QUdbX]/PhC2lt*;Ro\KS$Q1\b+pLoCG1tERI;Sie+WalR`J$fPY1&"W=r`9T`/)e`OMe7N@$)Z/GXplR3OK+!X>\`K90epcoj':k"[C<LQh`?cUM_7~>endstream
endobj
63 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2435
>>
stream
Gau0D>BAQ-&q9SYfLIs,(kPg<IZ!ML-_9DLZ/'RaUR;NY65!aj?i\QRo'=VR'Iep&/4uHh)I''>ln6#cI7jW"IRs8r8c`h$i8$$hL]^F/+J.]2dr4aGl/K_e=mu!1.iA)j#@TTtCj57)3F'RVUle)NGY3P>Q>_euqo+,KO_:"&3JYF@V0PZjMh,G\ShVR;2M]C>aI#O11J$amj1:'"jRW6q4FJlb.YS:]9H5F!@*.'-JshE%KlUclJr1<pV=56GbD!h%gWrs*5.rj_O9sa.2bc^4D"KsKJ%UJ*c#NuJdGY2"_7`Zk/sudDb(EK.?k=oC@MqiWC1XdW+2UnsNJ;^F2(k`CGhQ/`o!tQbMbF/jZ]W<h=t^5"DQO]`;NZfhl\/>S6hJ.QB:fG,n8&Wj'm[ZkdLo[Q//HDLSE#2J_klF_TkA&qg2Htg?n1eXE`jUTGQt@u=hkt"h]VnT*f;LO27GkaLl;en6Kej]J;;"M)B)2@2DBWSP1.%4[9[Z]=+.!_`:;Z#PFJD7cas*g;@5G.-!Jd.mks0hbqT'-P-LVf/ZoL>J%]6?c;qV`i`V</KuPG92`OR=U$FLDI-*Dra38VZAKqT^A2<;0#+EL:E[9(tAMM2#:A*VVEPZmA8GC1JLB032Q&j0S[^?dY@hDf+\/qY>Se^ZpjfDH>Yt;O38Z])H531q4(KMpDj4E1=W\RimOj/ZMaV=$9K:$C7V5iig%-!L`FDfN$_Ok2lh-o"n3P"M8ef\oB?)O0+41B3QDY&WmnAJ8&,+>aJ)fla_!W,Bnbb0l^Jle_Y.6ZLFR#L?ee1>#WdrGVM<R)ikOC7jQ'\l!jj;^nhguLUFAjhmgV5[Mb%MFdE/#Gop&?KE=H*g.6.;57"]FSDAF#IFo%Ub?$(6L4EXgNt#A.%t`f8gC4"#s'\\\pBP='4REa[f..g,3;jSk3uH;$\Kt(:dH0]r7$UAK#GM2V8e5jp<YRN:WW,l&`5+p3&.('<nAno'D'=I..Q9)9Ya<e4Y@H;T.c1'-pTX<b@+jAJ'C3CrW`(ZXe1flO"+YVE#tP`].$:BNfMca)!07HH*bE(UR\jPqgLQM+)u(%1E8((`>1CdGSY@0eW+6bo_^rE9gF,*OoHQKhrc='Gt:II.4S%TOj0(f6>&Oa+57k^^>L&lk,J.Od4JS7MEh.6_".2kWfR3fc-/i53kAlYdFWR7CC)aja#5O?69OQefgCHPO!L,aMiRS%Q/9"-L"LO6C]&7l,%[MiDO1?kn:uHNIZ$!ee`I/hhO8_"3V2AnJQrIg)X[qCtB_4`$=4YZ9OS!dZN<KD?Bbj8u(<TKdMMXn75ug\NGmQ0*S/RTMB7GIal?C*jC?Rq9HLQ:uQSlrE#@Y).C!lpu8^GOcgpgl.6-^`NhIfS^-`T\e6GgcV3B3&0EeOS(Pgp>'#eZd)hpu._tH3Na=cUgi<5EGbUnED`?/I.Y`$"4^P@RVqJ8bLFouIR6iR2!6:S???rD+CH1?Pl#"`TY.I@Le:OSPB,QopE/:'*JQ*F)[+*Wbf:4NH#eN/Vc)e^`qMH(ppLGpL2OlNsO*dpn]drXbDN188K9iEs@%\#l;*AJX6Y2&6\-&:'US^Yj:>#K9Z'oN7[n(!d8/jU7N>5aj<ep'#o9g:ANnCO#O,?_dr\b?WN7\tHlB/_Tg@Wh#FoR6/7jP_h$60:"gjd2U2gJ>*d0''*MqTCMO)W`W1:jGom<hk%>tk![pac>k^T^1&kkP@mh[c>3OO,Nt_NA_>\9Ud+Z!?!df`&A9g=Y,"5S+Qoed=Uk$V3ln$i![S+;JTBi)@=>%&:4\:XoO:h'rJb%F)f1#1j>7B:8GTTjIbFLa.3ja7A$'A%^uclLcq]U^M[?q+d8M=pp>C#A&J)aA9_el^H8*7/b+4ca1W<XlqWQ=7$$+"\pJ'^#/Gn&o=t<Eb+6CN%o5l7(,_`cV:CBitu8^?[Q&gO#s\t>F<fN#38!5f>D3#D(pu/[AO7RpK[.em`07)KD5P/)+=b+W=!+7(/mM1$pP(G3F9AZ$(ZEW`8TIX5cAk'UDMHSEclBg*<,U"q]GQOo/Gsg5Dc:s.Po^d3"Xhn`\KrrQs]P'OCG3c5iT:d#6Bk=O#_\RpDkSUHe8U91h%2?!ddQ$f?'[1Z$JA+D,\;46EpWuP\\b10eH"t(39nu8GC#P=N%ARi8+q2B\[D`n[7-(Jm2b/F-_XB,G4;V/NlTn51`"[nbq2clZH[@Y6."nQfIK,FKmf7op?CfQWq#_m.'?LhI-@JfSF$!_9`[Hc=,sCg2R2Gqj_Ycab7n*Ke"<Jmp!bZ`q<>!kn:Q?Ktc97Hs#Utk&=m<L7=OVs,)36>Q*IPqb`DdhgX_8&p"[iB[5um$gZo,>f$)"CZJMPPhBGu*[+qlh="'#Q6gHc"m^\T3RDt;Mi=l>354Jf1+E\,+Ma!KCuGg>rrZ.Oen+~>endstream
endobj
xref
0 64
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000512 00000 n
0000000659 00000 n
0000000806 00000 n
0000000953 00000 n
0000001101 00000 n
0000001249 00000 n
0000001397 00000 n
0000001546 00000 n
0000001695 00000 n
0000001842 00000 n
0000001990 00000 n
0000002137 00000 n
0000002285 00000 n
0000002433 00000 n
0000002511 00000 n
0000002659 00000 n
0000002807 00000 n
0000002954 00000 n
0000003103 00000 n
0000003250 00000 n
0000003399 00000 n
0000003515 00000 n
0000003621 00000 n
0000003705 00000 n
0000003911 00000 n
0000004266 00000 n
0000004472 00000 n
0000004678 00000 n
0000004884 00000 n
0000005090 00000 n
0000005177 00000 n
0000005517 00000 n
0000005592 00000 n
0000005694 00000 n
0000005826 00000 n
0000005994 00000 n
0000006110 00000 n
0000006235 00000 n
0000006326 00000 n
0000006489 00000 n
0000006608 00000 n
0000006729 00000 n
0000006846 00000 n
0000007015 00000 n
0000007130 00000 n
0000007249 00000 n
0000007385 00000 n
0000007700 00000 n
0000007865 00000 n
0000007959 00000 n
0000008065 00000 n
0000008187 00000 n
0000008281 00000 n
0000008377 00000 n
0000009429 00000 n
0000010548 00000 n
0000014925 00000 n
0000019278 00000 n
0000023387 00000 n
trailer
<<
/ID
[<b40b5ca66a7183c90ac1aea1bb523033><b40b5ca66a7183c90ac1aea1bb523033>]
% ReportLab generated PDF document -- digest (opensource)
/Info 35 0 R
/Root 34 0 R
/Size 64
>>
startxref
25914
%%EOF

245
ALPHA.md Normal file
View File

@@ -0,0 +1,245 @@
# Android Cast — alpha release & QA
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-24 |
| Last modification date | 2026-05-24 |
| Co-authored | |
| Severity | medium |
| State | scheduled |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
Last updated: 2026-06-04. Use this document for **feature freeze**, **alpha builds**, and **sign-off** before widening testing.
**Related:** [ROADMAP.md](ROADMAP.md) · [README.md](../README.md)
---
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Alpha scope (what we ship)](#alpha-scope-what-we-ship)
- [Recommended defaults (both devices)](#recommended-defaults-both-devices)
- [Feature freeze checklist (engineering)](#feature-freeze-checklist-engineering)
- [Alpha app sign-off (manual QA)](#alpha-app-sign-off-manual-qa)
- [A. Smoke (required — all must pass)](#a-smoke-required-all-must-pass)
- [B. Resilience (required)](#b-resilience-required)
- [C. Settings matrix (pick one row for official alpha demo)](#c-settings-matrix-pick-one-row-for-official-alpha-demo)
- [D. Regression guards (quick)](#d-regression-guards-quick)
- [E. Known limitations (not alpha blockers)](#e-known-limitations-not-alpha-blockers)
- [Log capture (when something fails)](#log-capture-when-something-fails)
- [Alpha backend (optional for LAN-only alpha)](#alpha-backend-optional-for-lan-only-alpha)
- [F. Remote access — RSSH (required for full alpha sign-off)](#f-remote-access-rssh-required-for-full-alpha-sign-off)
- [G. Email + registration / 2FA (required for full alpha sign-off)](#g-email-registration-2fa-required-for-full-alpha-sign-off)
- [Ending feature freeze](#ending-feature-freeze)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Alpha scope (what we ship)
| In scope | Out of scope (frozen) |
|----------|------------------------|
| LAN discover → PIN → cast → stop | Internet relay, TLS on stream |
| **UDP** or **TCP** transport (must match on both devices) | QUIC, WebRTC, USB-tether transport in UI |
| H.264 + AAC (Android 10+ audio) | Passthrough video, Opus/Speex as default |
| **Single receiver** per cast session | 1:N multi-receiver (disabled in `CastConfig`) |
| Stream protection **None** (default) | FEC/NACK soak required for alpha sign-off |
| Screen capture (full / user-choice on API 34+) | Secondary-display capture, Miracast in-app |
| Camera cast (API 29+, best-effort) | Live sender mirror while casting |
| Receiver playback + notification stop | Play Store / AdMob production |
| **On-demand remote debug (RSSH, hidden)** — developer settings + BE | WireGuard-only remote access as alpha deliverable |
Code gate: `CastConfig.ALPHA_FEATURE_FREEZE = true` hides experimental transports and turns off multi-receiver selection.
---
## Recommended defaults (both devices)
Set in **Settings** (or use fresh install defaults):
| Setting | Sender | Receiver |
|---------|--------|----------|
| Transport | **UDP** | **UDP** |
| Stream protection | **None** | **None** |
| Quality | Medium | Medium |
| Resolution | Adaptive (or HD 720p if rotation is shaky) | — |
| Video codec | Auto | Auto |
| Audio | On (if API 29+) | Play incoming audio: On |
| Capture mode | Full screen (or User choice on Android 14+) | — |
| PIN | Same on both (default `1234`) | Same |
**Rotation soak:** On Mediatek / Freeme senders (e.g. BL6000), prefer **system rotation lock ON** during first alpha pass; then repeat with lock **OFF** to validate debounced reconfigure.
---
## Feature freeze checklist (engineering)
| Item | Status |
|------|--------|
| `ALPHA_FEATURE_FREEZE` — UDP/TCP only in settings | Done |
| Multi-receiver disabled for alpha | Done |
| Stored QUIC/WebRTC transport clamped to UDP on load | Done |
| Camera strings (permission vs unavailable) | Done |
| README matches `CastConfig` / `CastSettings` defaults | Done |
| Receiver: keyframe idle grace + debounced sender resize | Done (verify on soak) |
| Preview paused during cast (single MediaProjection) | Done |
| Developer-only: FEC, QUIC, USB-tether, immersive presets | Gated (dev settings) |
---
## Alpha app sign-off (manual QA)
Run on **two physical devices**, same WiFi subnet. Record build: About / `versionName` from APK, git commit, date.
### A. Smoke (required — all must pass)
| # | Step | Pass |
|---|------|------|
| A1 | Install same debug/release APK on sender + receiver | ☐ |
| A2 | Receiver: **Receive** → Start listening → notification *Listening* / *Ready* | ☐ |
| A3 | Sender: **Send** → discover receiver → correct PIN → approve capture | ☐ |
| A4 | Video appears on receiver within **15 s** of auth | ☐ |
| A5 | Audio audible (if enabled and API 29+) within **20 s** | ☐ |
| A6 | Sender notification shows *Casting* with resolution | ☐ |
| A7 | **Stop** from sender notification → stream ends, receiver returns to idle | ☐ |
| A8 | **Stop** from receiver notification → sender stops cleanly | ☐ |
| A9 | Second cast without reinstall (A2→A7 again) | ☐ |
### B. Resilience (required)
| # | Step | Pass |
|---|------|------|
| B1 | Sender home → cast 5 min, no reboot / ANR | ☐ |
| B2 | Receiver app to background 2 min → return → video recovers or clean idle | ☐ |
| B3 | Receiver rotate once → video continues or recovers within 10 s | ☐ |
| B4 | Sender rotate **with rotation lock ON** → 5 min stable | ☐ |
| B5 | Sender rotate **with lock OFF** (optional / device-specific) → no permanent freeze; note if reboot | ☐ |
### C. Settings matrix (pick one row for official alpha demo)
| Profile | Transport | Protection | Use |
|---------|-----------|------------|-----|
| **Alpha default** | UDP | None | Primary sign-off |
| Fallback | TCP | None | If UDP glitches on your AP |
| # | Step | Pass |
|---|------|------|
| C1 | UDP + None: 10 min soak | ☐ |
| C2 | (Optional) TCP + None: 10 min soak | ☐ |
### D. Regression guards (quick)
| # | Step | Pass |
|---|------|------|
| D1 | Wrong PIN → clear error, no crash | ☐ |
| D2 | Mismatched transport (sender TCP / receiver UDP) → fails fast with message | ☐ |
| D3 | Camera mode (if used): permission grant → cast 2 min | ☐ |
| D4 | Calibration test mode → pattern on receiver | ☐ |
### E. Known limitations (not alpha blockers)
- No TLS; LAN cleartext.
- UDP may drop frames under congestion.
- No live preview on sender while casting.
- Some apps silent under playback capture policy.
- Heavy rotation on some OEMs may still stress system WFD/WM (watchdog) — document device + lock state.
**LAN alpha approved when:** all **A**, **B1B4**, and **C1** pass on your reference sender + receiver pair. **Full alpha** additionally requires backend deploy and **F1F5** (RSSH).
---
## Log capture (when something fails)
From repo root:
```bash
./scripts/alpha-qa-logcat.sh SENDER_SERIAL RECEIVER_SERIAL
```
Or manually:
```bash
adb -s SENDER logcat -s ScreenCastService:* ReceiverCastService:* AndroidCast:* SenderScreenPreview:*
adb -s RECEIVER logcat -s ReceiverCastService:* AndroidCast:*
```
---
## Alpha backend (optional for LAN-only alpha)
LAN cast demo does **not** require OTA/crash backend. For **full alpha** (owner agreement) with updates, crash upload, and **production remote debug**:
| Item | Notes |
|------|--------|
| Host `examples/ota/v0/` | See [OTA.md](OTA.md) |
| Deploy crash reporter | See [CRASH_REPORTER.md](CRASH_REPORTER.md) |
| Configure URLs | Developer settings or `local.properties` |
| **RSSH remote access** | **Alpha essential:** hidden mode (no VPN consent); device polls BE, opens outbound reverse SSH to bastion; operator connects via BE UI whitelist + session. WireGuard v1 is lab-only for alpha sign-off. See [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) |
| **Email + 2FA auth** | **Alpha essential:** `info@`/`admin@` forward to Gmail; user register + email verify + TOTP; see [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) |
### F. Remote access — RSSH (required for full alpha sign-off)
| # | Step | Pass |
|---|------|------|
| F1 | Developer settings → **RSSH** (not WireGuard) → no Android VPN permission dialog | ☐ |
| F2 | Device polls BE (`type: ra`); operator whitelists device + opens session in dashboard | ☐ |
| F3 | Reverse tunnel active; operator reaches device debug endpoint on bastion (shell or agreed file path) | ☐ |
| F4 | Session expiry / disable tears down tunnel; BE notified | ☐ |
| F5 | No persistent VPN key icon while remote debug is active | ☐ |
**Full alpha approved when:** LAN sign-off (**A**, **B1B4**, **C1**) **and** backend items above **and** **F1F5** **and** **G1G6** pass on reference environment.
### G. Email + registration / 2FA (required for full alpha sign-off)
| # | Step | Pass |
|---|------|------|
| G1 | Inbound mail to `info@apps.f0xx.org` arrives at configured Gmail (forward) | ☐ |
| G2 | BE sends verification email (SMTP); link opens on mobile browser | ☐ |
| G3 | New user registers → verifies email → enrolls TOTP (QR) | ☐ |
| G4 | Login requires password + TOTP; lockout after repeated failures | ☐ |
| G5 | User can set recovery email; admin/root can clear auth lockouts | ☐ |
| G6 | Register/login UI matches crashes console theme (EN/RU, mobile layout) | ☐ |
---
## Ending feature freeze
1. Complete sign-off table above; note devices + APK build id in git tag message.
2. Tag release on `master` per [GIT_FLOW.md](GIT_FLOW.md).
3. To re-enable experiments on a dev branch: set `CastConfig.ALPHA_FEATURE_FREEZE = false` and restore `MULTI_RECEIVER_ENABLED` as needed.
Post-alpha priorities: rotation-stable encode path, encoder thumbnail on sender, secondary-display capture (roadmap D).

389
ALPHA.pdf Normal file
View File

@@ -0,0 +1,389 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 18 0 R /F4 19 0 R /F5 20 0 R /F6 21 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 762.0394 143.501 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 751.0394 182.629 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 740.0394 179.957 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 729.0394 157.733 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 718.6019 182.22 728.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 708.6019 138.0375 718.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 698.6019 243.495 708.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 688.6019 156.795 698.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 678.6019 195.15 688.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 668.0394 170.181 679.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 657.0394 202.645 668.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 646.6019 256.83 656.6019 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 636.6019 258.3 646.6019 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 626.0394 122.173 637.0394 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
19 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
21 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
22 0 obj
<<
/Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
23 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R ] /Contents 48 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
24 0 obj
<<
/Contents 49 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
25 0 obj
<<
/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
26 0 obj
<<
/Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
27 0 obj
<<
/Contents 52 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
28 0 obj
<<
/Contents 53 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
29 0 obj
<<
/Outlines 31 0 R /PageMode /UseNone /Pages 46 0 R /Type /Catalog
>>
endobj
30 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False
>>
endobj
31 0 obj
<<
/Count 16 /First 32 0 R /Last 45 0 R /Type /Outlines
>>
endobj
32 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 33 0 R /Parent 31 0 R /Title (Alpha scope \(what we ship\))
>>
endobj
33 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 31 0 R /Prev 32 0 R /Title (Recommended defaults \(both devices\))
>>
endobj
34 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 35 0 R /Parent 31 0 R /Prev 33 0 R /Title (Feature freeze checklist \(engineering\))
>>
endobj
35 0 obj
<<
/Count 5 /Dest [ 22 0 R /Fit ] /First 36 0 R /Last 40 0 R /Next 41 0 R /Parent 31 0 R
/Prev 34 0 R /Title (Alpha app sign-off \(manual QA\))
>>
endobj
36 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 37 0 R /Parent 35 0 R /Title (A. Smoke \(required \204 all must pass\))
>>
endobj
37 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 38 0 R /Parent 35 0 R /Prev 36 0 R /Title (B. Resilience \(required\))
>>
endobj
38 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 39 0 R /Parent 35 0 R /Prev 37 0 R /Title (C. Settings matrix \(pick one row for official alpha demo\))
>>
endobj
39 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 40 0 R /Parent 35 0 R /Prev 38 0 R /Title (D. Regression guards \(quick\))
>>
endobj
40 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 35 0 R /Prev 39 0 R /Title (E. Known limitations \(not alpha blockers\))
>>
endobj
41 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 42 0 R /Parent 31 0 R /Prev 35 0 R /Title (Log capture \(when something fails\))
>>
endobj
42 0 obj
<<
/Count 2 /Dest [ 22 0 R /Fit ] /First 43 0 R /Last 44 0 R /Next 45 0 R /Parent 31 0 R
/Prev 41 0 R /Title (Alpha backend \(optional for LAN-only alpha\))
>>
endobj
43 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 44 0 R /Parent 42 0 R /Title (F. Remote access \204 RSSH \(required for full alpha sign-off\))
>>
endobj
44 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 42 0 R /Prev 43 0 R /Title (G. Email + registration / 2FA \(required for full alpha sign-off\))
>>
endobj
45 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 31 0 R /Prev 42 0 R /Title (Ending feature freeze)
>>
endobj
46 0 obj
<<
/Count 7 /Kids [ 22 0 R 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R ] /Type /Pages
>>
endobj
47 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 844
>>
stream
Gat=*997d\&AI`dk#B`n0HS8qj@HbV<Q1X_;,=kn`%5'QYmk+lrdF1<ON0[Y:^!(Irk=fr4FSDLo3qI0:&r$'0V'F2"+LEo#mYq\hu(mZ&\cTPJk/s>%SA0:*KM/00cM7,\J4gqiS_XsKOak-#`Mo`l'qb]b>"qYXH>K6Urik8"S4T>rX@gi)Wk0:_IKf#i"'<)K(sI-5d"T;lnj*Ajm4eu0Ao(1V!5mP\hr<@E$Z-:%[RHYSj[8$q/\>*LBsB-eO5[c:ckn3nN>,1Qn<dcR#rXV"k2F9.)K4hHEU,+Y0kTE+rNJs9lsIU)5S,2#fH!@L.iKdKFnDH%u/P^V`B]BRR9\6%ditCU:6Q(H\g3h??m&7^Wl*",t'\1\P3pNAP'+eHc-=Ra/,Tn:qNg]Z)H<[W4ZV/768.h8LT[>MXhhNha+p,/`o+J<d.%`eEu3+lOIS)@[lIBEk-f`/[?(&Rii/`#JImdo>'[e;hOs=^I>a_95b&a#<-gZWWpZ9R-UD!TgBS1mA7K-mr/Mj1-)%;_K\T+Ej,r[H_`#l(i?#?O9C;blBM>k/poP`b3IP@p8UsVo:CRj30TVVds4n4DYkBuS`hc]-QNQQ@"hWMY8&'Q]N];s\;r_)2sLl!rL0k_M,"F,%^/$i*mA!Toen\8*adk)Bmq][i\JK,H%NEWVsic5!rpP)IT"um[N!,a1srmc2MPp_@Vs7&,5YiJRoi*kT*9?NX\`=+jAd&IJ>`eu)[KRiGQYc4K[W1G[CDZ6SIilmEO@@cgc=K-]_4gY.#C>.:OABUNi451gX`4c&c$lkequaQMP+aF>pb@mFN[bH(h!cO;$\lP&")3qc2~>endstream
endobj
48 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 885
>>
stream
Gatn%>>s91&;B$;(&J:(+=uF![noV`.F[7=7p^kS:=RpZO^uo6f6?<ZL>+D:nTZBU@O:p9m+GliM!3dqX)%crkAW9<^gia5^f-Q9r#AJpHZsga[kc31?=m%A78^9;RABI:]$-m(:9k")1<H;c"BK(',irF#Hh<s.Ymc\$hGB2V=!N_mL&JQlXV"51dV+]q]*"H#LA@04qiZuKlK?PBl6k?W*6N_c]WXN$gQ_ad9oEts^KCjR)[*WJmENHIOE<5na;%=r1oM5#J-AuJX1]aP>N8fWq-HX7K8>\2/>=!hhcYWT,5srk/!T@'3DWVQQV&K+V3ch-im7D]SUQF("fI[_cr:b=1dZD\L<Jg()#eLlSjLZ,#G*-PXa32:+pQYY=U4h+]^(fY#2[:<!MZ`jkt&!Y\7*(/`[*]YE.q';$FP8=2hWKLN6Cd_<'p&q0Ff#[XnkCMXcdHR)SqjO<5qfC<(\WTV<H[H^r?e7/]WQ.$,S8G3i@pkALHggbTa#6f7S<3OcSu$S20l=bsk(&)WcZ#WkbBhh[+F0)nAF"aCTXgO#;mZAdMABI7f1^6k.EukZ?8GW._1+!G0rol#8.D$r'[1Dq^j!q'h]sI/<0UkkaaKZU&-da3!3(j"0u*0ciVOb&b&:M6#t.Z_uao=KATR(kRO5)[)[i$iGXnBg[<%Y@)?Afl)6/_%FVlkuc/L3`c,RoqHZeZQ%Kr!a`1.Lj1%eHN=:k\Q_=T=i;[JLR71*L_$L4:i%uu?P)A-l)_gA0Km01S$HXM]5T0FD/6!"%Q;>06#!hgRDHSR%1P8kn9![$[PcXb230Xt>A1Q&f!/<RA4=XdOi,B6QLdtc$-RNgH;Eb2UgUh\rSriJS<s1Lp1%ibnZ35er<HG4GUi~>endstream
endobj
49 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9$AT&MD_`PtL;ZBnUJQ&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:ps&&=X(PCH'Wj;^H^&o:u0B4;^:%sp:ZE&Wa6R#'r_sXjY5-C*Ut>641HTb(JK2s*r~>endstream
endobj
50 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`Sh(bM!;,fa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#A2MA-a(rHVR:D)FhU3a#MVn+@6p(6,SFQ!,JH2MH_7OtUjbJt$1U9TU8;~>endstream
endobj
51 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3696
>>
stream
Gb"/*>>s9K&q9"FFOe)b@"dqa[e@N\E5FU<N,:IQ>?%.+eefq$ib0SsjO\=2A6j%X`lUF3.7.:fN-fl^H[>PSDBoe`7nWegq>_TRE-<OY,f='pOt*E7\:MiBdJ"l"4&aZ4h:+h-$qhCI;)Rii8,%?XR$qE!-[\-Fb#5L3/829W5n_l69tI.B<YX#TkSZiK2N8m53-IucF-&gn58E^\._l-4&?f@H::NuF'#_!],PV7sDb*CDk_rQ\Ng#:7.O*pYfa_m9)3(,lD/Zbhi@/b0L[NBo"/e/O]$J3,\6:cP_@o49#e[\X$/FGV@R=T&'[h8b@j]:o(0MMY+j<,Qa\>p_LmO!uaj06jo'+>..ejK9b.7V[974Ila!(B%JmsO6]5i$eckK4AR%U5J7C-KU,+,su[=ff]MPRXUcqHa2FK8o'-7HGVG>(9db(eb:6+$jOOunt+FhuR$V.5*,LpGI6KHq?I)QK,LVKq6PX*=8B<W",M>Ik5%"<L'OCHI80!d*gI*^@$s=>r%8C8esh>sU;Y78lHJ/6t)+&5Y3L.cE-Q!W/<p+iflk9<o&*Oojud*@";PPL%(=4`c:@TF/rir*).cQa/"\5hRu?#oi.1FEORLY%f"C=4)?YU+)=*n=_Q_p"hG@WUlF">do;<btQs(%5hB"1Uhsf`<_F*K?V,7m7C>h5o93HLu0L5KWDtA=&JaF"?X%rV8q@qa8Ri4dDLQ8A`76A$]^,b]TV7i;6(?OAPFg?CZR]QYAa6NY[hqrf/I@_D?^B<J(N:<9VR'UBL/6g5/qef#Z9&j6u9V\79iHLpK^X4"Y9=F7bpX!I%&X\2M14BS\833lqE9]4.r&sMU=-U"=<6cnT)maHj\mk398Uq'&)=.9)7OA&!tW@_tCtc^65DI!^@dh+&?AZYL,[/Y1s!uXuf$_>.fcB(1^^)':;HGE6j``M*ff[mSCVn*&ekC<W5Fb2Poq)$BsX1]OKe3No=l>d'NP&OMATN0T3%!`/?^#C@&RE3c/>d;3=Aqpm/k;F4XCi"pLP=(.out_k;M:]du]K(8UP+n8Vh'C!Hn)*$'V/j:R.rGKZ5ZAa0?m\HmhQqqU,<6S/Z/$85cXoE`;\$=gB#EV%GMEZ54Y.59gFe'\dHaa"eB,?L3JB=F(_5CeL^$Yo'Ui^P=A(3aDbF"JNk0*tA6?$3m"i&s=mfVG3'?4VnU_&4M_7&S4NLrZ-5K?<a('_Q"Xc4Aia/Ao9.U\@\DN=EJAS=MB^E?dP&;E$SC2skKd!QEu&p;6MpA7oNT4TXtI[SFhu05,`*<[d].MWS:?#1!;k]"l/2i4Dj>$@Z3kGZ#D,G\0#q.BC+>c15qrjSEC??dSHL@I1/PV4AP(YhP4gi@3794D%FW$ms8dn;//E-V9A'f+qRXoJSpX(S!P)B*^7:4s"BZVGG;iWcU.kIicM]FdsZ4W?,\T2V81']TBZj8n',p$7@iTUQ:a/_?+&[I)TUo>@L+JBc)*`3R@C3modf9K']^'`+*OlNcGXD^Z4dl,JfTR<Kj*O3W&1L@I.3L.JtrmAWInVdiktW+P'kWQ&1bT/VMbtFU3JT+UkWV6hjalM04DZaT@'Oap:2m!sg2&"8J)k+C(iRi);gi+JCQ@:\atlJ][sVjT?@i<sJ.C)j"V]dl?C!Gf[jChhLYYL<qf[*SY=t0$)cRg^0L/clqmpa5jo3-5@jde'F4BY:_2\LY:d]=0p)ns'3FU0ZZkT3?*-B#R0c8*XJ*DI;H*G`Ft^96+7h1KG1EK]hXEN?)7W90q+sFW[tR<GtjT^$(PrT*F@t,be#o]<(51N$k-cA%RaU,p2eTaQ8sdP_+gNJRVt>_;s98e]E.N9AU-,D`\15S]2p]dpu/kEc(LG)L"j<uK@Gt8-1MPnDY8X*it0S?pOdW`4sV#mT5Q_@h9qj2l-CK.3\SS3)HWu98EaYR6aR>uc!lt34'T&p8O]C8JFooM:(Bib=PRoi,'R`!Z5Q-D[\"OpGKT.dP6I.7ibmbJIT[(_UrIM-TfRntX6G5]*K]/+[cb[!rA*:YrIm4KdZNk;V=<e"K6@5qTV7\\1C&Bo'*gfDK0JOJp\>I>ZFBpZ"_`Q,'QGSK[Jf[Q"nm"gnZU9ur=3hHgk#C<=X"*:(kO@:ji(#EP/LeD*.BbN(mJBF&9Q]NMM23.$/Yo".qg&0BKj?cp_QMAWZ,?maGPgg%e(J+\e$?@,5>W:?1H<C#2IS\oWMBBL1ZjDoQ35mQJ=!s2gL^q1P8W_j4k(8S1X0QnFZR#J3e0frG8)?WU1n!f0iAsXJd,la27DWmpP5=kFfM!e?QD9&D@p6>G-Cu&AnN+OBt7kWs6,:&onpKaDI0'>H^>1]nG0E%<?6)$.<V2Mb;-cSoL\U*:!?S4P_,'i_5>dmBh*dGH)e@9@_,'Mhc\m(#:C;+m>BC?9M@$roXr`5Z[.836:PWQ4Vp;4$_uM63+t'05q+sGkV(uX2sc_&Y91$Gb^1kq"S9VOWIYfSH]T#:1N:M(U+].?f:@\\Wi:GbVu][LrS3AeAeBXT+J3RiC%3D=A@Si6cB>l2I.qVGBMF])2I^=5o8iL@Hk?W$'p8+n!7\1!e'0\/:t`b)^MaQ[A#'I0nD-=E)g[j6!$[E0:nX[MFm=(#)"[m$toX&;8FOhIj=Rb5@f5&7r1`lPTQ4OWD2SP,NmYT;et6^q\"a4Zg%ftcP@<q8/'cr:up4)`P<#I5BW_eIVeqYT%>NnPslj65_5;Ld@OY5_o&kPiW(L6T4Jc28l^<(g&;1Gd0l_L+SGYUHc'#ieDt5:B4(QbDHjKCEKh5S*_..^q/0BOL0"^R@)(Eua##]Q<lr%(c\?I"2EWD4q?=4>$UP(7`:;.11Nb(a]UbSUP5?QBO!f9"_N2e/SDZE?OS;bnJp,c9#WtU8_O6T[]W`u^7YJ?;#4t[\ZblEj2MTGcX*<3N@\(1<r7G^%O]Rm^@7ZY[<jTl!X_h(,Aj%,JUc4:MDcsMd:d5FK,CPI1VO,n8dmE4X>[:Zp+m\qQH[WolqKS_`9+56jMDrKS,NWP!2qGR06GD(21>47s^X:[5!e%K+:A`D'/=M0f8aW(fZD\(aUpn5pa\[j$#IXNaRn_:S_cl-f+L+)>T+!.l8uWg(0&Br%S0\2h4!hi?/VMCFS[qJKbH%$m^%@P7bcgI.]q2p`QPp&#q8N4](MN@ROB;K`6B.M:<W=$ck\]A9lfOHQg+^%]ZPXEr[u*Ko-IY3!nEaVg&c03\O+C`Q!N8akZo4nO>H+%$c7J[`VDJ>9Ie^d-G0rR?7\m,YReAZqi&[ZP>=k5Z#Ji<n+0=[+YYt*-pOEuYh=maQg8h;rG8(!d5ad:pkWC14]j4'hKUXU[]0$oQC"tZ26lJl-keqb7=5)m`c8l$TK:OnZSOLIA2C7^SV-uq2#aMMbdJg`B^-W1q/t#A30Gi5Kc1GR?LE8C_S&b];^hR5ofP\c9"IP1Ocg<tRHKOK>7oYKSMrfi%5>U;61/hG[2t-Mg-Zb4Vpio8EkP_ntV;-Yd=JbrOD\M6:`.#0bZOV[0IBqHhV9Iq;(5d26hqoW2&Upuu1`1640a!.P&[%c#_[\*Nn1')qotr"bUD?$3<[//6\f2';hF9rI(5;$b'WT`YB!i&9>?^pt(C(9Op=BM-HK1W*W#3kGd/5FJRP["@d!'ic<"u)VL*2m$=3-i'8WoK23?nuoI(cH_8,ia*B39T~>endstream
endobj
52 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3406
>>
stream
Gb!l">Ar7U(4OT5+L"t`9cI62m[5Hcign<%BdV,!?NMbqMfpp%M^g##PPt0$.*8cr8f$Zu`>"6;Fsp7DjEJ[d%n7oDi8Sgl6KmbGNMdV%K'jB[gP1^UhP89+UX)H?)X7*QBjh7\*V^qbiO,C]reTZ5+pVNqfhPod9O)[k/L20:*$6&LCO3^Sg>AC/9\alNOQrqgb$NFPA:UoE%FVYS"6dVA3d,3Q35L[`9!:LCI"A$r)n&B.R]kY<+n3Wh9-KB"7MGY.]))Tc*D9SN_U"MhG$MrWb[=*;954WmRcJ'a2#9?B)+QMX)&4%K$#F1uU*h;7F-JmY#qBO-:J(BA#hYbLO"+<X`#"64o>f_\=_;&L)g_g+HTZG?+=+Qt(CrT?7"M<HYVQ]lO#ujMGTX_bFVk.pnG*VOF?bWuLa<aa%hVN&1qC.`,^=\9VS[\dL+VE'X:X7sXnI2S7b77?O`cGAFb5$@98He_B.OAJS0g@Z(=AZ7&Y._EO[!LBpmbrAPUqk':cFT!"Nu&&QK*_ieDB<'GhT;YT&)/mQ8`n`8c,=/Eh9_Cp-S6`auY'U#27=Eh%Rirf_,S^\R9L_k<@,H27QKi)Su!P82?EN'SS7\k)D9.#P3]&g:Zqj`3f./GZ1C4$LX/Fc!ZE4d#@*AUIkA=bRA?E%VLMS07mV=BG6rA3=!)X#74="(V\1G#uQlI1lal9:P]MU'+`J:NKX/t$:oY^TRVOmIiK=inSAH*p@mshNXQ.U(\WoH<i@kK@Pi]b[4rpglGg9ih!Hf]a)X_s)q$-NG!U2u"h%g6#?mT,YkK%Vd+WSlQ.P5M<]uAK1Po+S@)jbCLKo&ZfIkK*h3'4m^M[laIr2I;FptUC9cLGH0g#s_ZFP=4j4=B:!l>37J]HF#Lkn;[BF9SP"5U76MBeG2V5YWjkB/%P\E@gJ9R=`'NHku^-^J6c_^`l:A9\Rjm&cCG3mHQJ,@DSN#R&'=\p&tDmT@AU5gomf7VbP'dZ])+4]]+cUj43bTqEn-WQoOM.[]5lKPtPT2FQ(YSm\aMf-P+Vn1[!$<%Fa6Oft.DQ7N/&8<G<G#J(\2`rkKb)PbFIXb(\u]?Z%oUp"W0SrmHO4\sfn^YK_Ri*/h&mX`[`>RJ1Lh+8lLHK^\RU<YjKLj:J?>;21!/-NUiJI8_QdEL*pno0pfgWVdd'O3-NE2Er5e!qR6'o<^$9$"h:!rm&\,j%gR^Z7`s=hmXiXYr(m4(?pPi0co*`p*oQljjt5190>".f^g1/%_UN+7o<#ki6g?2:,f8+k16ZJK=W\jFhr&F0rCQ[6lre$DAHf'e(qGa-Ml-dqP9lpEQ8%G0q.dCS4&g'[><&QEn&Ch!X(`E=I"db/B/a/BP^CAF.(t-g:ah'J15):223]fA`8iO48RcBjEVkEafJ<&X>:Q&+&gU)MFo/]&RsMM$5sn7'LeW]H%g*gU[sAc?c&K/Cog`A)[C#7D6>ThkKE)odq0,1`XsOH@6/+88SQEq\J?!R^$*%eM=M+\VU6:;k6BshKcur:->9TW)p]`P8qCnVfDQ(;92dC_>1<@]<SR@kJIW2I1PO%+:#1/V\ulAM+YR&f:Eb*&a^1<(jguJ!$+?$:=(7Z\`gg)"<9VWF:VW#cK>#AW4$?kAmc]GrO8kfLq8e$pKB[bZV@H@\tJB!+0:nE8e^R4l/++2L3]Ffp)7i)0`(WfDa^t,iWWo@1-'p?gcpRCjD7HK=F'uT#@<bhh73U0g/QTfeg>aO5]A4"161r$,:epp075@uUh^M+HILRSMHPM!euZo7/'S89BJhj/hbXRfgEecd27I;L":5B,[`ImJ;0Q6Q?,Beo_+g]imaZls9+5:?Uoa`79gc"ns8PX=-j6"2M5nO_%T"g^#rCg=]Q8c\dm7Fb]gYhLS4Z"I`]GqNqaKC#/uLe]pIhES.g1a:M;F:W<j!#o5Tn&W6g\fV)Yr-EiD+<$.q^r7NBoQt9FtsQe*Ce"7'-]-q\g8+O0KBMb@HE=\Y^Rp^t\Xs#'WGNSZo?OR!V*>qnoBAmuU5_pT6PU=!$5-aAV48<Dst8FC1k,p=GsZ8ZrL*je5KCPMjOqh-=6$Agh+X"_'f;;5>W&Jn>?Y=k`3=Ng245$7a*EcV8eCZ5&YEGDPgLV[NX(.YNmeBNu?Se?Li,l+/U\"c_LM5-%Q^ZiZ@ZQTo_KbCr:J@D<Q+acgeW@Y1<h&$%1jFXAorVaZh+W`0!Q/([Js'o>$#N,elZ`@Mf4\H[JGNlKJ!-rUF4".T_n[Ea"ho:1Z5=ZZCHfk^SIg#OXfS,N;jjou]AVo.7%qSoomrr-lB57gPt9],>5e*j^6Li:lIZn8p`"8t=?cO#K(<E,j$UkP0)]S[[naA`K[fcG3Q]+PVnUg?XER0%5$au.9sithoM.3HW`NBIeW%etJG'7)mR0%FX\rlP\(WElh2_Eh*n:N8g1:agRG('::>L=V&"RY)=TG7q,W=c$X;"V>.Q^*j`_.:UWo7TUMuq([b0;#q7T99mIu_/!JIaT;p?VA[NaDNO]@->?.\A.0%cPNHX7IT%:Xo,!ZLOl3VIrQOKSb;T)EnWBmMGkiL5=:"E7EDc(k_6RFRS?:/9l,EGH#<:JadbkE,&o<AN69GtQq0F>G<osG!mk(i#R7o65C^9AUl+VTh)!((bQ7hWu^O,k=.5.kB!%dmYQpK&Z/30i/WaDVTAO0gsjt*-!>%?Es#Fo5TSQdD[S)\%aHY[F4H>1TjK)&`n6Np[J;3\@da7/K)g,5^h0\Z/rlaY)"IBetiN63P9$k+]1ij,:("Xl;bZ>%Ni\lLjDFu=ErFRM`/Ze<\!Dj2+$(maLI?@dtsWufVj/jRL4T<c/J:qj"a"Iq=,AT9J:Q,U$c6<3(pG'q32(XSqHq_#ss/4NApg^g!KWm#)+Zi0J"Gg43"RM+Qg'su;+gs#1/[o=f1MA[;lCbDG=cc-._:Hm6kQnRVZ2mPq1]=bkf]`$1Co(Hhh*o1.o?XE\;[s-EV[7VC&=lA'5>dJgqX9N?jp/-@q8Cm?2WZXc%4d@L+5n@E]HpB&^KafNJ.NZe;@u:-TM&?t)/tF&Y9i9:*dn'TZ.gkB*BU,t;jkD_jr]!'D030sb#5cDf$FC%@KEK-$bl4&7N*RY6p=qd7A>%h%&%Hmrrid"\brQB'%0nn?_@r"j`MlhIJmd@+Y,!AoEL0WXHl\Vt#PA-"XsM&MWYD6P^a`KC52K#R7D*$7(B[B?#;$"8?3(EqInjOZh\&\7<,:#+1nf%1JjRZ;Q-fkN`ghdSg&Ns,\1Z4PYc8Ga=[Of+XI$%,qsV_:Wh>41:eC7oF#(=XW[fb5^.ugkL[6nVel>\I;1@!]A8E5-4A]QEi'2?srU?sG@X'fNSr;=[gDF7:#2nU#'+J%`b;6@5$mq"n@-e]hDrdWIgARcqFS%fjZVoW~>endstream
endobj
53 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2946
>>
stream
Gb!#^D/\/g')nJ00u/3W[]qCKeS9'=\Z+V$025?bf``,l!nj;bD.QQpaa(F(^I.;*Z6EG7>9"DElS':f3A]a?o8H@.`ZHE(?Yf^+c!YK^J1**U!mr8&B%J`.Sc!Us;Ft@HaXY]SYkTl;L6%0orY)G0HnHZXcu_oYLgiWAW";j/rc02cL27>W."A*i%>Rpom3ssd,P2mHi?:pud]4'NE0,ms_I=3NSRMqu..99mJgDdN5XCPp'EpF'EU!D[9I'2DYUG6)R/Ml*H8=0KYP'=4BJa,%4a<7t@X@_U?gJa2lkWQ"75AC`[r-2`V%)L$Mk0M..>?O08D6&2g/TiOY%g2M^gh;e#jl;bh@!F&e$+,th&#)J^R1&L^H>62GIQ&SG^A!V6gM>#"nS)KC5YU?@<rqOgQ8Pa55(W51\Fj86\oCq]Da8](7#J``tl*2"X04j?!=uT'm.m@7\seYo`"gtL2"P7$MIppUDA#&Q$-=76^=sN,;30RJD-tE]=.Gg2)05E&([oD:`hU>U%$_+)>nZ%!t474q8^s*E9OX?TF`g'Mr<(Z/DF\GS`6En[dfl)I0V?LVI;lD>88r]gJR;mruAQ;lkk//3dnSq,C"BK<!3_5\Z2"()cP)%g\:fe&OlJf"M_PlSS[]?JS,%%"J.dF1Z*P3,q?6T<KXoaqeWg-!0:ZnD*%ZB3u^DD/hY0<pJ4jtn#d0C:XV7Fj,NGP\6%d4"SO`[[g65-gi2'kLg0,B])IlkV^+&Hak4nYouf\WSM/dA(pjUYlQ!>F?AjoJ0Hg/6LYT.6R#PNtQZ$R9lumPrNq&j:a9l*"RsfBL?V'iKD!lVVcb7e7HL.Ih"M`s25>S)K@VU.=b+I/)M58%IW"(S!d\>m?4oQ`\'mq2Im,mF.#s&#A(TDZ]MdKls8(e$2]YXf3Z3`'tKaA`1'?]0bP"d?^Q(/)n\U9M+iEPR=BY%^_F]lU;Y=k`a7&$h"0M*ln3b:[#+W`Epbg)Wqs%kglQP)uMBa>ap`<;<p*d](/CPs"V&[uYgot8`'HT\Dnr#&&Q<MQcY8KK@]Y(EGVKE.17/!dVr/ZR6T5R=WpTL&_`Z-95'lCj20g?I.p7_R@!-5R9@7QCHcLse:PrMqh-6oQCEPc\H&hT%IOc_Rim&ImcJK]5)j?288sr.mK%$8=)(Z[XO,.;?7W'kt80MQ8rk]!7l^PJ:bIWG;W).&UieUZ5X"gtf`RCLDu@q+f"98!J>Tm)Bbd(EBM4Ct0`DSc(^]1l?YL.lLF6RtjJra=>Gs26.@$@q:KO#fBIgY45^]5=0%Mil*'?#(bXkSLB+l+!?,5.Ja&]UOb+Jc7m2]!U8sIUZ9`Bb$V0nn^%d7?`m3e:#T]s79#+4Bb@u'["(&%C+n-po5F&BLX*@ZTY;'nf8fqqSH;e"BXFX=hDi#MKp5Y$">KY(JK:p)e@IaVSK2fo-rdTq.(&,5-jTL=glZf&0KHK32/pGl(gnDAm(%nVTlu5(?nZsIVK9C1\OSB6\P&9t38U2X4kcg9W5-,CobKaYm*Zimo5_bM31a$>VSCS=E03HfjplKM_Ac.g)G*hI%pGduY(h+CBmXO>+^YhDX%F%[:WYjS=@39"ME#,t:>mmr6<mW[_,2HB8&SJr^ek4!-b>`d"MVQsYhPa!)bF$(Hp5X6CKZ4kC6C3M<\5iK'hsiiJTB'ZAHajh9R+CX`!UZih[Y7"4_$s/=]9f?7#)'k\K9SfCg^<[;gl>V8c/uh!hTbH%HEZ',^2OH&s&r%&_hNPS"&/jn!NJClg4mp`uN?;nPi*>;J\)$l1JN1MC([;`$gNILjjV$$L*_.e42VdrF\'"rW]R@<-t*YJX5E/5W@0,M$-sG,N8Q&5^>C4M"\ha@=YA\'_^.s!4BrRo0*7U.iD=h;^4\JUhY5cn@"2`UJYqe95MuP_n8i`K17on`pR#7o#C*TIuH=pii.9YH.%:''%5G"HUf`lr1o6#/n3$S]6fp",AAUA^.Kq]mFP\G!phVZg5fAhk.T?AJ*nRF4hSdKS$*9UmI$=9!E[:G4%,_F^H;?cV@mklV2MQS6qMaS>pMFlVTugR3kOEGc?&sQ#ufTd_0PL>pGu_(ZBsdj)jf?pSW,he^"qT59Noe#iun<(#.4#6^TH:r0p4p.%sJYK:<ug&jK(M*)"1[=9YX<VgkF#p4??NShDnUgptT:m>oB\Y[W4AD&S40>i8fTG:>YO8f]#!I`aCG-mhZQ_"OrGl9Y^X:juMq:/G!^kMuSiCH.%H!Fm$mXr!k8oTD&T<`='VZ8==2_hF5)L)2*sf>U9(1_F#@rhD8`GDrKH#11s>_n5&2jdknB>4E5flY-=;5U'Dt#M"=F)8$((dM:Zje?[@`N#1J@6/<,#M)nNOZSbsjtBPig4nmOTmSI=#1k^oN27%H^24antA^K!YN>^Ug/r+phQn%=B^Vt7=o>1G1G<"5[i?`TV==Y__s"c/HP<5Lpia_2P6(BNLIG?fV$9?aC5CCn[Ndf7klF`o8Z'@cRfCCXrC$$6*62$&"kJd]T5r=[R"UQ7ho:GCsQnG-;*Unnu<4W*#R(ZnrR6[nFlQM&C[;18Zf)Fa@W[U?8\d&Q]a]+tQ![jVpsB"2#80gS9n'Bg'FhUk\+U5Eb+;t/j_p2kS.qn'4Tg?]H/CgA%2L/"\X8VVTY;dXd4^>euA-h`/_gYA>**Gp;2r[=d5abC21+o+0X1qLEu14$MOO(JpOn^HTA4T8h!s2KZV7C6m,/\[A[`@RfrNF*d1B3.mXn\t:g\+0*)Ho(V@4oY@S_%&0pPWH1:TCh+4VsfX7EBe.h"JO"*Q[e&X,D7T`=M,_%-^"65r`']iO$?8B+iN^6B's_NaF&R!Bm>B)R.)+XI-Wk3'OY+bWZ$]Jo[jNh(0`kNc]>!mm?1/&&CZ1#kDiJuSQh8>P0/B-\nXiJBF`E+P"s(p(u$iT!9W-!E:Pq+iDT=~>endstream
endobj
xref
0 54
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000512 00000 n
0000000659 00000 n
0000000806 00000 n
0000000953 00000 n
0000001099 00000 n
0000001247 00000 n
0000001395 00000 n
0000001543 00000 n
0000001690 00000 n
0000001838 00000 n
0000001986 00000 n
0000002133 00000 n
0000002279 00000 n
0000002427 00000 n
0000002543 00000 n
0000002621 00000 n
0000002727 00000 n
0000002811 00000 n
0000003017 00000 n
0000003330 00000 n
0000003536 00000 n
0000003742 00000 n
0000003948 00000 n
0000004154 00000 n
0000004360 00000 n
0000004447 00000 n
0000004758 00000 n
0000004833 00000 n
0000004943 00000 n
0000005075 00000 n
0000005210 00000 n
0000005376 00000 n
0000005498 00000 n
0000005619 00000 n
0000005773 00000 n
0000005898 00000 n
0000006023 00000 n
0000006154 00000 n
0000006333 00000 n
0000006478 00000 n
0000006626 00000 n
0000006729 00000 n
0000006832 00000 n
0000007767 00000 n
0000008743 00000 n
0000008973 00000 n
0000009201 00000 n
0000012989 00000 n
0000016487 00000 n
trailer
<<
/ID
[<b351513c4424db373b9ab2ba803fc31a><b351513c4424db373b9ab2ba803fc31a>]
% ReportLab generated PDF document -- digest (opensource)
/Info 30 0 R
/Root 29 0 R
/Size 54
>>
startxref
19525
%%EOF

108
ALPHA_SOAK.md Normal file
View File

@@ -0,0 +1,108 @@
# Alpha soak checklist (LAN demo)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-23 |
| Last modification date | 2026-05-23 |
| Co-authored | |
| Severity | medium |
| State | scheduled |
| Document type | draft |
<!-- doc-meta:end -->
\newpage
\newpage
---
Manual QA on **two devices** on the same WiFi. Goal: stable show path without experimental transports or stream protection.
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Before you start](#before-you-start)
- [Happy path (~15 min)](#happy-path-15-min)
- [Stress (optional, ~10 min)](#stress-optional-10-min)
- [Pass criteria](#pass-criteria)
- [If something fails](#if-something-fails)
- [Not in alpha soak scope](#not-in-alpha-soak-scope)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Before you start
| Setting | Sender | Receiver |
|---------|--------|----------|
| Transport | **UDP** | **UDP** (must match) |
| Stream protection | **None** | **None** |
| Video codec | **H.264 (AUTO)** | **H.264 (AUTO)** |
| Resolution (sender) | **720p** or **Auto** for first pass | — |
| Multi-receiver | **One** receiver selected | — |
PIN default: `1234` (change in settings if needed).
## Happy path (~15 min)
1. Install the same debug/release APK on both devices.
2. **Receiver:** Main → **Receive****Start listening** (notification shows *Listening*).
3. **Sender:** Main → **Send** → refresh discovery → pick receiver → enter PIN → approve screen capture.
4. Confirm sender notification: *Casting via UDP · WxH*.
5. Confirm video and audio on receiver for **≥ 5 minutes**.
6. Rotate sender once; confirm stream recovers or stop/restart cleanly.
7. Press **Stop** on sender notification; receiver returns to awaiting state.
## Stress (optional, ~10 min)
- Background sender app for 30s, return — stream continues or fails with clear status (no silent hang).
- Lock/unlock receiver once during cast.
- Second cast session without reinstall (stop → start again).
## Pass criteria
- No crash on either device during happy path.
- Video appears within **10s** of capture consent.
- Stop ends services and notifications on both sides.
## If something fails
| Symptom | Check |
|---------|--------|
| No video | Transport match; receiver listening first; open receiver UI once for surface attach |
| Auth failed | PIN; same WiFi; firewall on LAN |
| *Screen capture data missing* | Reinstall APK; Android 13+ consent intent |
| Choppy only on UDP | Retry with **TCP** on both sides (fallback test) |
## Not in alpha soak scope
- QUIC / WebRTC / USB-tether transports
- FEC / NACK stream protection
- 1:N multi-receiver (unless explicitly testing)
- OTA / crash upload (backend track) — except **RSSH remote access**, which is **full alpha essential** (separate checklist in [ALPHA.md](ALPHA.md) § F)

252
ALPHA_SOAK.pdf Normal file
View File

@@ -0,0 +1,252 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 10 0 R /F4 11 0 R /F5 12 0 R /F6 13 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 762.0394 102.157 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 751.0394 122.829 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 740.0394 137.053 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 729.0394 89.69304 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 718.0394 105.709 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 707.0394 131.061 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
11 0 obj
<<
/BaseFont /ZapfDingbats /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
12 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/BaseFont /Symbol /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 28 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R ] /Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 28 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
16 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 28 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 28 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 28 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Outlines 21 0 R /PageMode /UseNone /Pages 28 0 R /Type /Catalog
>>
endobj
20 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False
>>
endobj
21 0 obj
<<
/Count 6 /First 22 0 R /Last 27 0 R /Type /Outlines
>>
endobj
22 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 23 0 R /Parent 21 0 R /Title (Before you start)
>>
endobj
23 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 24 0 R /Parent 21 0 R /Prev 22 0 R /Title (Happy path \(~15 min\))
>>
endobj
24 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 25 0 R /Parent 21 0 R /Prev 23 0 R /Title (Stress \(optional, ~10 min\))
>>
endobj
25 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 26 0 R /Parent 21 0 R /Prev 24 0 R /Title (Pass criteria)
>>
endobj
26 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 27 0 R /Parent 21 0 R /Prev 25 0 R /Title (If something fails)
>>
endobj
27 0 obj
<<
/Dest [ 14 0 R /Fit ] /Parent 21 0 R /Prev 26 0 R /Title (Not in alpha soak scope)
>>
endobj
28 0 obj
<<
/Count 5 /Kids [ 14 0 R 15 0 R 16 0 R 17 0 R 18 0 R ] /Type /Pages
>>
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 840
>>
stream
Gat=*?#Q2d'Re<2\;t@#\A%rq8+t>)>I:LAg/FgS"_XM<Td-@3s8<?TiF</pKG"[IkM5+XCO?_mIB`KR`W5IT\@G$n#6"s+&-;bM55LI-'7?8u,Z0St!jpO3E`ZA5*@B9E>]LtB<E)%FZt-=)8:ODd-Q^0=n7*Dh!QmM-@sJ(8S=m<($@W7ZDtCUO@$_!lY[![ZdjEd[n1^D[h]Oaqno/^TrZh3i0P^`lc9(ond]/DM8uncO.YN!"f#ih3W5W#Mon2BIF-tg]8IXJRa(Okmik+rM`*oXD6\<g@3P(;KT:d1iU,>:mFr]tXF.:(W[*1.2aHUg56>BnND)q3cSAXQi3M[c3cu)Jj^!,i'+kE!*eJMV4R?rsiU78VO-k)Ui0A-<&`-1^h<mm_CSQi0X#=W78LU;r_k'SML6m=%Z1!Dk7c/,gplf^IM4-i4(*W`Med+`4!,jhk&&_,Pdfn-[?[__C^m*D:dE;TF[i4_-'d4S&"Q\CLY`X\toYRU&"'4b,nKL+2Gg(Sdf#@Ndq)3m"t4*7O>2Tu:#0oqMP>"-UX$Q'qaf)im0j-*VBA@LfC<i'\`mfi3s.p85UP=F6kj`n\R@hV?/\AKI0m*HsB[A1WrGIAIspR;ZQXh-ql5:"F#Cpk3lYKo@2&*=DO@$R;@rF18;'B1\tUocpdmJSD:VSM*LD')ig;?".X7573r"16HMCD1J^c4K*f6@FYiA9gaC+/0LmD1LCKUDjbM+]JQ;.YMIX7"sP."L%')asmuOoO4Y<h!q4GleT,J9VEX;*A#Med/m/qBXb)Odj=;H`iLJ<jI2`O5IjH#M<GXbj:pPWA0V[ondg\fFB/~>endstream
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 535
>>
stream
Gatn#d;%Di'Sc)J'`V`*L8lJ&QFshoX`UX\)>.QKW1MF^8M!Y8<dN5-TBq6:[=ObZ";]Y1qS(kM?ij,^L24I+/h3Au&kJ[!Jo[0P]#LX_np<]Yb$;5=9:eL"`HYGGl+Q_VE@ir6JRrE/Pm1/bPW?S;:"#I%KHZ@jRi(/#&>COG.s&>-E(e]`NOFi*%+R"1ms=^%[#XYarW[h[`tS*,O9YQ8>rlooi-#CRpc&>HQ`#FBRBCm%BAs8Vkp&c9Rs&d066XgU;RhS?L61o-MiDMsjujbaUW/1I[g^,NOh"%%[;Vf4N>Y$Zi`,!b9*D`3ERhF?=INhXAcfM*(&k7lBNgct[k^OnX:]4!g".7%Z=KB_e2nX'd$j2d6'@J&b(B,1Ij)qp?8I3P1jOSG*u29V[JbX/p&&Ruj6#+_$et[!63r9Ql[F/aC"d^F^%:PRder,Or\X?*qLH(2&Rj:6cM)<WQd`%P[.VT;^g;\*.OS*tT'q$ZVb^F#ip`<2-\BPV,G!O2,a1WBLbfc*L]!3ca^8-/-@Z~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhM``PkDdF-fp`Q&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:emFgZ@onCPq82Ws8G-_Tt6^R9oWtig34W3;D"0MP=sG_Q,?.RGJoD!mbh^0(JJ'S*<~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`SgmmFhnNfa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B](9((,pZH62[FH7Fl==&J^W3%Z>&B5W+giG*2MH_7OtUjbJt$1U9Q2!n~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2647
>>
stream
Gau0F8UK2M&A@sBbY`?nKE*sQ4t1AJ]5\fDEc]V3VB?7(7Bg%VplaOk!B3j`7>c7Q`K<`B"5qGtAnVMUIVAGi[flI_JWHEP:^3+D:lAF0\V*0o8,jXX@]!]MB4r5"\0Hb__fG:XURRefL,O%0$3)'?A6LS)L^fOo:d]_>AkNX,MFR;r*b"Q'#:]E+Adg/>G;/gt-aA'I+F.!A'0i9?iaS^.I%Vi7$NVth$MZ63;(E_?<uV;.:K@-R9*0Smm[lb7n-:&S/CZJ/4B]J+9GT*P5l;$Z,aT]GE-"UhK/ch-&qc)2R7QfnKTSl[QUhjQCmiDM`p]c2,"@<i62+c#&AtlWjpA2K=tk\X-<S3.S39NTPh7=jRjU0_if'DTSd@:m3fNHo?kS+&cCP=HTd.!Y$/6c'4Z&WU0Y<dX@;M0C>Joi>QH$5r\uC)jEto"+;5r,nPVfBLEA04GNb-em,0#_4[L'kl3JKTuinA3Z;&"Emi+%#PZ:BRFk.h4:!j4@E7ZmOG9QBE);b/7\K$EcD+e,m^2+KgjinQH&G^&&0/;>it1Z)YG!Q5,=#RR=72I_<i<[D.<H01Yg&fEeiC51FP0jJ5<96;j$;57[N(_LL[[tj$Ki<f<i96OTkN^:oLj_it],cr^[.3h_P;OrKES`g"HlZSXfZ.m\rV>n*lmT5:JN\Z.L@%#"@/:$5,VXKri"cFFZd+r;uDF^_U,&9_TWBdgXp""/7%1bnfD`JI<4>ddY3t@h"YO2(S0ZQLsNpe@Qp?AhMjSrEbo)*Y:2Ral2I]?*Fji:MgfX!nu;RoVJ#B6(CDL)<RM2Lrd\&N-V2J6ITK"6ie<C.bImJ%_4_NZd!T>_g#mq_29njBcG1@DHY6c&\$GXr:?E]&Z,O$54/*=VItE;Brlr])/ik#sZsc0sCOO-"WNQS`-]b,T^J_d,0.eF+>l[uJqNg5OisOCQcEn>[Hn*`3llX)\3n6Yuh,aoT/T%H,!A!3&5#!3bqoC7Dahp"l^,C2^!.jIkhtlCo-0Bk#AJ7>W`SROP(C,-<LF**dns?X.#n1$<5P-`&'AKfr:F>+9*DFPn`qn0gO+BOr&_M*]E1&7Qtudrug)!IHY>g2@p]4m-8KP1M/3_(Tp$V%!'NWn!8"X2;*gfQ4.&M^;!R,!`m/ql>Ti-6CUUNtJr@Ns1KG^Z^"16/fP8Eif]ZY1[0#N,7(&,mcZd<7t>*X2<<<389\N:.(#n+Fd>dlNQ21;?V@b-R't<n!_3!.Znf:nLR&l8i;@`bB!H!2k/9==^S8T/Za"CrI5umV8!&TOdJW_7Z3M9VpI,(q?RD(pU&'n4TpQ`_$>+Ml1$I=H\>UYfSbCg^uJjd7\YgTD-$81"!-5BE2E]4)b\9*&C`<$"HLfQX90RBO;sA3_j"TSo_mtKEL35!XM6(8X.B41#V?R!^se]N3rW)nRb(,2-=ReXNk%>d"a_"ddq#NDeE(p%m16isi)V-d&LHipnAHc]p-KXB7>&Y1k$59f</=sBlXk[&d5^W&]'PU-r^q,9>iV";FrXH>VVCBd[;A7..of">/NS\26!ZZqR@.'V+m<kPCH-&i79^td`?Cg\3`)O,&GP)"05>T'Vj;,+-lK)`7P.E8lbY%;4Zu7a7JeY:6=8%If/2L]"Y?=gAuTs\!^nCNRGR+3qJk)ebd8F&R&+eg/0L6B7NKi<V,ME=S2PQ;HCRXi4ag!+?e(pJ[U,>tOk-Le+f!4]D"G'UhP#9=Wd6sa2NDq./=X55O9Lia64_>K:JB&^NB_H#.p\1RBOA>pNO9gPOi.YPH&%XnpU,=[;RddgUkt`N":_c+'=q)&Mi*(/34_+jFru.?O$.J0gJHb9^dZYlF+]f`D[EAIrH;RT-;3_bk[@bUcQ#!L`D=N>9_l"39eXYFE0d6`_(VsiEf=Ys6Bk4!l5Sn=jq+Sid&9+i6)1jNc.k>eD*;g<^#r+*;fJY*HIKJFg1t_Q\$'#s^:L"T*o(me=b_c:2T!a57NB>1,Pl4"]Vk`lBB.@i#6osBFg[iQJ&='$g89UJh@-PaKW6fM4(+tZ6;\tSpD8+L_;&12C_3MlSDuc(,huV:Bd7bjU??8eg='?-E&_)b((-sD0/[P*X#HKTS"JfA`^W)Vb_FO,Ed[PW%FX:K[JBu;4Q4AH0XkNifoACkNbk,]a4FZn@3!K8-"lGT^Qmat,h>s]o@o<I*QtdCd^`'qL"7C7HO6uUE(efGDK[UM7=p5,7bfoU^6>n*[j,[F!\);4&U?Y$#[!N@4,N5'D+JHEIqWt@6X'BAO=N%&p?QQdqY&`7`^[KOoG4Hb@LJ1_7-9pD#A\dJc7E-n3&')n.;bEerG]S-n+%hWgbDp=B6^6J`+8b>74[u>\\C+VO76F;geRJppNN5n,U-`;X1r!)YS6<O:>]h7TE!:(?]ECGD#^>"LCG*GCS@CK=0PqVrIDV7`JLbm`6ZesSkRMO[1c,;,.2?HNpPR[d@J@-Ru@rES6VtcYP8MbV4ALWU=PG#1jt_BHfCqN_IT_GG^"@l8T0GV!r%dYDK=V.n@Xk>!p!Bj5#3npk!k2g@FUOshsN8>,34mS4a<DrkbP7rH$jmc4siqX/@/0\42<&C7B,Ugk4aacA4rSB(*o[[T5D/m%ZsdIS(Nr81F*2`!bJR;"%W@l&4;k2~>endstream
endobj
xref
0 34
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000512 00000 n
0000000659 00000 n
0000000806 00000 n
0000000954 00000 n
0000001101 00000 n
0000001248 00000 n
0000001364 00000 n
0000001448 00000 n
0000001554 00000 n
0000001632 00000 n
0000001838 00000 n
0000002092 00000 n
0000002298 00000 n
0000002504 00000 n
0000002710 00000 n
0000002797 00000 n
0000003105 00000 n
0000003179 00000 n
0000003277 00000 n
0000003394 00000 n
0000003517 00000 n
0000003625 00000 n
0000003738 00000 n
0000003843 00000 n
0000003932 00000 n
0000004863 00000 n
0000005489 00000 n
0000005719 00000 n
0000005947 00000 n
trailer
<<
/ID
[<62ff1d8c69f4e2aa7b4e34c69a669dbd><62ff1d8c69f4e2aa7b4e34c69a669dbd>]
% ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R
/Root 19 0 R
/Size 34
>>
startxref
8686
%%EOF

365
AV_QUALITY_QA_SESSION.md Normal file
View File

@@ -0,0 +1,365 @@
# AV quality enhancement — Q&A session notes
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-21 |
| Last modification date | 2026-05-21 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | internal |
<!-- doc-meta:end -->
\newpage
\newpage
---
Research and recommendations for **movie cast** vs **video cast** quality enhancement in the Android Cast project. Combines user requirements, comparison tables, algorithm choices, placement (sender vs receiver), and fit with the current codebase.
**Status:** Receiver experimental presets in **Developer settings** — audio PCM chain + video GLES post-decode (immersive / HDR shaders).
**Related code:** `app/.../receiver/av/*`, `ReceiverVideoGlRenderer`, `AudioDecoder`, `VideoDecoder` / `LibvpxVideoDecoder`, `ReceiverPlaybackActivity`, `DeveloperSettingsActivity`.
---
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [0. Receiver A/V presets (product, May 2026)](#0-receiver-av-presets-product-may-2026)
- [1. User requirements (preliminary conditions)](#1-user-requirements-preliminary-conditions)
- [2. Mode overview](#2-mode-overview)
- [3. Video — method comparison](#3-video-method-comparison)
- [4. Audio — method comparison](#4-audio-method-comparison)
- [5. Single vs 2-frame vs stream — decision matrix](#5-single-vs-2-frame-vs-stream-decision-matrix)
- [6. PTS/DTS reassembly (2.3)](#6-ptsdts-reassembly-23)
- [7. Pipeline placement: sender (2.1) vs receiver (2.2)](#7-pipeline-placement-sender-21-vs-receiver-22)
- [User question](#user-question)
- [Assessment](#assessment)
- [Preference (consensus from session)](#preference-consensus-from-session)
- [8. Named algorithm pipelines (best practices)](#8-named-algorithm-pipelines-best-practices)
- [A. Receiver — live video cast (default)](#a-receiver-live-video-cast-default)
- [B. Receiver — movie cast (+1 frame delay)](#b-receiver-movie-cast-1-frame-delay)
- [C. Sender — optional compression prep only](#c-sender-optional-compression-prep-only)
- [D. Avoid for realtime in this project](#d-avoid-for-realtime-in-this-project)
- [9. Ranked preferences (quality benefit vs implementation in android cast)](#9-ranked-preferences-quality-benefit-vs-implementation-in-android-cast)
- [10. Multithreading pipeline (target architecture)](#10-multithreading-pipeline-target-architecture)
- [11. Short answers to numbered conditions](#11-short-answers-to-numbered-conditions)
- [12. Bottom line](#12-bottom-line)
- [13. Other work in same project session (context only)](#13-other-work-in-same-project-session-context-only)
- [14. Open decisions when implementation starts](#14-open-decisions-when-implementation-starts)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 0. Receiver A/V presets (product, May 2026)
Developer-only combo in **Developer settings** (not the main cast drawer):
| Video | Audio | Pipeline today |
|-------|-------|----------------|
| **Default** — decode→Surface passthrough | **Default** — decode→AudioTrack passthrough | Hook only; no DSP / no GLES |
| **Immersive** — GLES edge-aware denoise + mild sharpen | **Bass boost** — shelf below ~150 Hz, gentle treble cut | PCM + GLES; **intensity 010** |
| **HDR** — GLES Reinhard-lite + saturation | **Immersive** — clarity + noise gate + limiter (no ML v1) | RNNoise-class deferred; see §8A |
| | **Dolby 5.1 / 7.1** — crossfeed + short delays for stereo headsets | Spatialization stub; **intensity 010** |
**Immersive audio (this pass):** clearer, wider perceived loudness without extra hiss — high-pass (~90 Hz), envelope noise gate, mild presence lift, soft ceiling. Not “cinema reverb”; aim for intelligibility on cast compression artifacts. Scale all stages by **intensity/10**.
**Intensity:** 0 = bypass effect (passthrough samples); 10 = maximum for that preset.
**Integration:** `ReceiverAvRuntime``AudioDecoder` after AAC decode; video decoder → intermediate OES `SurfaceTexture` → GLES shader → `TextureView` (`ReceiverVideoGlHub`) when preset is immersive/HDR; default preset uses direct decode→`TextureView` Surface.
---
## 1. User requirements (preliminary conditions)
1. Content is either **movie cast** or **video cast** (live screen/camera); **both** must be supported with different tuning.
2. **Both** video and audio need quality enhancement.
3. Two processing shapes:
- **2.1 Temporal (N=2 frames/windows):** small gap between sequential video frames → predict/enhance using deltas between frames; same idea for audio (overlapping buffers).
- **2.2 Single frame/chunk:** static image from sink/source; one video frame; one audio PCM chunk.
4. **2.3 Reassembly:** output stream must keep **original PTS/DTS** (or fixed delay with uniform compensation); avoid changing frame/sample count on live paths unless explicitly designed.
**Comparison dimensions requested:** implementation details, difficulty, realtime on modern ARM (GPU Android/Linux where applicable), input vs output quality, single vs 2-frame vs stream (with delay), multithreading.
---
## 2. Mode overview
| Mode | Video input | Audio input | Typical delay | Best when |
|------|-------------|-------------|---------------|-----------|
| **2.2 Single** | One buffer / frame | 1040 ms PCM chunk | **01 frame** (~033 ms @ 30 fps) | Live **video cast**, UI, games |
| **2.1 Two-frame (N=2)** | Pair with small temporal gap | Overlapping windows | **~1 frame** (+ overlap) | Motion; mild temporal help |
| **Stream (38)** | Short FIFO | Ring + STFT state | **100400 ms+** | **Movie cast**, VOD-like |
| **Heavy stream ML** | Long context | Streaming neural | **200 ms2 s+** | Offline / non-live only |
**Quality (typical):** stream/temporal > 2-frame > single frame
**Latency (best first):** single frame > 2-frame > stream
**Policy split:**
| Cast type | Video | Audio |
|-----------|-------|-------|
| **Video cast (live)** | 2.2 default; optional light 2.1 (+1 frame max) | 2.2 (**RNNoise** + limiter) |
| **Movie cast** | 2.1 or 34 frame **TNR** / **MCTF-lite** | 2-window spectral or **DeepFilterNet2** streaming |
---
## 3. Video — method comparison
| Method | Implementation | Difficulty | ARM @ 720p30 | ARM @ 1080p30 | GPU (Android/Linux) | Quality vs input | PTS/DTS | Multithread |
|--------|----------------|------------|--------------|---------------|---------------------|------------------|---------|-------------|
| **Single: bilateral / guided filter denoise** | GLES/Vulkan on YUV; NEON fallback | Low | Yes | Yes (tuned) | **High** | Smallmedium | Trivial | Yes (tiles) |
| **Single: tone / CLAHE-lite** | LUT + luma histogram tiles | Lowmed | Yes | Yes | Medium | Medium in flat scenes | Trivial | Yes |
| **Single: light SR (ESPCN-class tiny CNN)** | TFLite/NCNN → NNAPI/GPU | Medium | Maybe | Tight | High | Medium (edges/text) | Trivial | Yes (async infer) |
| **Single: heavy SR (Real-ESRGAN, SwinIR)** | Large CNN | High | No | No | BW-bound | High but slow | Trivial | Limited |
| **2-frame: DIS / Farneback flow + warp blend** | Flow on downscaled luma → warp prev | Medium | Yes | Maybe | Medhigh | Medium | **+1 frame** | Yes |
| **2-frame: residual / delta enhance** | `enhanced = f(curr) + α·(curr warped_prev)` | Medium | Yes | Maybe | Medium | Medhigh on repetitive motion | **+1 frame** | Yes |
| **2-frame: RIFE-lite / FILM-small** | Interpolate between frames | Medhigh | Borderline 720p | Unlikely 1080p30 | High if custom | High for large gaps | Timing care | Yes |
| **Stream: IIR TNR / motion-mask accumulate** | 34 frame buffer | Medium | Yes | Maybe | High | High static; trail risk | **+23 frames** | Yes |
| **Stream: ML temporal (VRT/RVRT-class)** | Stateful temporal CNN | Very high | No live | No | Medium | Very high | Buffer delay | Some |
---
## 4. Audio — method comparison
| Method | Implementation | Difficulty | ARM realtime 48 kHz | GPU | Quality vs input | PTS/DTS | Multithread |
|--------|----------------|------------|---------------------|-----|------------------|---------|-------------|
| **Single: DC block + EQ + limiter** | Biquads per chunk | Low | Yes | Low | Small | Preserve samples | Yes |
| **Single: RNNoise / DeepFilterNet (10 ms)** | TFLite/ONNX streaming | Medium | Yes mono; maybe stereo | NNAPI | High for noise | +1020 ms | Yes (infer thread) |
| **2-window: Wiener / min-stats STFT** | 20 ms, 50% hop | Medium | Yes | Low | Medhigh | +1 hop (~10 ms) | Yes |
| **2-window: PLC** | Predict from last 2 frames | Lowmed | Yes | Low | High only on loss | Keep clock | Yes |
| **Stream: multiband + AGC** | Ring buffer envelopes | Lowmed | Yes | Low | Medium intelligibility | 530 ms | Yes |
| **Stream: DeepFilterNet2** | Stateful STFT+GRU | Medhigh | Yes modern ARM | NNAPI | High | 2040 ms | Yes |
---
## 5. Single vs 2-frame vs stream — decision matrix
| Criterion | Single (2.2) | 2-frame (2.1) | Stream (38) |
|-----------|--------------|---------------|--------------|
| Implementation complexity | Lowest | Medium | Highest |
| Encoder integration | Easiest | +1 frame pipeline | Delay queue |
| ARM realtime 720p30 | **Best** | Good | Good (TNR); poor heavy ML |
| ARM realtime 1080p30 | Good | Marginal | Marginal |
| GPU fit | **Best** | Good (warp) | Good (TNR) |
| Quality gain (video) | Lowmedium | Medium + motion | High static / medium action |
| Quality gain (audio) | Medium | Medhigh | High steady noise |
| Artifact risk | Banding, oversharpen | Ghosting, warp error | Trails, smear |
| PTS/DTS | Trivial | Fixed +1 frame delay | Uniform delay all outputs |
| Multithread | Capture ∥ GPU ∥ encode | Flow ∥ enhance | Buffer thread + pool |
| **Movie cast** | OK quick polish | **Recommended** | **Recommended** if delay OK |
| **Video cast** | **Recommended** | Optional 1-frame | **Not recommended** live |
---
## 6. PTS/DTS reassembly (2.3)
| Approach | Description | Modes |
|----------|-------------|-------|
| **In-place payload replace** | Same PTS/DTS, duration; swap enhanced payload | Single |
| **Delay compensation** | Queue enhanced frames; release with **original PTS** | 2-frame, stream |
| **Composition timestamp** | `presentationTimeUs` from source → enhanced buffer (MediaCodec) | All on Android |
| **Audio** | Keep **sample count** constant → PTS unchanged | Prefer for live |
**Rule:** Do not change frame/sample count on live cast unless explicit insert/drop policy exists.
---
## 7. Pipeline placement: sender (2.1) vs receiver (2.2)
### User question
- **2.1 Sender:** enhancement between capture (camera/screen) and encoder.
- **2.2 Receiver:** enhancement between decoded stream and renderer.
- **User intuition:** 2.1 has limited value because encoder lossy output destroys pre-enhance detail; increases sender load.
### Assessment
| Claim | Verdict |
|-------|---------|
| Encoder “kills” sharpen/SR/local contrast pushed pre-encode | **True** |
| 2.1 is pointless | **False** for **denoise-before-encode** (helps bitrate/artifacts at same rate) |
| 2.1 burns sender CPU for little gain | **Often true** on thermally limited phones |
### Preference (consensus from session)
| Placement | Role |
|-----------|------|
| **Receiver (2.2)** | **Primary perceived quality** — deblock, denoise, mild sharpen; optional 2-frame for movie |
| **Sender (2.1)** | **Optional compression prep only** (bilateral/NLMeans denoise), not main beauty pipeline |
| **Both** | Receiver polishes; sender light or off on live video cast |
**Current app hooks:**
| Pipeline | Integration point | Difficulty (1=easy, 5=hard) |
|----------|-------------------|------------------------------|
| **2.2 Video receiver** | After `VideoDecoder` / `LibvpxCapableVideoDecoder`, before `TextureView` | **34** (new GLES: decode → FBO → display) |
| **2.2 Audio receiver** | `AudioDecoder` PCM before `AudioTrack` | **2** |
| **2.1 Video sender** | Between capture and `VideoEncoder.prepare()` Surface | **45** (intermediate EGL Surface, rotation) |
| **2.1 Audio sender** | Before `AudioEncoder` | **23** |
| **2-frame video** | Ring buffer +1 frame delay | **+1** on above |
**Recommended implementation fork:** decoded-buffer → **GLES** → Surface (consistent for MediaCodec and libvpx VP9), not only `TextureView` overlay.
---
## 8. Named algorithm pipelines (best practices)
### A. Receiver — live video cast (default)
**Video (2.2, GPU-first):**
1. Post-decode **deblocking filter** (lite H.264-style / mild dering; VP9 loop filter already in decode)
2. **Guided filter** or **bilateral filter** (edge-preserving denoise) on luma
3. Mild **unsharp mask** (limit on blocky regions)
4. Optional: **gamma** / **BT.1886** + **CLAHE-lite**
**Audio (2.2):**
1. **RNNoise** (10 ms frames)
2. **Limiter** + high-pass (DC removal)
3. Optional: **SpeexDSP** `preprocess` (AGC, mild denoise)
### B. Receiver — movie cast (+1 frame delay)
**Video (2.1):**
1. **DIS** or **Farneback** optical flow (downscaled)
2. **MCTF-lite** or **IIR temporal denoise** on stable regions
3. Spatial chain from A (guided/bilateral + mild unsharp)
**Audio (2.1):**
1. **STFT overlap-add** + **Wiener filter**, or **DeepFilterNet2** (streaming)
2. **Multiband compressor**
### C. Sender — optional compression prep only
**Video:** **Bilateral** or fast **NLMeans** only when bitrate-starved — **no** Real-ESRGAN / strong sharpen pre-encode.
**Audio:** **SpeexDSP preprocess** or light **RNNoise** before AAC.
### D. Avoid for realtime in this project
| Algorithm | Reason |
|-----------|--------|
| **Real-ESRGAN**, **SwinIR**, **BasicVSR++** | Too heavy ARM @ 720p1080p30 live |
| **RIFE** / **FILM** @ 1080p30 full rate | Borderline; movie-only, lower rate |
| **BM3D**, **VRT/RVRT** | Offline-tier |
| Heavy **2.1 sender SR** before VP9 | Encoder erases; thermals |
---
## 9. Ranked preferences (quality benefit vs implementation in android cast)
Descending order — build in this sequence:
| Rank | Choice | Placement | Core algorithms | Quality benefit | Impl difficulty (this app) |
|------|--------|-----------|-----------------|-----------------|---------------------------|
| **1** | Receiver video **2.2** (GPU) | 2.2 | Guided/bilateral + deblock + mild unsharp | High vs blocky stream | **34** |
| **2** | Receiver audio **2.2** | 2.2 | RNNoise + limiter (+ SpeexDSP) | High for noise | **2** |
| **3** | Receiver video **2.1** (movie) | 2.2 + 1f delay | DIS/Farneback + MCTF-lite / IIR TNR + spatial | Higher static/grain | **4** |
| **4** | Receiver audio **2.1** (movie) | 2.2 | DeepFilterNet2 or Wiener STFT | Mediumhigh | **3** |
| **5** | Sender audio light prep | 2.1 | SpeexDSP / RNNoise → AAC | Lowmedium | **23** |
| **6** | Sender video denoise only | 2.1 | Bilateral / fast NLMeans (not SR) | Lowmedium (bitrate) | **45** |
| **7** | Sender video **2.1** beauty | 2.1 | Flow + enhance pre-encode | Low (washed by codec) | **5** |
| **8** | Heavy SR either side | either | Real-ESRGAN class | High offline only | **5**, not realtime |
---
## 10. Multithreading pipeline (target architecture)
```
[Capture / Network decode] → queue → [Enhance worker(s)] → queue → [Encode / Render]
N=2: previous frame
Stream: ring buffer
```
| Stage | Threads | Notes |
|-------|---------|-------|
| Capture / decode callback | 1 | Non-blocking |
| Enhance (GPU) | 12 | Double-buffered FBO / AHardwareBuffer |
| CPU fallback (NEON) | 24 | Tile-based |
| ML audio | 1 | Async from video |
| Encode / display | 1 | MediaCodec async; TextureView main |
**Golden rule:** max **one frame** enhancement queue for **video cast**; **24 frames** for **movie cast**.
Receiver already has `videoDecodeThread` in `ReceiverCastService` — natural host for GL enhance stage.
---
## 11. Short answers to numbered conditions
| # | Requirement | Preference |
|---|-------------|------------|
| 1 | Movie or video cast | Different latency policies; same codebase, different presets |
| 2 | Process both A+V | Shared audio chain; video policy switches by cast mode |
| 2.1.1 | 2-frame video deltas | **DIS/Farneback + residual** — not full RIFE on ARM live |
| 2.1.2 | 2-frame audio | Overlap-add STFT or streaming **DeepFilterNet** |
| 2.2.1 | Single image video | **GPU bilateral/guided + tone**; tiny SR @ 720p only if ever |
| 2.2.2 | Single chunk audio | **RNNoise + limiter** |
| 2.3 | PTS/DTS reassembly | In-place or fixed-delay queue; no timeline shift on live |
---
## 12. Bottom line
| Goal | Best option |
|------|-------------|
| Lowest latency + ARM-safe | **Single frame receiver (2.2)** + GPU shaders |
| Best quality per ms delay | **2-frame receiver (2.1)** + 2-window/stream audio |
| Best absolute quality (movie) | Small **stream buffer (34)** video TNR + streaming audio ML |
| Avoid realtime cast | Heavy per-frame SR, RIFE @ 1080p30, deep temporal video CNN |
| **Placement** | **Receiver-first**; sender **denoise-only** at most |
**Users encoder observation is correct for “beauty” enhancement pre-encode;** denoise-before-encode remains the only strong sender-side exception.
---
## 13. Other work in same project session (context only)
Unrelated to AV enhancement but done in parallel on this branch:
- OTA optional `.otabundle.zip`, crash reporter (`:crashwatcher`), HEADER.txt automation, PHP crash backend — see `docs/OTA.md`, `docs/CRASH_REPORTER.md`, `examples/crash_reporter/`.
---
## 14. Open decisions when implementation starts
1. Enhance on **display path** (`TextureView`) vs **decode buffer → GLES → Surface** (recommended: latter).
2. User-toggle vs automatic preset for movie vs video cast.
3. Whether sender **denoise-only** preset is worth thermals on low-end devices.
4. GPU API: **OpenGL ES 3.x compute** vs **Vulkan** (ES3 often faster to integrate in Android media apps).
---
*Document generated from AV quality Q&A session. Update as prototypes prove FPS/latency on target devices.*

524
AV_QUALITY_QA_SESSION.pdf Normal file
View File

@@ -0,0 +1,524 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 26 0 R /F4 27 0 R /F5 28 0 R /F6 29 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 762.0394 203.525 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 751.0394 205.277 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 740.0394 108.373 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 729.0394 157.733 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 718.0394 157.733 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 707.0394 219.509 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 696.0394 150.157 707.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 685.0394 229.309 696.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 674.6019 105.9525 684.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 664.6019 100.53 674.6019 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 654.6019 183.8925 664.6019 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 644.0394 206.181 655.0394 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 633.6019 186.81 643.6019 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 623.6019 202.8525 633.6019 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 613.6019 208.9125 623.6019 ] /Subtype /Link /Type /Annot
>>
endobj
19 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 59.68504 603.6019 173.8875 613.6019 ] /Subtype /Link /Type /Annot
>>
endobj
20 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 593.0394 304.461 604.0394 ] /Subtype /Link /Type /Annot
>>
endobj
21 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 582.0394 211.085 593.0394 ] /Subtype /Link /Type /Annot
>>
endobj
22 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 571.0394 195.533 582.0394 ] /Subtype /Link /Type /Annot
>>
endobj
23 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 560.0394 99.04504 571.0394 ] /Subtype /Link /Type /Annot
>>
endobj
24 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 549.0394 234.197 560.0394 ] /Subtype /Link /Type /Annot
>>
endobj
25 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 30 0 R /Fit ] /Rect [ 45.68504 538.0394 213.757 549.0394 ] /Subtype /Link /Type /Annot
>>
endobj
26 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
27 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
28 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
29 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
30 0 obj
<<
/Contents 65 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
31 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R
24 0 R 25 0 R ] /Contents 66 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
32 0 obj
<<
/Contents 67 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
33 0 obj
<<
/Contents 68 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
34 0 obj
<<
/Contents 69 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
35 0 obj
<<
/Contents 70 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
36 0 obj
<<
/Contents 71 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
37 0 obj
<<
/Contents 72 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
38 0 obj
<<
/Contents 73 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 64 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
39 0 obj
<<
/Outlines 41 0 R /PageMode /UseNone /Pages 64 0 R /Type /Catalog
>>
endobj
40 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False
>>
endobj
41 0 obj
<<
/Count 24 /First 42 0 R /Last 63 0 R /Type /Outlines
>>
endobj
42 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 43 0 R /Parent 41 0 R /Title (0. Receiver A/V presets \(product, May 2026\))
>>
endobj
43 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 44 0 R /Parent 41 0 R /Prev 42 0 R /Title (1. User requirements \(preliminary conditions\))
>>
endobj
44 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 45 0 R /Parent 41 0 R /Prev 43 0 R /Title (2. Mode overview)
>>
endobj
45 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 46 0 R /Parent 41 0 R /Prev 44 0 R /Title (3. Video \204 method comparison)
>>
endobj
46 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 47 0 R /Parent 41 0 R /Prev 45 0 R /Title (4. Audio \204 method comparison)
>>
endobj
47 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 48 0 R /Parent 41 0 R /Prev 46 0 R /Title (5. Single vs 2-frame vs stream \204 decision matrix)
>>
endobj
48 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 49 0 R /Parent 41 0 R /Prev 47 0 R /Title (6. PTS/DTS reassembly \(2.3\))
>>
endobj
49 0 obj
<<
/Count 3 /Dest [ 30 0 R /Fit ] /First 50 0 R /Last 52 0 R /Next 53 0 R /Parent 41 0 R
/Prev 48 0 R /Title (7. Pipeline placement: sender \(2.1\) vs receiver \(2.2\))
>>
endobj
50 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 51 0 R /Parent 49 0 R /Title (User question)
>>
endobj
51 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 52 0 R /Parent 49 0 R /Prev 50 0 R /Title (Assessment)
>>
endobj
52 0 obj
<<
/Dest [ 30 0 R /Fit ] /Parent 49 0 R /Prev 51 0 R /Title (Preference \(consensus from session\))
>>
endobj
53 0 obj
<<
/Count 4 /Dest [ 30 0 R /Fit ] /First 54 0 R /Last 57 0 R /Next 58 0 R /Parent 41 0 R
/Prev 49 0 R /Title (8. Named algorithm pipelines \(best practices\))
>>
endobj
54 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 55 0 R /Parent 53 0 R /Title (A. Receiver \204 live video cast \(default\))
>>
endobj
55 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 56 0 R /Parent 53 0 R /Prev 54 0 R /Title (B. Receiver \204 movie cast \(+1 frame delay\))
>>
endobj
56 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 57 0 R /Parent 53 0 R /Prev 55 0 R /Title (C. Sender \204 optional compression prep only)
>>
endobj
57 0 obj
<<
/Dest [ 30 0 R /Fit ] /Parent 53 0 R /Prev 56 0 R /Title (D. Avoid for realtime in this project)
>>
endobj
58 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 59 0 R /Parent 41 0 R /Prev 53 0 R /Title (9. Ranked preferences \(quality benefit vs implementation in android cast\))
>>
endobj
59 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 60 0 R /Parent 41 0 R /Prev 58 0 R /Title (10. Multithreading pipeline \(target architecture\))
>>
endobj
60 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 61 0 R /Parent 41 0 R /Prev 59 0 R /Title (11. Short answers to numbered conditions)
>>
endobj
61 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 62 0 R /Parent 41 0 R /Prev 60 0 R /Title (12. Bottom line)
>>
endobj
62 0 obj
<<
/Dest [ 30 0 R /Fit ] /Next 63 0 R /Parent 41 0 R /Prev 61 0 R /Title (13. Other work in same project session \(context only\))
>>
endobj
63 0 obj
<<
/Dest [ 30 0 R /Fit ] /Parent 41 0 R /Prev 62 0 R /Title (14. Open decisions when implementation starts)
>>
endobj
64 0 obj
<<
/Count 9 /Kids [ 30 0 R 31 0 R 32 0 R 33 0 R 34 0 R 35 0 R 36 0 R 37 0 R 38 0 R ] /Type /Pages
>>
endobj
65 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 885
>>
stream
Gat=*D/\,^&H9tY\5*d=K^3^WP'kK^G3X`=;6ifX-D:\R9gt4'KNA'GZU5lhK92`3/AF<.miJ+ILHs+>`W&%)p_(C8/HR[5Q@PKE#f^fs$sj/5Z'#J=>sUJ!V$M3+DK'4A!:dk$+GdikBV\=jI]rgl/CX8D!u.&j:mX2Z!TJ?e.MmmRbi8u^$ASN^]PPK.PeViG*lDNkA"D]MQSAZ-cO`4TnC+Q1@A>cN-qtgUF1j",K.@ehpGV_@b]h89M*r>2+G-=8&XmRFWk7F*@3)^AWBqtI#KAI1c-dI,LY:at5J@O)bU3=*H8D,KrVB+F`SS*?CP3nU<J<<\"k#7p)qcKHLO!Cs8Hf?UKfR"(H\A)T@:[`QL?4]Z[,6<AUW:rIJri@+h4s1ZB($@AFe2Z@_TU9s3Yr+Mc<K'M#&Y"sm/MuS#+aRIdUK-mAo&8oC_pX=0#AgG4rm3CODp*J''=sg^"!SS.b[@=M_!<EYT[%+oXFX+RW\,]!pV$i.D]AG8[s&tgXA(l]Il7q\F9*+A2DMV__9rf(/n4O)89]Db\_B*KR*0&O4`3:"(kHtLl)d[ZWm-h@2s0*Lsnf"N95O.keC%D#C(aWdns48\*e3:Kkfas%Sgq<VYX;*k/BHt?[qKG0;<==U(I$!BUX-+dJh)13a"kmb2hR68ADRbgfoJNl$Q^b+o2"V#Le%k7nKki,[;FbraC7O"6))TKdfY5mJV6-\\QtYeW7mTa`c8i/Z*3(K%>ZjART/l#pNS2<2WhiAkRdA'J30g%HNck<J.4_+_q?d,'XR598$523#0'jB'g2OeaBXmma0Ya3]U3<8X)W;Mu.io'Lu+8RVh*DoSEEi3@h?W*(A43WL!o8&Da(d*$SX?<so-Jqu[s8JkH~>endstream
endobj
66 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1194
>>
stream
Gatn&gMYb*&;KZF'Q]]/JJ"l&n!m)['PNqGXThnC5X7tabXN3[P&fishk>E_2@Xi_/n)PYA_HZpB)\DI$5(#9oNl_A(DWHiTKjAo/"d2?T_\f[X-9Fp]"^$m8QMWa!dUB$-nZ22*I^_WK#MF4,VHDO07`+4.h`9di<91E*lH>1WX`OCI14,#K[Yf3>6/IB[HamXXZ;%!.oa9nfXLX%.t/947/V9GSjG06OA=1J!e6)2aGnP#426D6$R;a(`Ta$q1Y27Y\Dg_#&^(r&*b7h[MMr2C@ZX3?Vbq3V\^X;TU6V;9\_aoV2:>+I)_\Y0#fXdh>c@q.L2#C5na$jXrCkDI)*B6b-QAae_,q2<Y\Vcs._:1HBiOQ;6F0K>$%^d^aT''hV4h!h!d!$ZTU7VAf^C?8]BRgI/U4`LCd]ia2@O]_CAI83q6Da^`Kq1HWTNB1eJrq))^Dq*]>]eB,kbs0XGB6+j@CI"e57l,BR)atlQ$hi2^gXU6l0JgFo#JTmMb\kb^Z.G-GW/u!6s?aC2us8SLi]5'ZS8M,CC7br!^bZN(]4%lniPF)_G)WMnTsoI^D-eNu8D.[-GG(@7%UVP-@c2;\8__U2#4f%n]L,mU4]AOgF0r<XC\r?<^P?02'UM+(tK&EkU,M-G?m5$Cf-hCQ<LpdmgG6fNKph(7+CJ2-m,!o/M:!8mu@8NG/.=MiWVSqZ@:LDZPkn%8LuWSbua])U?S4"/rriHJh5#nLegbfdPOh9"CpO[oQtHA.Ni8%>(R_U@s[_>1-5n.']GXImcR28$+fq4H7"=FpgsjMj:Zp8S'ifPKe-7<(G417_@3\o(uBJ/V4F['W8Yq'EK[#o@VuuH.4Rs>HT'$"g71![$]9=$ST4FgusO"hgDO_6M0DCBEM:$5B][nEV!Z)o70Gcc$Uot\5:bNqmo1@`(koDHk^5-CV*&I4tHTq]#Qe.f_>;P2qVs\<d_6;6G86l#9*j:OU8/qd3!!P"4(c\po<op[[5;r:)ZVJc6MVqhS+Q,UtFRTk\3H.N5Fo@GN]9UM':c%Mn=N7F#E5d]S"nnrU#s)ViTA8pU@L+q>:!cj7R'52*4SjQu9A?m!qTI/B5rr#B5_);l?cY(1\Bhf=hluTq-o.^;bU[jL]Pu4J=UXi:V*1IEZ\&ZMihIMq+VC:2#3$@OXkt/Z+f(1,t'[+*$6"DdDgtUA~>endstream
endobj
67 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhN!`PtL;EgKhRb!t#B&/9d3qYl2*>SS#d@,P7/>#50G/8u*qX3a*o;=[-1\:f:S99RH9^:u=:W,?5@WKD='qjtjfWhhI]cVi$+69!=K+,Djqgq_aM+&&1F+T~>endstream
endobj
68 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhM``Q!aY\P"Hkb!t#B&/9d3qYl1?\4RNK@,P7/>#0K/(:XMG<G3MH.!["Sb[DUbP,'l(hqsbX<&ZW/fZ:Y(IF%*9WhhI]cVi$+_BI;_Fi9b/DW#Eb%ns2J+o~>endstream
endobj
69 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4721
>>
stream
Gau`W>BAQ/%Xua-i1mnYA<FHL1V7_6mIkQI8^kWFbg,9<Q=ual=`SY)P'uP456,i2/F2ThbaD.oaJ;'4!.bX\9o03A<;u?(#l.t*VXGq,N6IWAlAA@!fAt9Rg%R:kcdWECZjg"_f$%*\Wa"uLg#p?]Q;LEPIKl$N\JWl<Hp]Mc7AZl`)J&h8W2^]E(bTaelDjc=<b-6sA*ubQRQ,jg,4:2T%1bQK&Rc67AJgP2AH"O#aKcU0RrB`7ELf)2#BG\8ZA\$iGJ`BKj4s+&j,;uPn6kVd/r(_d+p*q/3OoL0-km_J"0[i('`QA56m6KcbsDOq->C[Ki:D:F,5GDi/nrX]Jc3AZ-a80('8\[OC<j]ZnMtUe<>^R']clf)'J=jAoppK\a%6^:"e*4jCnHV+NuhZ!95uLO6D!+(ad8Jd&\d1X*a^l]=,M,dK:cA4AEo'&X<`N-fi/['lABjXS0X)DX\8(GkE58s/WU&&_=9fdTZ/6H):ip!QLD=*_GgRM&eV5)3lOV//E3Is#g(nl)j$0a<*kb^H1.s:Hm%/t:+eYh3sU$;Y`L7;*lsRnG^E6G[0,uX+(#J>Lp"-W,W"0bJ_Y>(8[p,_E$UbI)-2jFc?;9L'P1H]FR&4i^XV+#YJ4%mp+,S/]mh^&Jk(6m?(pK=9'PunQ)'o4Ji%8,YJjt[II;?V2#ZGth"f7=_K!=r\V\tT8aBlf1m<^M8m5O@UgmE0/X<dA0$RErc-SO!TQ='!NX8D,9XA482H($D)BBJb`LN@&NX;.`RuDJ`TSglCZ[Dn_'fohF92J"'$sW%b!,dC&m7VtZ0.581'?/u7r9&hAT<"k]<VETVCAP@1+HI3*\R+PJkN'X.+\ZJ0j/[X*S#-M?B]S6qeK)W%deGH3^jgn;^0-,e\["d<-IQ+:d"u2l+^X4lThGEnFV?*j,M7l^l'4C[Wm*@f,DnB8<Qc_HA[%RtJ`3p8?CN\%r(LO`RH+Es=2`5O(EeQ!q%RVq1e!PfFAi.0^LWduQU#s.FEP928e54I3pB7Q_D5ahfb#pcLeT\1'O%R%(c>P2R,Rq1YA4k628ETNF`mgpl#9i5C[+Te3jh^*HmcAX#F%N;cuWN..oaW`agI&L[$sekXu@k+UK'MY9.0E,.*8JWjt,r_6Um?s7qS-DY@O7iUsTh5%G<d^S?/J>?*4"SnGd*(.Wldua;W!@WTAa#:+[QhoH=XX&Tfjr,RDNHLHC=)6&'L8QQpggPALu[`r[YHMF\pAm]H8<oAm]$oQUq)R$6,*'9eFF4[`B!Q3[?q4Ft=55gX$RL2jO4j#-RtA-"mgM>X$$8j</o:0%?SI9uK36l\HMJE*G4<JJn2@^OE'imH@U;O@8gOKg3cKEj6o&1TAL;RJl9RPra*!S=b1m@F\V>9`M;;AdasM#jPF11TV$'%?kl1NIobG!HD<8S1.:8+P`!%58U?Lfc.mk*kXM7l2kV<=!F9Tt8"P`T8b'*O$uR00*Q9hBbR)8/[e4fiK3j@;6hAm,^*.>]2uZnNmY<_QgN`g+9TZN8l4>A-0-I%O44FOBfH>KX[1H%.]#ZB[%P.O\rmC-lm*\%pL_,aFsSUejJFB?(>Hhs+PrPT.)\)FG#bQn,)il!54s2fWs04!I41WK,d^gT;r2=X8@1-)9l[lo2GqPW>GV?;]1+`@K=bKAH8<Z!Hu7./(UAJQC.r_:d1*pBW@J7",G'TI/%b%Ho3G^ps*Qnl@u!u'3a]RWlFDY@D1bNM_._GSoR%jP-p*WbUYA^"jjhELY*0/P:(o=$Ga@gZ?sM#;_R'Je?D+KV?u5-8`UF*'k\l-(9@*3;M>j7bg:QD<J(MoKW[:mF!BY'`;bBmHM4G/ieo*.!cb_D#V6D:BVgepJCq)ZjoSH72uk3IKil+qqB^."7b3OFApn$f`'VO#:RGX3/C)$l0cIY;,m%IH&hE%ZFWcDu^(2U,=`K6h06QVgSq:[*!(Nf\#,J&r=aU+$R#f578(ph^Dnr@e11)ZEMq+U1G_*Z7g%l]\H3s4Pm`'MfRktdUZ?DBnko-i.3c!J8DqtQl?Y/c,YA@*IKNGe_1e>Q>\'-e:k@2h+:j4lUU09mR-&/M%X#E%f]Zc:1fi*"8l=`-(L3j!>i[5#g20O#A4GVs\7fUJO,so[F@f,H7:WP'Lm&T&?a4Y?S!UE+aW4hcsMEJ"dOP:cV^Jbp>V>tlo0lSA70.*JL*(E^lB\G=75BK8FG*6WLcp9?#S^I/"M/MY@KTRPmWfSn,lE[LHE(!WU'4GT!n:[m&%k3D"k"M1#o?pt[TigqVf-Bt,_9MQ``VAK1I=!^J&_oLeUY(Pc&#QK3(?(q4WY&?dah%K:-3^TeJ9TK0(17gZ-<W"r"Lpg%/PjdpK>mmo6(i/D>JI8,;J5]a#%fXt?su8-]`A2ML/,YuDKWRcfbTsFZ$h#<2$IH)LqO66@puT.J$J*TH\H4L0!guuMR'!BE$V8MC0>[>0u1nP^&d=U4rD0(Di?4lY^gup,Q]WHC'#^O#"/(+fn/@M-a4IRq;5*^*'i1^a;Kt9r$S!m:UnM'f*Af.'G/(O`/2\11@gcYW"+jk(3X5%/V$h,Y0_C$>9pFDd3=?'.0)%[Rj&-Kh\M]XbfBmj!@UID5'>IknEj>^#t/JDHnp3pQl']HD#W0G%T@n+Q[kfT<P6uie]&'F*3_SCB"!$,4qlA*%N88&8;I<=nBJg7L.D;=[t%66,i[)rEu3^nhq"c[&V.h#bi`mC0RZ$43-J/lf;3_H""Q18bftZV!I\l3X9=>++V$OcB86pa(CXu2]$Ed9J[*X6g^k#,!`S@\+:h\s5gMqkc(DJZE(/d$^gaQN"l(%@<m0\B.qT&F2-$t16[5s+@!^J_Me+CSK+`s(e2n;J^jX<gTM-OmSiFTJc_V9j/k#;tF*QI0&^TEj2i+:3Eclj6@XWb#V<GEaY=4O:,=%(#XL9MZCnP7k+G0!p^lr=6]=?4)mQo1H=OFuoKO%-:j.EmE+"(3!5ij0.%IaNC^mI&35%k]O4kjfAKkQ#,M[:U15D5(:ltrK7fG3nOX&HeVmG+nqdj_N/r_A#j6:G7jHTMDT4J;jC]D:j]'Nf0fh>O2NQ;'-,l&)T!K:P8/io!2a4g*#^-5t5;e7BuU6_.Ag@UPC=4Sefr((R"0*LZW\PGK`I2I"3Tn-?H:R1>bBkU?#LiG7i`@DBM];*QHqIY)g3"aVrK]RI'<a"Kbj),=8T-JjO6IKM:Rd?VqWhH/B$[>[-A+bqoaq=m`'ehiXC[p@*IBe[f^OKN?[2+G_lFg<0kYjr_;?Tt%`8-GI6<LL>9>7a6qS-Kmf=?C\1)ADFQ'4lKg.Jh=%;2-k^^mjC=\Weroi*Q:![)#*SY+X>cKD/T:ep,66bIE#.)V/\e"6Uh.1k'p5+i9JeXQip/PcVFQ/kh7$A#u4`'Y$NJ@q#tRO[4mFG$8JGi8)FtT6^rT4Rmd*JCijjrba>EK6Cm.R_a5';)NQ1kg*T0l^RV@n;[qEk.!lXG_iPqhHp!:>PlDs1-2F`i6Lbe5b0jc"$FJ']j->c(Df_#ci((W2"f(1;=src/)I./l(^\cUp<kbN"",0kWO;s0'+qtm&L@-1;UhVqbK)P!(Bu#61?_0EE1;"DHp2_=9erZSe/^(C4FC_pSSK^>&<*DU'IdJ@Z;8M/Iki3+%d<IX\"G5D6<a)Ef`S-.@#mfj[2&0R9$17g%6a8o&8/s(Gi"a;7jO+`UG3]5%/QfjE"X0H^l?!P&73E8RnBY;;h1(F8W_m-G*^ASlJK:pG/2fCN-G-Ml\NB'/)cg-V`As3'(SMDIW[1FB4N:bBYS;L9+Dn]*fc*#L<U"H.%2/NEW]p<)86pO@_?CW1%]m-MaulcGlVHH"e8bXTS>72[dV=n%R/W^H=6>E79NLiE99&V.=CK/3C.?mCr`D*Sjs<*X4=Ob8X'?,hsU1K/;qReN+iZirNoW+;?o46ggd-hO`uU=ceEE:KA_mq.5gPoIY"+FKna#nea"I)?T2go(Y9l.*4.e''AJi3,AD^h]CTcRb$ZW]U?Nu(RbPATeDTmk4'Y=AAEP32aL5Hm8"HUd/!C+n3Qu]4CO$Ck4`k9AFre[U,AHbieVV]1QJDq!Cp91J>\U6Z]kT^YI!<-Xc]0C1W;%]qrMs8RYjPYpA=AZe&r%BX/VOS1']&mTkntZ%R%,t>#'#"K&=)D4t(N@h'#7&]NSQoLTs^t(k:K*Sm+-gACo%Vo1ih%'I=5[J#j@]b<eVK`C&0P<8D"2m,Dm85Nm;q-gV!5&,Wr'`RM$@IYX,dQD2ZHj6%5QWnk@O@4RSroTYWFRIW>Zg*u-8H!b,RGWT?U!bLiC,133u,-@95,#?g8K;eM+)K@C+$:<#O%a;X+F?0iq>]eOr,*I2b`6sj'^b;'`FIMW5HmuQ\r$ENjVS,a2]A!32Hk93]TF(&t9u!S)2^bPoDt;MHYPWM;m@U"gbD2qSqtNmhIM]dhf=J0'p0QAm(lfs?=%>up4R"5U`G@=>\CY`Saoah@J]$3))"NXhVN*8d5@B$[!?Brq92!gDVj-9l*\X@8,^,oWY#LqNPN=IYQ,fKuC$#Fs10G'GRGHJWc/)[LqB:/8"q6qB>9#7;8odmBfg1ctX`uP3O^'oj<'<$R=i)U/p;;WIRAClU*mF%fc21qeb3VD((CtjQWOC<Ugii\+H$CW=Ys@iF8a=(bY#go6Ar<#RhMO<1)D#P(3+I\P!8k)60ejfmQlVOp3VsoJ9+@W~>endstream
endobj
70 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4206
>>
stream
Gau0Hh/h=/&qBX_JX;j'l^0$7mrn-8q/G?E<ZP=Wmq+\'6VEN#L*@@qZK$;A^\n.1"@-#k!E>XaCI:b^!k\=3(;!4,i6m+Zq>Xn5^tUk/4bj"]0OFkE4$K$=Je.IJDb"d@A/ID2#6Wo)m$GYg?YcV,?[7-cdt^$kJKH-IJg^aL*rR(2eW33q'"Us,K9nfHha4RuIEQhYhs,2Nrn_;-34n4@@G#oRdGkZ0YI)>qs0>*2mpE!IfRQ/k1K2e*$2qh4*^C$J%3p?+Eb!R4ce"BbC0;g@_rQR5pDNr:e4#D6kmq2$-^ao>-LWS55(,E48<(6BW3">;T9'laNf1O];&^^E=QHo"Te,p.b8_LSCqEHQa9PVn`I4/Wq'S&AE&B$c\q/o<&H\Qi.cp+4?Td-g'X$tHR!fN,DWE-&m/O2T2KC%_:&Nr-I"U(&0SVK5YOI]kf7U.f2fYK$,@(Q/_]+[WXKo4q[]L<)=00CL>mH+8DmPmX8KLHl%J'39FC9K8_c$F.C*4c/r+4sg6"oFn#&Ye1DQ_kk:DWlG/?LBuPPeRVF?7,65m*d2dVO723^:BTi'3c1&Y4I$o0@ca('S,6l@5#BCH=Vj^,Z(9rFHt]-qk-;XW4i/XOH@Ei>mtHf7NU![dPnp/NA6SS2HrY.\0iG?1)R='>1uueS1#Xf*]"Id'_8a-?O**6a99MPuc5AJ/$+,\5]Sag>!Uqiafskj"k@&4lbBV8[FTk2m\QXdl.@%:F*]BBgEXTlh3(cmkKpnX8o(WGGQ)_8=D?+()hE[/6`TRF1-#e34UQJq/BFZ!,8?@E4;r8Pnd(5pbS80NB[gF,AVUu-ko%DEFYr<\C=q#2P^']R%TWX%3OhCqYoJD`3O8J4&=$>hkc77*,I.*6lL\AV<*G]G*cWkm5Xr!BA-Si3>5KT>h$'(b,[t"$'e1d3"0t#Wm4I'ktcb,il?%T1M+8JJSW)*.oNgDZMG^4&8nBSG'/&V3ctt:ClWBrf"H.h(kMr7U[UIO+Xsd\q'k$kRJV2b/$ds-R<2//iiEnS8=Vo?q*O,]$DFAdNYf1Rn`B$%QnQ9?`=?oM=_p_L5ChdF(cgEGf'f@oN7r4<"oHE5mkAGalZi4IMK1n37n'@!!:RZDi6>Ej!T%;.RXdW41'H/ko37NfRE0#.M)Vpk20fn5?5GI2:FNXTXT_mG$\pUK#P8X!l<RhAES`%BUi#)gipuB1>,U%m,fB?XMr`DW?K6')A?!Y1:%!!!KX!4>1J<_g'su-7N1N9SU)"W9[mdjGAO^^F\d5j9ES+I9qN>LP/'^OT"1kbLnQZMKRk(XuT?.Ek[T-UH+PL9fNJLS3JrJNopo<c2NCL(o@S(VZ<Tj*aOd6pT4`nE]-J_[g3*lOe/hUa@3V2K/_J[!5SZL:4K^.f2Z!D9Z]FEoS+uA.33;NMIE,rEnX`-91i7H.ZR<-q?>S!D@lO`kqW@J@72DD5tZmL^'76.&88Hqd2*,?CK>&3ApU54)W#=^WN%ZH0#Xp=<eG]bGlK\)]fU_3=fjECp&VmG8no92V/nd.u>XX'1BiGPU=n7nHkF\6t:.q>^\Nlu(.GEgf,R6W)&EJanMlf>6U/_5=Z4c?t<H.K+NWr]mCVU)$Y1M;29lj\u)U&Y(:jMWJ;!si64S:*M?!q=\RG$V<VD1t(S2tRbij^eQY&9PBWE;@Q@Ob%GT9"IE"6Vd4L,[Vb+I?H*OX>hhpfT;<ndt#k'qQl81d@UN563)5!"XEVi=;Z-W$Z/Ar",S(D/NOhs2\:)lqCTW&SIZul&@>hZkhSMN4Z@1_T$d@!U'FQ][10eg8>^Rj`p9)Dmsa`(T3lnXi.Q+.qS8-o-]4iBlDOdq+=H[noYBkXhtHHKW+d>]#6GjJ5_fLsnJuSW?'599djtQ9>X&oqYUJ(lPTH?P$Y\-8K)A/1G2*p[JHm0uBlj>M'VNA\m2&><2*B5Gm;(+*,)`lBk_J3C+)Yc.3=86R9G_Nm#_S@hS)+[Okl$$OC`9Q`29$ZFW9a42$mKEfjc6m\XlNs)am^53/-Ou\eR[<\+H2hX3fqTIhNQKI/@J\E]_B;lE%(MKeoE[\U?5"QRRl$456bIZrHJ#Fp5fs!Tp8n_F`WeWNpG+^fIZ+T2U+ek$m%hW8+OoJ\1I#E0`,t0ZrtB@r9!YMr+>^`VL*:kXUKYID,':#C6P"@/+ZU38jQf9G$j[:ld^7[:+@(qMm)%UI/#75^ltiKFUd4K"gWa$4?GpED:<&"2F>2inZ^8n0/Wf8@;+%:V#`&e^<2opa3J,V/$NO/pI:QE'KE?Ok]M_4>Y;!P.V5*IiX4?^kHJ^!qKCDVd-5\?9rA/ojCIV(N&ToFUO\_@dG]1/JM\WM>pBMFOE-n=Q+Ca<qJA:lQdBMhiMH.r5`-?t3ig07@^-j&]!<sZ6KBoQ/0tBJW`6(0#]_&U0;iOCdX[K@B:^<lRA4G'b$;k>FBL@'0b54YOek?]g[65IWFubeAM[2Zg`6Ua*D[qf(/G!?Ih8.!bH@'a-2@kjF]I@3IQejo;duiS7ZhUSi;Ng@'Z5V)%Nf`u]'(m3XEDsYZdh*MXRn4'?-jb4SGb0Q#,JF!^TlV(6cWUMmX`_DNV5=5_"Hg$EUOC%'-92cQXisG<EZChQ<*"06=]7bmdaQI1@@HsgtnR5QJ5V<HJIm>9^o$`J7m2K)*BE!Ke%9o0OMc%1a2r.(:fL;s'ZYg>/\[(*GqBSD"m;iF-l#^l,I`:;$.]-ZKYr(=_=bFfWTs&E;k["@\X$_OTOM!"GJj[/PoPDH5mV7g4.M":%B38)7_,=KO*WYO^YteTW)@6Kh[?Qr^['*s/SdGJC1uBTQ*)-8Q8ch=MT(WEMe!,J_qm*bsAdT*ZZ$?f@&M)QFIY+mFghuj"=cQ&DaC?0k5A%R,9!Eq4d$Fi9T2?N4JD4JUT>1A^0>r$QY7,NS<reG]80.8S5oHmP9V/3QVeY,Wb!3[nh#J.10Sm*Hd%NP6l?L.oWBJQpP)l.,(B(W>B[Z0m<p%(2uu2W#J;"#doJ&khSMN&P?4kDsBXE5bc:rl`L]1)[O%<qlSU1oq61Fl/%,RhmE'Lp@jS.=:[ab[kR=VlTN<Z:@.f8n,q&!%iA?#!XH)(*q39_?2GZ>[8!R+FeU0gOGX`4=Jp:5^5l<OJp'&'?C<4'$T6;c>KlWNIF!k;M*-!P1Q6-02ao\`k6+sBVWo&Z)[7(*"#S0P9.]i^%GU8-SO)M`f=@[M6AKb29@0^XC;n#+c@#qagBl\A'N]*jUmd5*E6)QT<R-GnJelP^!PIf\MuOtijtotPnVi+>?3)d!.aT(4XpTNPjK2,ZO"838R3Ic.b^-@e7\i&(=gRln2/O,1))o0Ta;6BW"RNR:?`(uKF"+[XQ]kH<:ZplQ]8h'=+WDsk197-;@k$kL]+OhH`ENNG-42M=//OfWCV<d@6OOQ@alrj(Xe,O:(\"\+oG%XsgG,QEdX4*frS_Cn.>ebF0@u(.`]YY>PZR<N$]MKhMmTsu*2TAtG1r>P<Ee7C>k'#9a<A;"Dq=VnWefso?q#g`Om?,-[=srO9Y'gbp0*d-!Gj?\EHY>O?!UF`_mo"7\PGdl"[%qo/lZbPnJo$C,a$ANSUht,D(K)hf0t_9f_\N.>APiWl6Q\5TNqmq(M[q40MWV&YY*'Ln8ta@/nlRViT@B(XC&[sB>65+:7'::eRRMV-YCZHR]NGreZT77jL2=D[qWRP=+`H)Gf41d^u"5oFs3!/2O<t)Tr@6G&DN]T0$@_6)+e@sNTJC;$Ph22:p"`;&-.eeLKD572psh.NF:%Xlq\U?]pl>l-f=G,nDI7$Nm@<80'>l&<^jQ:"b8GQ5d@FMgKN:b@pY$6m'>@^I2]GAQYu(?^$7Ph:X;CrQL@$(s02YA,Nq@ORsU)`G\]G5jtA`^9LSh[]$riSq;+JS,^KaEJ63PPl!,q>NL%O._-iBT![rUfe&hX`/%n$,`E,+dN)[^XK`Iq5oOiDGXOfF=WrD>jLZ+iSBYu[k&u/;g0D^hn?f$ILA(kD`[pT/AW^#\pe:[7%:7m"kOOJ^LNBP5]Q9"?oo0j"XV+5*[-bf]10+^CJ5iDM;.Y,i:8aj'L7(bAi`P(h-bR%^]D@fE_K3igG5b-d*W["+6BqV%MO]8dQ/&5(b'"CC9.Ku>+:^Pe%]3N)aq,DV_Gkk:6e`+TBST'osP8lFK+pZ+e>:=Fl#igHGlQi9'C=7/bE:eXQic;g~>endstream
endobj
71 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3600
>>
stream
GauHN96ZlH'&NLeoMV)Z8BmY!begV@RCUp@'O/]%S!MQeHAmBJ_2eNnXbPse!KB8**hUB*IT>"kX"#fl_[k&l?QTjU\4!Yf0Z#kSD@nca'.r\"Vrbl^(Keti?#=Ie$TY@!Is<:kb=9HH]XnHf,QjI#Vs.<md1dX2MRL9*bd.adMEJeuNmLQ30T_mp=^Q\P3aoi*f'E-X:Fr.9A/cG]2;uY=[/:jpIhOnmLS&<_N#g.H_T-SD:Y4Ra5_BO%FRH:N\n99*2:?JmpMWpG_LVCe:U`&=d*<jEe'ZB"n66rR\oY;g8UNf*RX>PX&4,n#,o%*SZfcD-rWiH$IA+3_@p0_WJ)>aicn+gPM0k6m\^5cs!&$slMtc=#3BV5oT9l`i`jtm^ZnN/`=B7VM_n,9tS/9*)6TJtMB$N7g>X$6fmk#<EQ=db[g]ZFk1rmFmi,m"m\`Obmj/-%>[j6[4&='C7Z4bK+@G)Q&/0AK5b6O+HiLCrirXh:gFNo&Xf;TaD;;.mESZC"@h67<`)4j!8>bjBGm[9?*cLEnND-4+0etrGpq%PF8cU@0XEfk42>L_<W1gdL@bj4"BF";Qt>Ogic:&d83ifL89h-S&%-g8df=6t?.q)W,Uolo0ggs`J6H8pBN0B%ZVf#HtrfQ"p&pK'&.I'TUt,"FjC)j!hg4Nok@m5li-Q^6*J!(U1ZaX,Z:0bJ676MuO*L^&2=X=IpHOVA7G=m=L36<LJ`H[I`WXRUVqP9U-I*RTD$?M,^LB\K#K7Ks%O%LV)lRaR@#.#U7d$Dcg:f:hD^-$pm^Kd:RL`b`[<<0`CG#n/UiM$"3al?OYf6p(JdGj++h@>s=9%Ic799X#6X%'H+^6T!"ph&Kn.fk,1;;bhrjPMd'KqSCp;G8l;B`5=D9X-$mmS^p<eZC>X8pO`"5gfEZ_94Nbg?g*6,)_h,2:REPaU;XQr:p,3g!RIp$9cUG[>7t0YlXeNS$Pd2+>0$Y9C"-0m%eB<BfN/t:D1Se+!;%F*`^jTkC0!#[K/EpHf7Oc1&Ld1KERH&.6IsmFIQM-T_7u#=i,Jag8X*:Om!sAhN;u)j7c:6_!.[lbJ>ZiV?pSW0Sr*Q)p9apGP1\,:Eo"g@@I[1A@*J#.fbsW:*fBJ_ph-'uZYe%PKQjkU2(U,^OZ9Q^7#0sbG7O/uVH2QO55e;mP+/N/D287Dl+-NKhKW(&<FL5@kZO($d_McN,Piq]fpX4C"e<TS$qrHD;$cWN0ZJ@S[8&ldm\7=^@H8$]libM<b6]E0X92jr7FYrNhu\H6+9P2!+>35H3!6S`nJur<,!DW=]DS5Vj2!4F*5.7[=,OdFaR[9-3aFt#?0Imp]RO:5Xj44Wbrp7d@&FZ`j0lTB1#Rr;$g$r6Mkc&d$R_Ac?+]#/>1)DbdK"qA,q6Xca2o56,:53sNpn*77tM2O=G,Ll'kD0LmWk\Ci?C=oQbl\4$3Vb"FsCLD7Xpp_6L=i/o.N`\;[!p^BncphXrmkT-QHsk^_.deRJ6cO<ch&6:(,>l5NM`u_DI+t"E"Kh$]c8.!glRq^rqBbrsDM)MmSN8_?^9"<Ok(Ic<jgFh6.u]jBRLL0S)&"W]lM1>5DV.BmAg<$mg5ni4eAI9^90]3?"aLpk3d"dBWa/IcI%WhQV-,2!VW?hO?a#!tq6%g-4f5n\fgI_]L5bHP/]k0mgF&q;Ie;!Y-N]#WGW'pRqDo5)Z6EFYFQ))JiNqBjW'<a/9MW\dYYo"R?i^EgZ(M%/&'&+2m@7"P3`@CSB#_%78)!F@BWB_8Y->a2sqL&P">UCes8R`!P1]Fi"_^&/Qe!GhiVna-RkOP?)B;PlBlYnO"ME"Ls20KQRO@G1BN^Dr7u0`.FcrH&W+S0AFtN5^`T6HJN6V"t.r[EIcYU+@o.,K=g6Xo4YVA%61rQ!'HOn[/kGnH`5eH^7ZoNl@c5cA4[PEekQolJujh9UB=&YGu,*o!.mJ%6j'$$RD=Dg60(un:)\*'MM-Q5ilhX;JE=G<LhKnC`Q>U/(<[Z_q\B>n*]CFrS)8uK'e8n7;BUN&a/ME$nk)&fnh0sILtR?t7hj?14)G(5Sq,!FT(l8jG6!#m+JM#),3ah8d=M+_,2cb#;"e;>1gX4U1.QQpT35N/.S<(n*-u:o5HbJgrVS+V8tq=_k8#Q,G6Ioq5@r8R`I%qC1%Am`n%MU"iBX^6n_L$49M<YN+<CO\-#!G)Q`8d[j&(Ff+2andgA^q$>H*i23sICVY=e`;G'h<5@h`giAWm#If2#A,3DLf$3-2Kp;pIHHF)'H["0;_PV[>mo,8aReM`L@(cBr*DNi%/phALAWO3ek,Ti5-?n0o$V,(5s9Ui@pu7hS7(+,3Sh_dJ#B9jBbuYl/,B7Mr>#i##S$rAEA21LJU3q`eU[hIp9))"I]-LMZV>Ig$/7jfP'n`@LT&7NZZo[Tg18rBrJFnaDR/+"rcL4+r@5M/QE*S*oJk:_ZgucPVOCQpU!&C]SS5<t`7Pi*BqBZ=f;L5&m&,B&[uMQka.2e*krS3Vmkc,-pcRce;$VV`Ue</EnfbchbcIbHOYJ?E.WS#"3mo4H4@^I6jLo5!=:T<N'n[r]`Sq)n04mGiY&uDo-+GDX7^!aZ:an'W!dcPD(C?X6bKLPuughOcjQ3;RU<XPP(,+?Z>qfq`9;')qH5)bmc9q!X]AbGB=)TA#k,QK$C8PA5pX=N$^e.M<X%gW_JKA%r&]Dl_mn@]lS_8f4%E^1"N>CJCB3W>dP7[MEZI&.qlF\EL=3nHaR(WBQGA_>e="410CnD,EtsZPu:HBhMo:<@'T6?@c\e,aj@&.s)\BWB#1W`0YbYR;[PW7\?o]DN33H"a(HkfA/2hhcY58kPEosUdi.Fk<Z?=DdZR\^K^;0heZLs.Atq.s]!!_%FYuFdJHi('O6h#,dju`qnadEu>>Ain6t<1q=ac0`R-8K1MU*P9^AW0%Jrq(KWRSjA"?4e`7MK_M[9Gfr!jiJ``NIW(b).%rM9&^c`M!&A,AV5n)-fKm(S4<tWuSbg'\0,a.u;%.'`(3YL;1Bm3ZrU*El,n8Y,e8"huYfmpq&4lao5,&!4\sN:sgbCi1I52jXNDu%/SaFMaC?86WL*W<-_Z84&E;X>6;lPM%iK\ECn<o<&KFC=hX,6I9ui=DHY3Bf^=^+3nLUI=>8OV`#;g44IJ+TCn^!sl6VtqQMF)]a14W`pDJYW5;`Ga;k=tWMIq-F$pl'f]+R/_r%O)j"sE].P0FTDU5u@n>2?3`G2sB\mDO<L-9T?E/(H^7q5^g,e]@s*g!OO-[9!2VQrR2$Z>rcf+:;2gk3JjWB]t@^HAh8LaQeuQ5kregaW4LW;+N%o,NJnfE1grH2`G(s%oYhaD=Ku_iZ1$MB%9GC[:anmC=m[rp:U.eGrKs!It!gn%3<0GQh`JI+-UhMD[n^;[9CCW_1CV2Yjj'p]''%DY<q&?7>rC%F897AMHh18c@e.2fj30U40b.HB[:l!BgfK#q6r;UH&(mWpaR!3qAVD,oP!)K7-Uo0LIF^40-[L=Xl=;*oR\%;^A`M$=ZAW[s!JW2*"ui&7d])574?n#Jsn4seZL=7>f!T',4L=&`botUM$*J129nK(rrYE&#;Z~>endstream
endobj
72 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3820
>>
stream
GauHO>BAOW(4OT53&<WXUGOi&JDs#>lqDha;g3'0Ojr_bh$8af/1)(1OHa&Nq`Q(@X*=[+FR(7`$1IrXi5<),"EaPbl/1n5i-Z$4<s/e_q$P2F)oMRX50i&5Im`oma/3H/VEP4=+e!:B`qUQ4j+jG'j_-!b2aAAQp$!3FM0Q9tgNA;n@<AZgPe5^tiXn:LC;VBGni4-1:>fRQNkd,R)\,O/Z^6HVDdiV[k!+]ZI)_eea]Pu`(6ck\WR$H6ZP)kL;YG5&iW_kZ]Hn).=K7L=\*kUI1[Qg?pLE9[Ke/cUk<srEKOPjpbNmj\c+`cT_Pl/PMF@PQ0_a<ff59NXb_MH*4sLT]n2Z<pR4kDT2RY)E;riTQE,p:[^H1nuKFdZqI6E:;Dmj)-F'nJ\o]!cl!f.Mun2Wo7d0kS/Jg&%3/Bn/)8-9*O#.#mS4IhPGAmU!1$o`-[;LR=D7$a_YcY!P#1O]QJcX4OALt=338r9"*a!:4m\+Gmr9$AB6d+S>3%O(7?T5@BVEnO]rGKgZtlgXt!p<)u(>`IT)\"M]CoBfQ3q2Qi08cE-4-P9u106i_EZp+J_T\fL$[p[#bgi0TZGT;Ldn0BPTn!_2!G.^'_nmImO7^9qM5bN-^!IOtH`tilTrus0693tuQVEe&@pqF9a%2k`")l%!)YCoYhoY8#Kj_V](CE),[J`em$@,WCD;EVeo+$K.*-AMef+DcKkjifsb31G@X]7X&3*DmY96FoO0.^0R-\D`Td&gB-n(#skddbQr0ZItQ8^Fs6oks@k5Mp'fu4PtKj+%U<rGLr]E(OQre*6uh^A/I/enbBcHi9VobQ@K(3DJ$CEcrd'[9\*O/%#&'!RrlrlEY+5K"OjC=;b_J7@2bdV'2,eTctnn@`^K%3!'de`SGP^LKN`=]$lg!_59WVgZgVAO[Ot`UD@FeIqi?]X+C/e\Vi1NMkb`eE"Zt9*oYAR(_!Euo\X>tp9JRRG5DG;eX>c]%VmtlTDB2(aG?,[f%Ed5I"V//T_0'SI87J)\%PkX+Z-$*"E,P.38$O)?(UIa-\=O3'3bP_\I[Npf]c38552BQ@OK*]j@0gYoSmL>kH4.K\[F_mdZRhh4qUX#5+$];@E=JS$Z@&gY4uQM4Q.OsilMBYOg$'<okHrt=4pW`%/<G7.ncCf^,ICgl([qs'Dgrsqqp0e!.[md%;%5ePTI>il>N9`0!FM_qdjb8Rb\fVY4M/A3<eAqe`8f\!!QoD40@JqIf+_-m7aMNr3:dYm:#+o4i62'e@Tc^ar0Z3a#gh)A4T9m-Sjsa^[lNTciLd;l1l'dpP<ea6o[=a9.aPY?hiu7rT7!$&%=7]\?@/f@r@0taae2q<1uM<hJ@`MDJ5:Fm5#iNjs1A@IH)1P6Z3EoKR6\_*MMM-E5'NE&e"RbgM(KL:]`Sa$2L=)VS[UO37>P&.9(Uj_Su])a@/.uuDs1;spT`]iqF*M95Pt><@JKPJ+:@nd9Ah6'DmT-4L'J"u:8fl;kR'hspkrNc`ZI,GeL()K'go9"*kR5Mo_]k4.%)<ifSSN:pGZR\I4#lqNf.q=_pHYK-3F__ea84t7o?^>#/mKe,mKKR%FG"_;E!_nP]\`Pfq,Qn)'E`*bJGroT_hnGM+fI3:Bkgr#o"=a?,IX(7JL=4H>^2l,8a;A[QZ^+oY&T+F:V!2Xs58GlikSGa6k42ThdL#7X]Et(n^,OV0`UU[>&On`c?A?7],)^K3Y1OJu,q[GqR,'0<!3MI`TPJZ0bto7c&hF[^+@U,]IA'\N1UP>s:Q:3.4EjQ3Y\9Xh+]bmgl0hpODf`lOoadCt>,.I)H&PWZO$C$fOkd=gqilQ[IbIm^ITcY'@Y+a%8NuS]tLr'Ggs.*V]fDP`u<4eT#)TaC3le+!Ye?Bm).,M8c!b]R*gIOeiB,%Oh#Q:5YA_!,(rH4]`a`_Y_7YU<<3g?6\0a;2MhGA(.cD8pmUZQ/YF^<6AHj5HoS^#Z+i,-KqSl<!$!o/`_2JINtuk/IaJ31n58T9u%o!a==5\8RP?_?^`laGBl>5/W)Y*9iohD0YA2jPPO%AdMcck$4/ttF"IBKWt5Zbh`ZCHR+G"3[@pJ=7X^_CUAm2f"XoqMCm61l3u$L$0e()=\dih.f:l7KegiIOQ9+T*Ni3*j_bH\q>JB`Q)e&QnLq@)gW?\0B?"'fePMiH<1P]s^:6``40Df9.99]C0r'd/a(eIVa1m$5SVj84\,?W%q*2cUb=Ptm2=(\l#"#[4Wp;u0L9#`R5e/>t,T*<Fs.H#r3V@F%baS2BFX;jH-_eAA^Yk4V@Za81+]YI`iD6^mG'J&Hn4+`lp#)RjD291<2IBaB4\KFRR2O3gieCgjq$HLk%0`)'t&Z-%nYo'KF!uX@hK+;,&R8]/HCC$-0RL@DaM$3=D3)g,hJ*Oc`GZWncbrak"RK[7(BN3QP-+l&,;/anC50kcdo-GppJW7TuXpjUF+jp`X<FX6-hNZ1s=g_cu"n!\9lg"-K^I47M0H'P5TY+/c^Ynr>QH7%Eb=I;S@90Pr5<i$i7C=%H*gGZ;k04DD]W,'ZddkrB$pI:KMoq(IoqEoZOk-_%H(;1+,]`Y0-@@@[6aU/>.LSrp04/S5a1TNseY)97#%cB\1s=Pi[E0<K:iXXdp=i1.><J/J'%=qqJS6\Xb4bPg<nVBS<X)"??9D3@*LaemD9j<pp<MrjTc9G39oRB_Q_E`6@q<$S[0*h,U4Zp'N9D1UkJ!*:U,FSfD7qMspWUN'@F6;qJ48lEj)R*37=Ad^S.Fn=Hp7SEO$oO^_if[+modT?G-IB9`l;Cta)KI:3MSelfN&E>c41%ncXHGT*B9bis2rTf=oY/SAN*/rpDr.2p!<X8QruN_U\!\X'L08/^Q'r!("#J8A,K>/5`kcEF#^LK\7)[es.SmM1%iWrGB%eP=`Qj81m.\W,([&=DRF5HQ<#9qRYdN/$2qN^8PE<p%i.4rc-Qcqp;9s!28e#*=S8+klf"lF]'[PC)V*<gPngG#@WdSf1J?AqKWCeAGb-B-#tI7OoNdjR-K47ArUhkD1LrAhUXWD6V?3+[IW;0s=)R;UFh@L(`70Y#Ku$>arkY_j(Z$[?>'!Cf?)COBJr#MnVZ9O]^<2Zn_qi2m-^j8:@-$VNG?j17?Wja`Ae@6l<,(`Th\;/ors+;R)0etiG9;=LP7a8[5]iY3=*W]HPinVL)2n<r7l'Amcu*d-gn@+R7%(L%Q;T)V(;BuiG0qL.'"g!<CcSUEoSjZ*q[,:&LOp-o;5PctiTrman\Bg"5/<]g/MuW,O$COZ]aQ,-6(tuq7N@Fnr)E<(&"b=^3fin2oB5Vs6^_D$m?,Q*\MFd?VQS=gKQb?Q`0b63^4Yu^JPVuSj8hY1T-)i*fW`,[8,kpb5YC4FdW_=3&r2/6*qegfRSB*&F7\I+q>#eXcV3k?6QOrTBVA[;heR>"\;EO*VPKCG41MIm(?U#'.)!Eb`DH@!G00Ghl>pM(<0JIYBRZ,.]g.Ti4F5.GG#`H,M3E_r_i;cc=GLmZb2q)H^(d;\qC$F:"<jk+Xe%e$1)B9>0aBiJd9sBm,E,ZUpmFW,,U!<&bp'RFDAXq<V"\J,EEU2\c=%ik0SV5rct[ZAGbT0(79@fArp'gNPT0?@^@/_5s(l"U$'`Qh(T2>85I[NX.7uQ1b!^/H"VKmn!4Pk!3RW#XQ`n*P#t6-gMEugj!o))r>Yc>?O2[)fT`g&'olGDCXS>3=XlIOh%^U@kk;"i.oUf;G"qSJtRLuC'fCB[;g+Apo5G514@mXu>R_ML[V4)=s=aQN9QGYOqNB!jC7)Fh;N%(pSa?e^aILQ>7r"$!E%a+~>endstream
endobj
73 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2545
>>
stream
Gat=,>Edc#'7HK[d8*sl)2om!E:U;FkE9Xm-PIWUQ`8[O[MYOeQ=Cf"q0r:ondR8W$q8EkOjr2J3pJm<!?GMms6iAV+/^g&,=!YNk<M_cK]s;_SR29/,3;LFW9FMknD1qlY)1?m;>!PRY@9AGQsa+#7L?Jr:FW(Zk&7F[f@HLd$-i3bPtTjIm21$=XXV3PB%M&ug,5r>m*aI0oc,<K;BkAd5ghe4%3W5Z@(3h&eE;BUU5]=X)<fI,0`;=,\'!2ZHu7(>bLUc*G;$5tQjSmI#r@\q@.`m-5pqETC;_A-<2\U@K9-irP6FUi%rCJ8!k\[EiX`7#HnHNR@GnhN>R-\+i#'IL'gU)T14r-mr,>1\,\NmN&WN;MncH`-H-.=;m`Xd%`oLhf*Er5P_([f0\>+bA.M1h"I%Cc7=;pcKo7"M;Un/"m,n"d,F\k5GU==`rA2%AF+cJYCh1CGuF@YA@+:M;k^pVrj_!OD9$OiB[jpX;1C?8j\D@%e,K)0W/duYl(k5-j#hG$;3+L@<=.#=LhKXO"#h@8Yi>nCU1IGicj!)<?<!,36mSo8k*gB]eB&UQ,ZH5]5i3LcN4O5F@]KFFP_iS]!<NdRX9-=Yi)k6K([p_>,cp_,<3T%([mEVKmW*&sP2&T$^Z/9D0P%cfb0E/C^+=N8+e1:35]]'X),n/qeS&^<"cK'XmFjFXaPF=8eTRsgf%"uk!;jsVQ&O(R?d#ADC80;%9R&i,NK903Yj\DP@!b?j0%OtZV9b^mjCLke:?5q[b7[`,XA3Ugtbqg#"IWFK*7Rd];6/'WZgXorj&TMRcp9^nNT(8uf\@5G(T%d<T;C<D)BW/e;Un>R=FG&0/XA#Oa_#4ST15ddpUJ5=\qh=3c]+2fpl_cNGpLJ;`BDAU?j`p8Cpr*/3C9hRk<W1,1XWTH/ccS4`"**73@,NGY"NONLJ8uiZ&T!rbTTc/:m=XG=hPW24T9']Zh[IL'W6Y]^-BL]-06$"UBRN%5ICt"P/Emq`jDHc\.?,O?D`nDg-ijZt*j@Ya[;7a2WDPk#^V3ksY'l$Rif'Q,sIcMn*7?Rfn)-!'nY1p(bX2>aY<dn%CN)AiR"&rTTal\A8=)Y?aG#63Fe_dYH'k/WR*lmW5_NV!J:N)['nK"QK/JlXokn0m%k1OH58,>/ZdNEmj"=&^Jj=0osSh"I0pKVFMSaDTKmF(tpLjcNL'[#iCXTgi!B]EnU7C^QdNPJIG'!$PL+W]Bfjg)L=SO<nJI7']o)k8MCo,J;DRfV:!a/!BLV@R0)B-VVY54+sq`tX8N/i9>g#^dp$6</t1Fa'bB38_t.c;lsg8leX*f1#0"-[*Nt4YcSEU`rSWj&%!sV3Vtrp$cpZ$0UH>8#8^"*d?7H>6fD;nDf\qrHXKj+E$L[a^`!19h+n?<q<N!jE6Xi9_I)@a3e/s@HH6E,/[>oViXXG]QVi4A]ETS/bVZC=?F+5g1V]^LtbPBh'a:j8*SYF"_R*,0V]V\e1FEf`E#3fYoQe'Q>L<Dm2?3EN+LF`6;t`<lMQL!k`uo"mS_uTj=+DL.KPHb:rb/"psk8$iWar::Sa?gZ1ga[N"_0gB?TWV5FK#j_;^':OJKNr=SP^V@T\_d,0Z"@NG96chhVH@KPZ3S>BO\J2+7@I/*__6I>N.Tpl!2aC9&Af"<0$'<uW&H?e[b1GNoLt$9gh0Y8D3cCQ\UJ+9nf#6^!>!$6mTg<2_tQ4:u6(--J5j,[&_RRBE5$`ee;4SRJl+[Q;pppGh8,'_p<$_s#.dZ-8LOpt.C'=;-E\.c_aBKrF%Zc;L1AGi'Z,LMhgT'br3f-GrB<@8Fg]i9PJa$78f?(phF)R`O,t)>bT/rYHghp0<6:1:]Z]@#iM&'%C%_5+f&kl=[l=Z23_0212tUh;"$u%$(LY/#&QefC8S"Yr&S3rBde)5BBEAS+6<kP69Tj(AZ55.d.e7ru-qFTDfee5(4MJdImLMV@3C?U*Sf?O@SqM[FmiU>mL4q[]<O9\SiCHAmI`N'8`@N)"4aH.#pr&0CU\m;(J@UksAu5bmk,s9c15h=-WnQ67&ABD*`'YiOUWVi:qjs<3MmtCsbpBH_d(hm9HOd8&3`'P_#+TZd&*3<N;^4ct_@R-Yh>HlfoY's)L33ADLdacE[B;mg!p>La&*/rV*<<5diegl,!rt[Gt0K:n#44<u$XB-D"4MJ07kDa]$l"ntJV7!`b3qC.P>1/n+"HioMF10HpJ.i-@q4Wg2OO[8N`$7ZEfIds8AuV'nbA//>]^Z0nZgi:Bt,ES4b.L.b_=f',oej\l'+kje9AB)(Z>h.,n5!CM4M=r\j@%Q*pbcds8^n/%#W=`ZYZ5g6K_KK5V3U4j=,^f'iq@f9+\@MJ^=;V)NT!sU;JA1(Cq_]^LkU<@AFi!_JP\$r%YnTfI+]ObEpSpAQ/][Y2)&ZK5@F[k9Z+FASPDkj<J,A.Zsm>X\;VDF@J-9V=<ic+WO)-#G!Wei9T7QOKrrcW57RUZuif[,$'[/5.,.-o$)IqtYlAmR5\WCJPb,%R)0\`^g0g`8Uuom_,^q*=~>endstream
endobj
xref
0 74
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000512 00000 n
0000000659 00000 n
0000000806 00000 n
0000000953 00000 n
0000001100 00000 n
0000001247 00000 n
0000001395 00000 n
0000001543 00000 n
0000001692 00000 n
0000001839 00000 n
0000001988 00000 n
0000002136 00000 n
0000002283 00000 n
0000002432 00000 n
0000002581 00000 n
0000002730 00000 n
0000002878 00000 n
0000003026 00000 n
0000003174 00000 n
0000003323 00000 n
0000003471 00000 n
0000003619 00000 n
0000003735 00000 n
0000003841 00000 n
0000003919 00000 n
0000004003 00000 n
0000004209 00000 n
0000004581 00000 n
0000004787 00000 n
0000004993 00000 n
0000005199 00000 n
0000005405 00000 n
0000005611 00000 n
0000005817 00000 n
0000006023 00000 n
0000006110 00000 n
0000006430 00000 n
0000006505 00000 n
0000006632 00000 n
0000006774 00000 n
0000006885 00000 n
0000007011 00000 n
0000007137 00000 n
0000007283 00000 n
0000007407 00000 n
0000007598 00000 n
0000007693 00000 n
0000007798 00000 n
0000007917 00000 n
0000008098 00000 n
0000008224 00000 n
0000008365 00000 n
0000008505 00000 n
0000008624 00000 n
0000008794 00000 n
0000008940 00000 n
0000009075 00000 n
0000009185 00000 n
0000009335 00000 n
0000009462 00000 n
0000009579 00000 n
0000010555 00000 n
0000011841 00000 n
0000012071 00000 n
0000012301 00000 n
0000017114 00000 n
0000021412 00000 n
0000025104 00000 n
0000029016 00000 n
trailer
<<
/ID
[<8790530eb11d5413e4389657b86cbd98><8790530eb11d5413e4389657b86cbd98>]
% ReportLab generated PDF document -- digest (opensource)
/Info 40 0 R
/Root 39 0 R
/Size 74
>>
startxref
31653
%%EOF

139
BUILD_DEPLOY.md Normal file
View File

@@ -0,0 +1,139 @@
# Android Cast build ecosystem
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-30 |
| Last modification date | 2026-05-30 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Shared authentication](#shared-authentication)
- [Local stack](#local-stack)
- [Local developer rebuild](#local-developer-rebuild)
- [Trigger a build](#trigger-a-build)
- [Docker CI](#docker-ci)
- [CircleCI](#circleci)
- [Production notes](#production-notes)
- [Alpine BE (config.php paths)](#alpine-be-configphp-paths)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Shared authentication
All web consoles (Crashes, Tickets, Builder) share:
| Setting | Value |
|---------|--------|
| Session name | `ac_crash_sess` |
| Cookie path | `/app/androidcast_project` |
| User store | MariaDB `users` table (same DB as crashes) |
| PHP auth | `examples/crash_reporter/backend/src/Auth.php` |
Sign in once on any console; the session cookie is valid for `/crashes/`, `/build/`, and future apps under the same path prefix.
Orchestration generates matching configs:
- `orchestration/runtime/crash-config.php`
- `orchestration/runtime/build-config.php`
Both use `${MARIADB_USER}` / `${MARIADB_PASSWORD}` from `orchestration/.env`.
## Local stack
```bash
cd orchestration
./deploy.sh
```
| URL | Purpose |
|-----|---------|
| `http://localhost:8080/app/androidcast_project/` | Hub landing |
| `…/crashes/` | Crash / ticket console |
| `…/build/` | APK builder console |
| `…/v0/ota/` | Published OTA artifacts (when auto-deploy enabled) |
Direct debug ports: crashes `:8082`, builder `:8083`.
## Local developer rebuild
`./rebuild.sh` (repo root):
1. `scripts/init-third-party-submodules.sh` — libvpx, opus, speex, **wireguard-android** (+ nested wireguard-tools / elf-cleaner)
2. `scripts/build-native-codecs.sh` per ABI
3. `./gradlew :tunnel:assembleDebug :app:assembleDebug` — WireGuard userspace (`libwg-go.so`) from `third-party/wireguard-android`
4. Unit tests, adb install (when devices configured)
No runtime fetch from zx2c4/GitHub during Gradle: sources come from checked-out `third-party/` submodules only.
## Trigger a build
1. Open Builder → sign in (default dev user from crash schema seed).
2. Set git ref, pipeline options, OTA channel.
3. **Start build** → detail page polls log every 1s.
4. Artifacts land in `out/builds/<id>/`; optional OTA copy to `orchestration/runtime/ota-artifacts/`.
## Docker CI
- Image version label: `00.BB.CC.DDDD` (see root `Dockerfile`).
- Host wrapper: `scripts/docker-build-runner.sh`
- In-container pipeline: `scripts/ci-build-and-publish-ota.sh`
- OTA generator: `scripts/generate-ota-v0.sh [channel]`
## CircleCI
- Pipeline description: `build.config.yml`
- Local CLI stub: `.circleci/config.yml` (`circleci config validate`)
## Production notes
- Mount builder PHP with Docker socket (build host only).
- FE nginx: read-only OTA under `/v0/ota/`; BE RWX on artifact mount.
- Keep `session_cookie_path` identical across all PHP apps behind the hub.
### Alpine BE (`config.php` paths)
| Key | Example on artc0 |
|-----|------------------|
| `build.repo_root` | `/var/www/.../androidcast_project/android_cast` (git tree) |
| `build.artifacts_root` | `/var/www/.../androidcast_project/out/builds`**must exist**, writable by PHP-FPM (`nginx`) |
| `build.ota_mount` | `/var/www/.../androidcast_project/ota-artifacts` — nginx serves `…/v0/ota/` at `/v0/ota/` |
| `build.runner_script` | full path to `scripts/docker-build-runner.sh` (no `...` placeholders) |
```sh
sudo examples/crash_reporter/backend/scripts/prepare-be-ota-mount.sh
mkdir -p .../out/builds
chown -R nginx:nginx .../out .../ota-artifacts
chmod -R 775 .../out .../ota-artifacts
```
Apply nginx OTA block: `nginx.apps-port80.fragment` (`location ^~ /v0/ota/`), then `nginx -t && rc-service nginx reload` on BE.
If **Start build** shows *Network error*, check BE `php-fpm81` log: often `mkdir(): Permission denied` on `out/builds` (HTTP 500, not a browser network fault).

290
BUILD_DEPLOY.pdf Normal file
View File

@@ -0,0 +1,290 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 13 0 R /F5 14 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 762.0394 123.509 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 751.0394 85.70104 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 740.0394 128.389 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 729.0394 96.81304 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 718.0394 81.24504 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 707.0394 74.12504 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 696.0394 105.717 707.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 59.68504 685.6019 154.7325 695.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
17 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Outlines 23 0 R /PageMode /UseNone /Pages 32 0 R /Type /Catalog
>>
endobj
22 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115603+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115603+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False
>>
endobj
23 0 obj
<<
/Count 9 /First 24 0 R /Last 30 0 R /Type /Outlines
>>
endobj
24 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 25 0 R /Parent 23 0 R /Title (Shared authentication)
>>
endobj
25 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 26 0 R /Parent 23 0 R /Prev 24 0 R /Title (Local stack)
>>
endobj
26 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 27 0 R /Parent 23 0 R /Prev 25 0 R /Title (Local developer rebuild)
>>
endobj
27 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 28 0 R /Parent 23 0 R /Prev 26 0 R /Title (Trigger a build)
>>
endobj
28 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 29 0 R /Parent 23 0 R /Prev 27 0 R /Title (Docker CI)
>>
endobj
29 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 30 0 R /Parent 23 0 R /Prev 28 0 R /Title (CircleCI)
>>
endobj
30 0 obj
<<
/Count 1 /Dest [ 15 0 R /Fit ] /First 31 0 R /Last 31 0 R /Parent 23 0 R /Prev 29 0 R
/Title (Production notes)
>>
endobj
31 0 obj
<<
/Dest [ 15 0 R /Fit ] /Parent 30 0 R /Title (Alpine BE \(config.php paths\))
>>
endobj
32 0 obj
<<
/Count 6 /Kids [ 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R ] /Type /Pages
>>
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 841
>>
stream
Gat=*?#Q2d'Re<2\;sdh\A%rqa0o][9p)lmFOHa-(7^[?5R4)`rr!6SiF<1F#UL8#53Et/?Xk<WqdB67Mu^"D\@b0o#2T\`&-:VhDS=/a$%@gIOsigu!Es8*\LT5VL'_u>QQ-9T("Ed*/X\+#dcZJK`-iO5?=Ztg5ZsmjX@Rk.&;n]:EA)k,<?.f+(r2-'jm[U1XogB$,hlqAM*?Lb$Jks1g))@1hEH^*.O;<c.4Mh8U"aa%E[#]oYu#8=TLHm&VKZ_e_=c9D`Ve?cpA<k(:D*hrVB-=I'B\LV7!!m!XG,43`RmB2lXa>:\?sI4@E=]+5sf<oF^[:gV]*J:TeW>g'[U\I'Ks_-SjTSTo;ZkV`G&Y-Q4a).jXt!"Pa0)ah]fLmn!9-&?/=dTeKu\[bM<n6B\(3S#pg3M;4YW&_d@=ULUi6oMS*ROe/=>ZRgH8Vq+Hk\R*-AsELt*f[IRflgR?CWSHnQoT3C"\jupr*,X*gX$uYEoW^kf_P8ZZp;BbQ$ma<0'4$E\?9[0N+ES@0lNOE7mI*n]7#Zou*bQPpu)DhILhY:XDQdopWlrC$shg5BOoLp=N]1KnfHTL&/m`5Oi-L,RV7m$@1%pIBN`*j'mC%@d_/F*m1g4FUUgdEL7+lC!Ccomg^2p7Q?<r;^npK.6=SIgQj+miR2dlr/ff+n(m5P(ZH9IaB#n5tEY1I,1#Zn\d>j9SV&B5!$%9ibL-RNYk0g_Ru50tIP92+"\^W^_d[6IZ:t6L]`4[]Bge6LRsU_!PL/\_\F?V(#A3TnCmu-!DF+"l6V.Cq<#_S=>LIOB`H@m"^p!+q^C:3]TX="P"t)R/@'bQ$/)~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 571
>>
stream
Gatn#6#W5H'Z]\<'`V[SK?H&Y_er,28sLSPe*(4<Ul'a;=9rNYZsS<;Ut+2]^a`k$,Q-KoH[\'j4?$r`Ak-6ki;/a)*6A[)YQ2c`X(:p:7d%i8A0MAo!)cGOq/P?LF3Ddc\0(QTIOmT!J]X"o!Q",8m[Nj++V2]B1&.0QGu'eTl!pQ_GK=lC<GgJ@,eoIhl.J;V\AT3?-i!ZI:Pa`6BqYJW;PS9.SLkhnbYh[1HA"Qq:M$c]=^%N,Bm(?0oqQ63\U%/=DNo]LO'PoM,96JkWdiD>1$*lQfI=hG8PI*0lKmHU2g6LF?8r\mVDMGP`]us49Ej5hSTe6:B;=$YNT`&.rBHeRE#-1WAUh<2Rbep0X.$=p=KJ`t]0>,;He&l?Ra`,?p1f8AN0l7]l?t8i#g&f^J.iDUM?Vu;r*d'@I!s)"D?$7n(3F1EobED^]Ck`'\_=."s'j"g>L"NuFkY$=X:HnS[5jn]Z!#rCWDq3d['2u6d<86ZNHK#;RG.=;>%`OI/c1V^P6$d[GX%gB?`P&#EB]Td8L!dQUW=1(LFa^.L4n\/f+;j[R/I3ag00(~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2935
>>
stream
Gb"/)=``?=&Ur?8k_Gskg(eU7e>?-?_cJD0Z:)a4$<*rJ0I5T\1"[@/7oK0Ol+oZ;+@(P<qJ@fW'JNVGrH40Cd-s]SJ,2fgb;#r6FjLR7cTnrR$cY-pApj1(M-eK7CrPp\blW+M.#)?E\F/ENqZKhP>ts325uVUK[^mi2Y2X2b[pt#dG`K,j#gZ=(;n'@df\1)WD^WW?^64;T_fuKB+[r'<2=fp55,nXU_r,u03":CKl%6ohk`[i6nL9F3_tYT.ZS*=HRPo1lSQKa<H*j9tnnb940@tk(?eiHT,)Y<8:;LSJ?kF,-)-h/*(0$g?3:q]s.K4@UKi(,2j:m7mn[t&4a")CY>cgJYU3VK-S"Q,n7rpgi>h^)u\g0&NAWDb_N"eW<*#RrfE!iqDE(g",:9fee.8%=F"jPA6OZD.-09.$d0@&=[Df5(L\@Iud/TWkEYg6'='m_>g)!+B$VeI'ho1lP0VP,u<o9uf]6)';r2Qj6L(=UNcV=urFhdf.]ZtbE=Z?^6o5J?d"lMD7p_3alm+Rl&L"&(D]6RfcZm+7=c6INi%_L$.76+BOM6mZig=W$p%U^mcEhl379i*h\i\+&le:_"[\]SGiX6Wh(eW!HemcKk8uM=?U3?UDL$MS2<1LPkY.\RkgjR77mCNBfQ*>XJYh7GO<$mS_f5*8eKFbl_\kPoZkZqFm1]L.o!/3)1.,;p*W>_6=d^Wo1U;JO_64,&'(:Dts;07.C];Z?9i0hRu"e#lR^+aQ5JDi)9dQq1)LqYALr'f(3W4g:+o^=rY8TC&fJlD#g3.!IUmVi;[Nl_dRB^T8EZe(_oB',ci%Z'#&H62@%]fiL&HSg9EB'QP5gB=sUa1c`Ige/_c$O(r@+llr3f_,^u_ui%KTu(do7Ar5.tk!KSK5".1h"1i]q1?$0`@q-eo^XbfuE9"1^lj23E@1X%-^#6\Qlk1bIpko_bF5f2b:HrfqC!lo=_E]Y*LI-*UJP4BT?^mrCYImmVH,cSPo,U?qX$g46'"K^iASTa[60%mG#M=.rLnZb,p.aJYP/SQC+@6mtMZSa0fS=5+[*+c[h&#2h-?Z4AZ%*\pmD3MIorV+d[h1ppSbD3:qDsmVVYWbmqkMKV$hgbNNGJ1Cr`n>CA$ksUE2HKLT?/%0Iptl=KDjX<Eb\e8HH^s,tch[/aH5>OSi@,4R#q$`W1TGg,eKfX88[cuWquAWWY>-/fC-[u=]dR>Gs0U4L/(SCGn>HmUNig>$5h&:KlE>Cj56=R:AisM]#NP%X&@+jUjBP\0C&88bdW84UZn<o/r_A8r"(AmhV@_]p]*aeS!9/j/fpd9[Gf79%kiLY<r6i7%X`FHHG^Gp8>0?:;\GL>dEVg?F7;^n6r.>#f:AprbZBXuY^qnA[RmnQ92h967$QqP@B>dSW#NBG`@u8pmY_\o:@p(DeXeiiU3-l[,[RQQl(nTE;UFhmUb6\E+NpDd:BRt\/ZT6)>8=WbX>SA7$i+-Y4ho^OT-C]bm#ZQ*d0Dtl=5Ta^Y&gW:^S?s>MRdNe`qrDBIX(FdPc`'`-%:r6<ZDn&Q=?8J0J[d;M[BEW,Xu'-$HlaW6:r>7dLZp#D_(\/>I2RoY\n"*]`GZ^CL>bH5:*5\_1LNd$c9NY>5-KheUR_.5Q8?hmmTSbQD>&I6(h"/6M"TPfYt/9TVU95D8!Lrt1"eoN]$jkR>G<i578k?,(ieFX7A(`!^J-!%RP$3Wm&/8Ofj.Qd&D0^Q\OE<gc8c1Tq?5tq(<86m-UQ0+L=5DC>L*m/CqaZ7jYrG&4"5Nt><*#[A`oq1(+T5r5+FDtYf8#Re%Ak_QsVMG=d7WX$bL!3=Z'\RYfsKhpki)]=PCn-2[Q>m``P;j;3cVb%YR$cYbHH]b.Kn91l67Tf:1INdFF/t&S;a#m@qc"ca2S7lFSa881Vck/Ag<+H9DML10tM@f>uWILEN)nL+Guek"qCg;pfBA"r9]^4T_a")4dY9-d[m!31ls=&Ek!>9uDCmW\LZ):/IXNRsE"k9Ucm-ZRM0i&;[Vn1`6F<7^>R^]>r\G8tkEE;P<BC"9b!edRSn84`)1['o&N27'1O;@B<e]+:/]"Ke^Q&.r.tgBo/kf"d+8kLD]E7e?KKH<G]EGdEH@jXdRlnj<)a5KFoJ?:(^r')#kg=#>EYF"a&.E4(EA#oZ('RYsP@oe<-WTm!WDYOUK<8Aal&GUC:[c^D7nc^\aj,(ZLH[>qC8PPkAfE+Y4_<:ig^<4u.+=6e+@*^1J3l3*c-tBSJBZ\#]M1[LZ$1G>rbBNfD*o'3S7%R\j=r.T]]o2?)@M&a5E=MJ=ao(n$(hiD`TVI3`Kdg*PCD\+a"2_\:'Y9_fH&(!$Z$.Ye2Yi%gMfAkT+o5/THcp9AF@UA*W7Wu0;WW#q<nU6?"LU:qS@SbAgZpPtN9oV&R%c+:ks274`A_ab/mK7B/;o&'>de@j.(ZHmBj6B6l0O4WT57hsK[(:R:1#\_%63mX*_MSc-):qnQV*gA\]Z!&XBXK.g$+OY);7?c`o@e)mo6.^i]2\oD/9"7o,eY%r3gq!`G7Yb(6=7@e3%-?Y2-S[u%g#Fc7+,F4GFZV>?ir$_K+fq\WH/\Q5V@+e^Zs\0'CJq)LA</E7GeF0KY'$OrrlT]e#3'nF5G'/o+UCa2khJmj)m8>r=M<Hgc]XW5i9o>ao4tomj2"X!h1LJ[dG*<fc0ioq7nSuI?rGbi4qtJY&$%o&W#9:d'S"+9!V&/`!b?G9?V&?j^+!?@#USOPh]eQ`\^h^B0VX,"o&3T'3@aGEepL^jM2VTda)0G]]Ul,T+646!]5H]1lNqo*aEt';Nc"Obkqm:bjZh1[hktt7Z0Rn=rkC9^?TY8Hg!T;iRZOQdm7k[Ar2j:;Hf)&4<V6e9f@"fnJ`c.liZbjI>[6Qi-6_,c(#;5W)n'5A\sJoH^]kOD%C*^jrrI).NnO~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1205
>>
stream
Gau0B99Yi)&AJ$CoOUsmWBhZNbB$=VP0In3_b#5tX\/k,1BWFJnp#3.hnC[nYmiZK2-X:LOemN*^JEZ;-oguuo8X3/%nj,k;4oNm;kP3%@/1ah>Yb9icrMrYLbkeiKW]rWjrTs/WIEoO;PI&K#LB"UQb!3"F9[mqH93;A^^24rqI=:T"D:3,gDXAmot3#E(]:#aLll2oTLoSO1jIgWg)8me&>!+QN]"0ld"0e.QtuT`AtS/kOh;<B.>59`jT@&P[GNY4S:%q)QNU'&g1':!3:,EB6\)07`2%f@*,Irtc,rG3/;[N3es-p[4BBM1[9bZjF<IKcRVjnb8ohYW\Jj63I/e'!R8d>3@TC/U]bcX#Vt<3i(OK__B^,VCf+"EZFPFcD?[$Kbq/\HeljaL\;f%Y=fR/InkCrSCnN6<oWjV@kbAC9DpIp'-nSInWMM5O*6L;lsYj6W8"1sFnh'nBtHt#E@>''MXCZr%2EZO%dEjRd;mt@[Sp4_WgRu$?HQ+AVA0%Y4sZg'E,^-jC:':S@\*%Ih/TL9l'ZYOD*,(AV$1(dY`7&8ZK9=Gmc^f.gD1RqN,Mtp-/$((<u%fV@#LC&gmd.8^Ya0.^'L^sa>5ohABVOk[a>j/bu?LK53fXY72\n0A.f>l0d-G\;3JGoI@J?u!Ng\@0:B8s]LkKOV'#eEU>e!Jp:H9MWZha&#l84%^g@i\RbGGjrZ6Ln*sJgn[&"dA:V[eWI$RrErXRmLX`Y21N-pbOG#SC_p8=hS&*X$S9gm1$rY-klKF'GX6-jOT&i]&l].+1"9UEq,N-'!)F^V=o5,QH)(,.)Gc6eDn5f+*F&m:s"]"+VJW-LAoB]-Ds=IRh6aW&?bUCcA9q!oImt64mM79WN#*eVVYBSrk6,OIkKWr-H?2!n(3;V'38,@V(%EEP"N/7G^8TDdBU!'VWa)?9U,%u5;6bL;;9jo5j`eV\gpkihAO0%*QZm]9e_KN)m\P8?qEQgh!N!LEjZ(AR.K-Lbi.a9O,<`[4^EQ^[bhdj.@AWL#?517eStBj<sKljJP.HQm2a,,*^=,4IGiYYZI5b/B\uT<[eCnB2<;oUn-Z/-LBX#5<nEf3KU3PuUSh`fEmJsmq0BMUJF>IA:^R@72A396fk#H^JkF][=0%f6m3M<P%0PgTVlrRr(Ufb*ZgmB)r)lR('Lnr3:`>aDSd&RiE3TpIrWT('gYM~>endstream
endobj
xref
0 39
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000501 00000 n
0000000649 00000 n
0000000796 00000 n
0000000944 00000 n
0000001092 00000 n
0000001240 00000 n
0000001388 00000 n
0000001537 00000 n
0000001653 00000 n
0000001759 00000 n
0000001837 00000 n
0000002043 00000 n
0000002311 00000 n
0000002517 00000 n
0000002723 00000 n
0000002929 00000 n
0000003135 00000 n
0000003222 00000 n
0000003525 00000 n
0000003599 00000 n
0000003702 00000 n
0000003808 00000 n
0000003926 00000 n
0000004036 00000 n
0000004140 00000 n
0000004243 00000 n
0000004380 00000 n
0000004479 00000 n
0000004575 00000 n
0000005507 00000 n
0000006169 00000 n
0000006397 00000 n
0000006625 00000 n
0000009652 00000 n
trailer
<<
/ID
[<bea06f961b571b40c7cf4c938ad3324f><bea06f961b571b40c7cf4c938ad3324f>]
% ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R
/Root 21 0 R
/Size 39
>>
startxref
10949
%%EOF

79
COMMERCIAL.md Normal file
View File

@@ -0,0 +1,79 @@
# Commercial features (developer-gated)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-22 |
| Last modification date | 2026-05-22 |
| Co-authored | |
| Severity | medium |
| State | postponed |
| Document type | draft |
<!-- doc-meta:end -->
\newpage
\newpage
---
Ads, in-app purchases, and Play Store helpers are **disabled for normal users**. Turn them on only in **Developer settings** while testing monetization.
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Developer toggles](#developer-toggles)
- [Before production](#before-production)
- [Code map](#code-map)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Developer toggles
| Setting | Effect |
|---------|--------|
| Enable in-app ads | Initializes `InAppAdCoordinator` (placeholder UI; AdMob not linked yet) |
| Enable in-app purchases | Connects Google Play Billing; placeholder SKU `androidcast_premium` |
| Enable Play Store integration | Allows listing intent + in-app review API |
Changing a toggle calls `CommercialFeatures.refreshFromPreferences()`.
## Before production
1. Create app in **Google Play Console** and add in-app products matching `InAppPurchaseCoordinator.SKU_PREMIUM`.
2. Add **AdMob** App ID to `AndroidManifest.xml` and replace `InAppAdCoordinator` placeholder with real ad units.
3. Privacy policy + ads declaration if using ads.
4. Remove or hide developer toggles in release builds (optional `BuildConfig.DEBUG` guard).
## Code map
- `com.foxx.androidcast.commercial.CommercialFeatures`
- `InAppPurchaseCoordinator` — Billing Client 7.x
- `InAppAdCoordinator` — stub
- `PlayStoreIntegration``market://` + Play In-App Review

204
COMMERCIAL.pdf Normal file
View File

@@ -0,0 +1,204 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 7 0 R /F4 8 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 9 0 R /Fit ] /Rect [ 45.68504 762.0394 110.157 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 9 0 R /Fit ] /Rect [ 45.68504 751.0394 108.829 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 9 0 R /Fit ] /Rect [ 45.68504 740.0394 82.58904 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
9 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R ] /Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
11 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Outlines 16 0 R /PageMode /UseNone /Pages 20 0 R /Type /Catalog
>>
endobj
15 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115604+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115604+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False
>>
endobj
16 0 obj
<<
/Count 3 /First 17 0 R /Last 19 0 R /Type /Outlines
>>
endobj
17 0 obj
<<
/Dest [ 9 0 R /Fit ] /Next 18 0 R /Parent 16 0 R /Title (Developer toggles)
>>
endobj
18 0 obj
<<
/Dest [ 9 0 R /Fit ] /Next 19 0 R /Parent 16 0 R /Prev 17 0 R /Title (Before production)
>>
endobj
19 0 obj
<<
/Dest [ 9 0 R /Fit ] /Parent 16 0 R /Prev 18 0 R /Title (Code map)
>>
endobj
20 0 obj
<<
/Count 5 /Kids [ 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
>>
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 841
>>
stream
Gat=*?#Q2d'Re<2\;t@#\A#s)n<deSZmjPT"RW0A.%$#E5XqDFrr!6SiF<1F#UL8[4m*k.Y!3JrH*I5(6i^S!G^)P?#&XF^#mYY\O7))T"@"-A+OV$iK2RS'bM`Y<L'b8U,qL1),H7u".)?m`OM4Et-_Aa7E$LQu!K&')@mpGe<lEa)h">*e8>8YQS']nVTY2mPoc@G;^AcL6A>do0S!Rjhp$N=FGB!"ZeqP-O0BP9m=<\?b@R8D(#i2qifHT=R)%5uS'j>]1@JZ38&M/Ap5<l:dhrdWFq3@Zq#so59X&;8?5ha4N@u4a_1u<"_>ehn!QlX&*8`PDQ<'tVC3/a\l@J!e5.1&"VgBpBQ1HW&WX6[Fh(1)H\f=K([Ibo+U0X/a3@/mDFdqKge,qZa?[Df/r:f`m'/?;Tc038bn,b1UAL&%(Ec#nfPM(VE5B.DCPO:[lfkBZb_8I*K$#3n0@@[IC,m-P<E^V'-Q)<+SQH_isDI@.t)L#9\*4]9gD)]#F++H9@ic(9/cn+[$E1-)+=``S:>3SK4a\'H0hr%JC8@jWlne"@$:n\uq4YH2V)@''ddh=!@+UM$kj#@JH^L[4cUF%=ppo;K\Q*:;*6)tZS>'CV*4[S,U$j,LZ\8\ZUA4OS3RfjQg)ok8e=on)N=mgo#:T+IJ@0$G?3e#pEQCC9u&Sb>p/)ATG[=I2C^3C$g)Zk:pr\Nj-L(7IUl-7c5%bop8cS/02F0rbCS2+"__Z:==!K%C:/Ks2K,g9N12Ecq:X"n)"\c1U2HrCiq2&oF`$.o@9`8a*UR"4JFao>GGZK!5qTgMFDo&:nM]f>oDX",Ku$^uQT@QZob~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 435
>>
stream
GatnbbAP0N&-^ED?ZDF>$feMP39dTYUe7;oZKrnIk#"I83WX]E-6Ue>$WJ9BYnTu]mYhh]pPYJQ[K=#H3^WrT,Z@OT![K!!P1Fi8%[DTS@AJ,JXYo,o%fCPMR.3^<-,F+3EZK^F-/sI5AhAK[*?!j-<o+Am6\q;[:=@JD^eU$FU,GGGh')S\EkeqfrOV'Fr#`Z.o7X(,O;bfN2C>o0EN&8@QCD[^+MO,t2,ifL8nTO;XBsQ=$=--1dX-.HYY*)S:r/siZpQJ_#\)2T^o()#ifJ,jT;?"3T1_L0\bPTH:\J\X/i+%%4cdX9\),ToLoi$!&43-Ic(L.OMIhLDF\=7m]_%!3/Bl\T$M?N`;6A$i]%We,`YE3J[)hC\<U?eiV?8"_icO(54jGTjiSfReeUisQ;<6mm'N7CsdnX,?"80]#lN``ta\D~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhM``PkDdF-fp`Q&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:emFgZ@onCPq82Ws8G-_Tt6^R9oWtig34W3;D"0MP=sG_Q,?.RGJoD!mbh^0(JJ'S*<~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`SgmmFhnNfa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B](9((,pZH62[FH7Fl==&J^W3%Z>&B5W+giG*2MH_7OtUjbJt$1U9Q2!n~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1582
>>
stream
Gatm;=``R^&:WeDoHTc?'MH/aSd7?6SP9hja;\,XMCh(LZGkHCa8GdN?c4o@#WkJ,Z.8.";tnm\$3krmnJt!t(DAdCVEc"mAj@qW_;nt5Re2(%BI.BG`:7Z]!nVW%1Fs@:p$T6q\Fs+a<t@1;[4uF5,Q_]l44g*/oQ#RRnO_li+,&lc+QG8l>f(NH":Le.I)=nT`eqOI6`(Lt$_`k18V,pj':A\_9rujo%qWnUPR(]p'8UcDE<WBWKe'^V@\:GHQ,Jb96j,Qj5P9bSgY+:!ZVVQW5q906`q-VWGZdQ`+;8J'=tEF_5X3]%aQ.928KF#k.DgA^Kbu&3CX?ri*M:u]D=%mRLhdja%KVc<NjHn3-&,"YD?8K:e9Cg2!]DbR`c_VA@PcX#(9i2GphY]lb9<JERG@q3]<smcpLp.Fk['BsXLN2GhYUGgH9PCSm6!>J/!4j'ULB)u*]%XHM!7!UlN+h@I>aif?B!WD"24=5UWe9FX92/Pfb[VUIju&bD2=j[V$JO"=jXl`RnS1`V@C4PD3sL]p)1A/l*CnSDh@AC&_:'$ilZ:Df*_G'J,c4DQ;.RNabXFda/H_@bs[Z$1cMo,`Ru8ijW%ZiDA\=R6E*P5LY[6\Nlp/"I7=/22=\5Hff+e"-Fo;S79Yn:<Qa7J@(@816`lPK`Z,k89gYZV9`%K%"0p3&`:SHtI>'t3X.PLR1/Kb*.Ddqu]npQOdM[TBbkchaX@[b'8gW):$DjGV@m'](DR@Cr/UP?g<#F]o.l9P:q"@g:n;\d&O[RAHi+_n5R>=D:U0US8M+"+9aZ't*UhBl-nbBK)JPu=j`=jkm:LPg"i3Y\*6p+-4!e^#VMsZ#`s$r,pbdD6omHa^F%pS[!9jH(-J)HlLLLO]Pq;u2I?*5?U>n+)G@fc&`"_>']!oDH887@J]]IOd8o1L3I9C&(idTorCF\IkK^<tc$*/8QB$+,k\j`AYkH"OSEnN9#U??Htg\5FD@I&Nf2,QSOn[G]^f&Lg;i$nmP)6q:bb/J8X#,+\G.l>93Mqr\>+8%+Ehp3"=^1!:5URHST)m*+L6CE?b+qJ!_nJRO-I#fos>j$_HLHt7k//(#E[nmRZ9iL5m$f2IKM(+f$_fZ(,TMROKT99;NTO708i7ut,L@.)J'dtSHFn?<p[60diNd<?F5@Wo/s)O+cVb@<QMnW^[UC)^'--tr2c9rH_#AD7og/Pm,+g,t$'[^,N:KH-bL-QTQq6FA^UA(ERFN"c$2K/ZnI-1Q#EpLX\"TQnSg+D3\1(W@13jH"-tIps1EHP,LT!VkNVOJ+p8>4Z9rc&#6uZOEF+;PCSK^0]s),&f.306.(en<of1bH$QARl:39'i2^2aWYZO:'00OrBlQq7!s91&]<C1oJhQl7rM"*[uEYG,;R`OYGBRR8['O6nrYa&HYN<<FsrCEZAUj!a#.GPXX=i#.[bKqQb4BtZH)quYkq)*E&PsA:C"CbYHE?jd@ApKl"SWcT%X[_p-h+>.C-_7ceH=7A[`MqB/s&M(K8_:^kcc'MQ`oElh`b4Tacd7lD:3eWbo>#HMGY'Cgp?.'iF^B3!XfJ%eT`&^03Hn~>endstream
endobj
xref
0 26
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000487 00000 n
0000000633 00000 n
0000000780 00000 n
0000000895 00000 n
0000001000 00000 n
0000001205 00000 n
0000001441 00000 n
0000001647 00000 n
0000001853 00000 n
0000002059 00000 n
0000002146 00000 n
0000002460 00000 n
0000002534 00000 n
0000002632 00000 n
0000002743 00000 n
0000002832 00000 n
0000002920 00000 n
0000003852 00000 n
0000004378 00000 n
0000004608 00000 n
0000004836 00000 n
trailer
<<
/ID
[<d086ae4cb3335d3ad3b7f0baa24fd954><d086ae4cb3335d3ad3b7f0baa24fd954>]
% ReportLab generated PDF document -- digest (opensource)
/Info 15 0 R
/Root 14 0 R
/Size 26
>>
startxref
6510
%%EOF

78
CRASH_REPORTER.md Normal file
View File

@@ -0,0 +1,78 @@
# Anonymous crash reporter
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-20 |
| Last modification date | 2026-05-20 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Android](#android)
- [Backend](#backend)
- [Report files](#report-files)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Android
- **Main process:** `CrashReporter.install()` — any uncaught Java exception (cast or idle UI) → `files/crash_reports/pending/crash_yyyyMMdd_HHmmss.json` (same stamp as `session_*.json`). Active cast session stats are flushed with `globals.reason: "crash"` when applicable.
- **Runtime context:** every report includes `runtime_context` (foreground activity, `CastActiveState`, last `session_*.json` name) and `scenario` (`cast_session` vs `app_runtime`).
- **Watcher process:** `CrashWatcherService` in `:crashwatcher` — reads `settings.json``crash`, uploads pending reports (JSON body gzip-compressed), ingests native stub files.
- **Native:** `crash_hook.c` (unwind + `dladdr`) writes stubs to `crash_reports/native/`; watcher converts to schema v1 JSON.
- **User toggle:** Settings drawer → **Send anonymous crash logs** (`AppPreferences.send_anonymous_crash_logs`, default on).
Deploy tuning via app-private `files/settings.json` (see `examples/settings.json`):
```json
"crash": {
"enabled": "true",
"upload_url": "https://f0xx.org/app/androidcast_project/crashes/api/upload.php",
"prune_after_upload": "true",
"upload_interval": "5m",
"max_pending": 32
}
```
## Backend
PHP console and ingest API: `examples/crash_reporter/backend/` — see [README](../examples/crash_reporter/backend/README.md).
Production nginx/hosts: [INFRA.md](INFRA.md).
Roadmap: `examples/crash_reporter/ROADMAP.md`.
## Report files
| Pattern | Example |
|---------|---------|
| Pending crash JSON | `crash_20260520_153045.json` |
| Session stats (compare) | `session_send_20260520_120000.json` |
JSON fields: `schema_version`, `report_id`, `report_file`, `generated_at_epoch_ms`, `scenario`, `runtime_context`, optional `session_context`, `java` / `native`, `device`, `app`, `build`, `fingerprint`.

210
CRASH_REPORTER.pdf Normal file
View File

@@ -0,0 +1,210 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 7 0 R /F4 8 0 R /F5 9 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 762.0394 73.25304 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 751.0394 76.81304 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 740.0394 86.14104 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
9 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
10 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R ] /Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
12 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Outlines 17 0 R /PageMode /UseNone /Pages 21 0 R /Type /Catalog
>>
endobj
16 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115604+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115604+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False
>>
endobj
17 0 obj
<<
/Count 3 /First 18 0 R /Last 20 0 R /Type /Outlines
>>
endobj
18 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 19 0 R /Parent 17 0 R /Title (Android)
>>
endobj
19 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 20 0 R /Parent 17 0 R /Prev 18 0 R /Title (Backend)
>>
endobj
20 0 obj
<<
/Dest [ 10 0 R /Fit ] /Parent 17 0 R /Prev 19 0 R /Title (Report files)
>>
endobj
21 0 obj
<<
/Count 5 /Kids [ 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R ] /Type /Pages
>>
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 836
>>
stream
Gat=*gQ'uA&:N^lk)lg"fAlXh4OPh9W^:G:U:sY?QmF8Gcm3GHhq&>WE[cYE7:=*+F6<7B*#jHt#E/U^"Rt*"a(GNci,8TUiS`$jk)]M#n0=)(,$dJp.&@2l4Do+MQr',0_?&Y@Ot%9('$TAAl0HE0i]2u"5;FZ)TWfj^ZO'H1;WaEfIgtFPSA`Gr3Fo?dK5:pphC!$?dneo:Jc>_6B"X9#@\-4*,na'H'GN]ecu.SN*?n6&dAf&^:tm^[nT1-en?eQ8^\qK_If<IEZNZ3:V5b"C64r@[6:O+']#I"qK4O=2hW\Qe@?*%c*b!-cFi(WjRg6iFT/)/K9/34bjNqd;'YVcXh*\8]o?)E)q/'I$/gIrQM`nqm/S]q:^HkPkF';1>FLCFmQ!9o[0(2bCU#'(Y,SWd/7<oF8Y@)S$/`o+JQ?gMMW7rO?ee:dhq)1fkbiK?"3)="NDLsjhNr!^CkCM7*VZul^^P>sZ`4(HX_2TI5eR0>f;B=gf.]e98]sJUJ%SskgP7oDmN-=ENK5Ht\=Un<7%QE?]a9761l88T+h"YCm'\0=4lrBb5hg5jYi0c.h]2$9AGWXf%mRXP!6e('+%X)NRpKGu'`*j)O1jMf<Q?<HSCr)HKFbk.06[mhbcoma\f;ib)WdT)Bqn)[;NJPal(ZmX(C):7%Ka%IIchs_&PO.TFQGjE;&(!d)XTd9BXq98J6[abjAG0J/+/0LmCOj&)UDa\D+_1Zu.g0c5G_L)T#cFQ#.B>iZ=/XTS\FW?Rf9^9^1l7AdFKsW/6ubU@U#!/;9omAVF0emU_GLGAoAO84#Xl/3:lZ,/5r8oL7fNIUic[Z~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 427
>>
stream
Gatn"_,%p%'ZTV>(#KTdPeM<H2fG?#%jp<"dDX7+A264$LA^q76&5RI@`V4=P4nSI^@.mEE$*T4fb,,=oL8rr0JcP@!gk>D:$8QEqoV1/0N(#`es^Rr#QC;ZAip](8MD$;>Xr1*M)ZVW+!'7&%N+1Z9@"/rL(IQUf;.kh76*_9]#M=Y^4H*5\_XPuI%95Pr#^<s_\8cq+gpsVRoX)a]mq!=f4#6<Cgl-G9Gq,oRFkMB243KZ]k"oV0W@'L_<21/Z"RM&PpoIJ83"mBb*^msPL-Ei^>8;D-KVh]llGMXroET$)31=&1JHGAMH:rXC7@)+ktpGY;.EO*$fhfXLCq7A]<P@qAtCoZf;mJ5mb^n'dK6Dkk%.Q!'>mKK6or8+h?cBi/nX)A'E#/0Uh1>:q$Y.U;:Je7$M:"jE7"C]VFSRS~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhM``PkDdF-fp`Q&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:emFgZ@onCPq82Ws8G-_Tt6^R9oWtig34W3;D"0MP=sG_Q,?.RGJoD!mbh^0(JJ'S*<~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`SgmmFhnNfa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B](9((,pZH62[FH7Fl==&J^W3%Z>&B5W+giG*2MH_7OtUjbJt$1U9Q2!n~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2014
>>
stream
Gatm<D/\/e&H;*)E<n86ku(Z[hJDXZ\u'8$VG36gqdMfu,hc9G;(<kPabKcg*C\YdojU#!P.%O3j1RR!I-tYL-N91nh]MrEG71/V":0_93=srLiV!Xag^\cC81eO;L-'QP)n_#C:t,,Whr&jjJ7ViS8bNFWL1<5e^VJJX4OY;=&[78"lg?J^VM8dsVdI4o2Zc2K(d?Uq63u?n5cK?CoRU&ho4leY\U`YL#\=p.Vf6tn[r2sQhg9/^OA;,OKUH"uUWa9JI=A0B+7IpO&^Q*PMXmKF*+#pA5UOs1kP;JkX#CO?Z=paor`ugWH-NZp(0hc?a9rd(Zf18X\Sjs:#>@6G.0<O&Q3DYJ.gGo+i)pYg!bT?#jTVFmU+ka!j4!@:D9UC"emSu"e8]^*YEog#BA_iKbk"])4S34H@QqrE#U?UQh`g#Fj^f;*+$V)DSf@EL.U%iCF-^d\^lf+.h?4rkOO/FU:"SO\W<)0J&Zn#l_:?Cp=cifM*9M7/0DQs@_hY0m2GGa^oT$o$+ZVn';`LbH5A<1TYiGjfQGl_:mUC&RXrS;h;.&]%lP5\+?&BgmY_#ggr39oM%ps>P"?I>1M'+E<hEk1V-%;tlSWA%Gq*LsT#_=FDUZ1O><L4!#7fEUsjp`VL&?Boc,aBLV$EteDfHb6JB>=\7;;1`O@7!tF?e]9%Y?FJ)1Ar,NG$uDR?B>*#l2?e\_32tXEC7G&L<ER21rI*IBPs8>`VlHRFcm"OK<XAQ('U;qmN_X=)JOn'3RoUR15f)m92-mY@HX$OcSsaq_He'DD%?:!mjNN%Ue)mF3?HI@cS5+tcV[rOMBVPb]Ru*JNleCr]3C2frE$9'f?0"=3RtdQ9[gb8TO$r]G_kigrn?-B`[hOffu1f#+JSuE5Xh83O`h[J;n%KqeBmb*e?u#A#O/(`']o*:@B^rK13!_o%d@o,YJTm$N]n3B@M-(8]rA%I6HL9Fm.COn[W]P')i@TW-#&c7KcJ%q,!SUik?2sSq4()1n5(&a.e5NUI%>j!7;!2&3@eV+f]"kKZ:Wq-F;Ws1*08ee'W"XhC_\\Tkh]o,=s:F8b=;7)]o7J9S[\=_7r7^N[a%XGLLYJHc6h+L'Y:8;=AeSWMFf^.LU@MA4jbZ+IpD[(\[%$jY1rmhrn3A>^^2n=D"8a`4t1$a:X6Bb2)4p'N7Id1.'e4])U6OI7jF:W&Rm\.HGHU#'(Yi]%gkH6@Jh[77TtM./[Ja5))`c.ZT4%m^6[I<6A.L,Y#se@PO+/Pm*W#9':1;^=o3CJiD+d"iON"hm-`fm]bYkOjSb)BIrTJHjR$&o8*RI%\=g#h?7Eb6;]Kp7'm20[o;b[9hQkq66S:lQg<h5jl6`LQqtf=MrT2!18(40W(0]PU8_;MGA>1TBL1O2@n6uuda3cZ]VDa-(?U%ef)bG:5IH%m\a_NH6KOmZkG5VkI3PRo,)SdBPW'.Tc*'I8%qMqBKH`nT:XD,S\bq2#33Mr*PJRM8Xdi3qu3HS^DPp1e6X9gpI0dr_J]Z%EVh(%9S9(cDHi(Ac\;5pmqq,qW&\UAe)gH="o'AC%?><qr7PHLcd_*jWQi^bMEiLq#E>gn+\l3gl,%Fs51<?^u>ZgM%E/WuZ4lCh%`@Prl[NpA)Dp9k]Agsi.&ktohVNb)\-DYdD#22(FpZ/"^$>OeSNF"H0SlWaAf<4t,_oe!6`7%OLU-N<W@p)3ne%u`79Ckk:IoD4LKZH'lZYU%P+NDD:a<"%j^HZSAXq(0!$dB*VF[MJAYH3"Q'ecJ&""\L-\-`(eR:b2J#b<qS.9I>,#R^.d^&SMg-TpoD3GF:r`%eCI8lFGWh3P4;UgU`&RcLmqZe+O(tE]bVYi.=TAqJ\tWI,#JgrM2TG[bU;`T==dOqpiS%Ut&OH/FNDtTP%X/$\Nf7@a2VMC2298SjP\s]]3JWP/<@MON:]F`<S1V=+cCK_-8akCq!3XZO-/,BRW/:D7($m^*_X;3d%[DaV^KfN[>5Zj(\o<3XCnT@g<5<!L!JO$i~>endstream
endobj
xref
0 27
0000000000 65535 f
0000000061 00000 n
0000000132 00000 n
0000000239 00000 n
0000000351 00000 n
0000000499 00000 n
0000000647 00000 n
0000000795 00000 n
0000000910 00000 n
0000001015 00000 n
0000001092 00000 n
0000001298 00000 n
0000001534 00000 n
0000001740 00000 n
0000001946 00000 n
0000002152 00000 n
0000002239 00000 n
0000002538 00000 n
0000002612 00000 n
0000002701 00000 n
0000002803 00000 n
0000002897 00000 n
0000002986 00000 n
0000003913 00000 n
0000004431 00000 n
0000004661 00000 n
0000004889 00000 n
trailer
<<
/ID
[<8fa35525e151cb86fcb8749d7347816b><8fa35525e151cb86fcb8749d7347816b>]
% ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R
/Root 15 0 R
/Size 27
>>
startxref
6995
%%EOF

View File

@@ -0,0 +1,394 @@
# URL shortener service — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-11 |
| Last modification date | 2026-06-11 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | DR |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20100611_3_url_shortener.txt](../drafts/20100611_3_url_shortener.txt)
**Specification:** [docs/specs/20100611_3_url_shortener.md](../specs/20100611_3_url_shortener.md)
**Date:** 2026-06-11
**Status:** Reviewed — ready for post-alpha implementation planning
**Severity:** Medium
**Documentation index:** [README.md](../README.md)
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Resolved open questions](#2-resolved-open-questions)
- [3. Authentication and trust](#3-authentication-and-trust)
- [3.1 Bearer token](#31-bearer-token)
- [3.2 Signer](#32-signer)
- [3.3 Shared session (operators)](#33-shared-session-operators)
- [3.4 Trust flow (normative)](#34-trust-flow-normative)
- [4. Slug algorithm](#4-slug-algorithm)
- [4.1 Decision: SHA-256 truncated](#41-decision-sha-256-truncated)
- [4.2 Collision policy](#42-collision-policy)
- [4.3 Draft example (preserved for comparison)](#43-draft-example-preserved-for-comparison)
- [5. API and flow corrections](#5-api-and-flow-corrections)
- [6. Data store](#6-data-store)
- [7. Deployment and infra](#7-deployment-and-infra)
- [8. QR codes](#8-qr-codes)
- [9. Security model](#9-security-model)
- [10. Integration with androidcast](#10-integration-with-androidcast)
- [11. Estimates](#11-estimates)
- [12. Task dependency graph](#12-task-dependency-graph)
- [13. Suggestions (PO / dev discretion)](#13-suggestions-po-dev-discretion)
- [14. Remaining open items](#14-remaining-open-items)
- [15. Changelog](#15-changelog)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Executive summary
The draft defines a **standalone URL shortener** at `https://s.f0xx.org`, separate from the crash-reporter BE app, with bearer/signer trust, TTL/permalinks, QR support, and a JSON shorten API.
**DR outcome:**
| Area | Decision |
|------|----------|
| Priority | **Post-alpha**; medium severity; may move up if BE load stays low |
| Slug hash | **SHA-256 truncated (12 hex)** — reject draft interleave + Java `hashCode` |
| Auth | **Bearer tokens** for API; **signer** for permanent TTL; operator **session RBAC** only for token admin UI |
| Placement | `backend/url-shortener/` scaffold in repo; dedicated nginx vhost; **opt-in** deploy |
| Admin UI | Hub **Short links** card (not separate admin skin) — SPEC §20 |
| Infra | Git remote + FE TLS **live**; nginx snippets in repo |
| Stack | PHP + nginx + MariaDB per draft |
| OpenAPI | **Required** in v1 |
---
## 2. Resolved open questions
Questions from draft §“TBD and open questions list” and inline `TBD` markers.
| # | Draft question / gap | DR answer |
|---|----------------------|-----------|
| Q1 | Token signing, trust models, sessions, security | See [§3](#3-authentication-and-trust) and [§9](#9-security-model). Machine clients use **bearer** (+ optional **signer**). Humans use existing BE session only to **mint** bearers. Optional HMAC for GET left as [suggestion](#13-suggestions-po-dev-discretion). |
| Q2 | `... TBD — open part` | Filled in SPEC: audit log, rate limits, SSRF blocklist, normalization rules, collision policy, admin tables, smoke tests. |
| Q3 | Database `*... TBD` columns | Full schema in [SPEC §9](../specs/20100611_3_url_shortener.md#9-database-schema): `origin_hash`, `expires_at`, `access_count`, FK tables for bearer/signer. |
| Q4 | Java hash vs SHA-256 | **SHA-256** (see [§4](#4-slug-algorithm)). Java hash is 32-bit, non-cryptographic, JVM-stable but poor for URL slugs. |
| Q5 | Interleaved URL+TOKEN string | **Rejected** for production. Deterministic `SHA256(bearer_id ‖ normalized_url)` instead. Draft interleave kept as historical note only. |
| Q6 | Authentication: “app-wide shared session, project-wise token” | **Split roles:** `curl`/services → **project bearer token**; console operators → **shared `ac_crash_sess`** to create/revoke tokens (same RBAC as crashes/tickets). Shorten API does **not** accept PHP session cookies. |
| Q7 | Submodule remote | **Done**`git://f0xx.org/androidcast_project/url-shortener`; pinned at `backend/url-shortener/` submodule (`next`). |
| Q8 | OTA-style “invalid URL” on unrelated services | N/A here; shorten service validates URL scheme/host and returns `INVALID_URL`. |
| Q9 | Step 9.1.1 / 9.1.2 trusted sub-capabilities | `can_temporary` on bearer; `can_permanent` on bearer **and** valid signer required for `ttl=0`. |
| Q10 | Cached hit when TTL “exceeds given limits” | If stored link **expired** → error `TTL_EXPIRED`, not silent renew. If valid → return same slug with **remaining** `ttl` in response (draft step 8). |
| Q11 | Estimations (draft empty) | See [§11](#11-estimates). |
---
## 3. Authentication and trust
### 3.1 Bearer token
- Opaque string (≥32 chars random), stored as **SHA-256 hash** in `bearer_tokens`.
- **`trusted=1`** required to **create** new links (draft steps 67).
- **Demo token:** single `trusted=0` token that can only resolve existing slugs in docs/examples — cannot create (403).
### 3.2 Signer
- Separate long-lived secret for **elevated** operations.
- Required when `ttl=0` (permanent permalink).
- Validated against `signer_keys` where `can_permanent=1` and not revoked.
### 3.3 Shared session (operators)
- **Frozen:** hub **Short links** card (SPEC §20) — same nav shell as Remote access / Builder.
- Actions: create bearer, revoke bearer, list links for bearer, purge expired.
- Uses same RBAC roles as [REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md) pattern (`admin`, `operator`).
### 3.4 Trust flow (normative)
Matches draft low-level flow with SPEC clarifications:
```text
Request → bearer present? ─no→ 401 MISSING_BEARER
→ lookup (bearer, url)
→ hit & valid TTL → 200 cached (remaining ttl)
→ hit & expired → 409 TTL_EXPIRED
→ miss → bearer trusted? ─no→ 403 ACCESS_DENIED
→ ttl=0 → signer ok? ─no→ 403 SIGNER_REQUIRED
→ compute slug, insert, return 200
```
---
## 4. Slug algorithm
### 4.1 Decision: SHA-256 truncated
**Chosen:**
```text
slug = hex(SHA256(bearer_id + "\x1f" + normalized_url))[0:12]
```
| Criterion | Java `hashCode` + interleave (draft) | SHA-256 truncated (chosen) |
|-----------|--------------------------------------|----------------------------|
| Collision resistance | Poor (~32 bits) | ~48 bits with 12 hex chars |
| Determinism | JVM-specified but weak | Standard crypto |
| Reversibility | No (good) | No (good) |
| Debuggability | Opaque interleave | Simple test vectors |
### 4.2 Collision policy
- Unique index on `slug`.
- On duplicate insert: append `-1``-f` (4 chars max) **or** re-hash with `:\x1e{counter}` appended to material.
- Log collision to `audit_log` at `WARN` level.
### 4.3 Draft example (preserved for comparison)
Draft interleaved `https://t.co` + token → `htta5bpesf:4/3/3td.2c3oa` → Java hash or SHA slice `4623b436f`.
**Do not implement interleave** — kept only to show original intent (per-token namespacing). Namespacing is achieved via `bearer_id` in hash input instead.
---
## 5. API and flow corrections
| Draft item | Correction |
|------------|------------|
| `GET https://s.f0xx.org?url=...` | Use **`/api/v1/shorten`** path so root `/{slug}` stays redirect-only |
| Response `ttl` as string | Keep string in JSON for draft compatibility; document integer seconds |
| `signer` on GET | Optional query param; omit from success body (security) |
| Fail body echoes `bearer` | Truncate in response to first 6 + `…` in production logs; full echo in JSON only for client debugging (SPEC) |
**Routing suggestion:** nginx sends `/api/` → PHP front controller; slug paths match regex (quoted on BE: `location ~ "^/[a-f0-9]{6,16}$"`) → redirect handler.
---
## 6. Data store
| Option | Pros | Cons | DR pick |
|--------|------|------|---------|
| A. Same MariaDB instance, new schema `url_shortener` | Simple ops, one backup | Shared failure domain | **Recommended v1** |
| B. Separate MariaDB database | Blast radius isolation | More admin | If PO wants hard isolation |
| C. SQLite on BE | Zero MariaDB migration | Poor fit for BE pattern | **Reject** |
Retention cron: `scripts/purge_url_shortener.php` (mirror `purge_remote_access.php` style) — delete expired rows + audit >90d.
---
## 7. Deployment and infra
Per [INFRA.md](../INFRA.md) topology:
```text
Internet → FE (s.f0xx.org TLS) → proxy_pass BE :80 → url-shortener php-fpm
```
| Step | Owner | Notes |
|------|-------|-------|
| DNS `s.f0xx.org` → FE | PO / infra | **Done** (with TLS) |
| FE TLS cert | PO / infra | **Done**`/etc/letsencrypt/live/s.f0xx.org/` |
| FE nginx `server_name s.f0xx.org` | DEV | [nginx.fe-s.f0xx.org.snippet](../../backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet) |
| BE vhost fragment | DEV | [nginx.be-url-shortener.fragment](../../backend/url-shortener/deploy/nginx.be-url-shortener.fragment) |
| Git remote | PO | **Done**`git://f0xx.org/androidcast_project/url-shortener` |
| Licenses | DEV | Add QR library to BE license inventory when impl starts |
**Safe deploy:** nginx fragment only; no topology changes to router/FE↔BE path. Validate with `nginx -t` before reload.
**Not alpha-blocking:** omit from `validate_be_services.sh` until first deploy.
---
## 8. QR codes
| Decision | Detail |
|----------|--------|
| Must-have | Yes (draft) |
| Format | PNG via `/api/v1/qr/{slug}.png` |
| Library | `endroid/qr-code` (MIT) — **suggestion**; PO approves license addition |
| Inline in JSON | Optional `qr` URL field in shorten success — **suggestion** for v1.1 |
---
## 9. Security model
| Threat | Mitigation |
|--------|------------|
| Token leak | Revoke bearer; rotate signer; rate limit |
| SSRF via shorten | Block private/metadata IPs at validation |
| Slug enumeration | 48+ bits entropy; rate limit redirects (suggestion) |
| Open redirect abuse | Only whitelisted bearers create links |
| Session fixation | N/A — API is stateless |
**Trust model summary:**
```text
┌─────────────────┐
│ Operator (web) │
│ ac_crash_sess │
└────────┬────────┘
│ mint/revoke
┌─────────────────┐
signer ───────►│ bearer_tokens │◄────── curl / services
(permanent) └────────┬────────┘
│ trusted create
┌─────────────────┐
│ links │
└─────────────────┘
```
---
## 10. Integration with androidcast
| Consumer | Use case | When |
|----------|----------|------|
| BE remote-access UI | Short “open session” links for operators | Post-alpha |
| OTA / docs | Stable short links in printed QR | Post-alpha |
| Android app | None v1 — use HTTPS from BE | Deferred |
| Crash reporter | **No** embed — standalone service | Normative |
Submodule lives beside `examples/crash_reporter/backend/` conceptually but **not** inside it.
---
## 11. Estimates
Engineering person-days (single DEV, familiar with BE stack):
| Task | Days |
|------|------|
| Repo scaffold + OpenAPI + DB migrations | 1.5 |
| Shorten API + auth + TTL logic | 2 |
| Redirect handler + permalink cache | 1 |
| QR endpoint + license | 0.5 |
| Admin token UI (minimal) | 1.5 |
| nginx FE/BE fragments + deploy doc | 1 |
| PHPUnit + smoke script | 1 |
| **Total** | **~8.5 days** |
**Severity:** Medium — no alpha path dependency.
**Reorder triggers (PO):** high demand for shareable debug links; BE CPU <50%; remote-access operator UX needs short URLs.
---
## 12. Task dependency graph
Legend: **1** = highest priority within this feature.
```mermaid
flowchart TD
T1["1. Submodule scaffold + openapi.yaml"]
T2["2. MariaDB schema + migrations"]
T3["3. Bearer/signer auth layer"]
T4["4. Shorten API (POST/GET)"]
T5["5. Redirect /{slug} + TTL"]
T6["6. QR PNG endpoint"]
T7["7. Admin token UI (RBAC)"]
T8["8. nginx FE/BE + DNS s.f0xx.org"]
T9["9. Tests + smoke + licenses"]
T1 --> T2
T2 --> T3
T3 --> T4
T4 --> T5
T4 --> T6
T3 --> T7
T5 --> T8
T4 --> T9
T6 --> T9
T7 --> T9
T8 --> T9
```
| # | Task | Blocker | Assignee |
|---|------|---------|----------|
| 1 | Submodule + OpenAPI | — | DEV |
| 2 | Schema | 1 | DEV |
| 3 | Auth | 2 | DEV |
| 4 | Shorten API | 3 | DEV |
| 5 | Redirect | 4 | DEV |
| 6 | QR | 4 | DEV |
| 7 | Admin UI | 3 | DEV |
| 8 | Infra deploy | 5, PO DNS | DEV + PO |
| 9 | Tests/licenses | 47 | DEV |
---
## 13. Suggestions (PO / dev discretion)
Items intentionally **not** frozen in SPEC — recommended but optional:
1. **HMAC signed GET**`&sig=HMAC-SHA256(url|ttl, bearer_secret)` to prevent query tampering on shared links; skip for v1 if all clients use POST.
2. ~~**Console integration**~~**Done (spec):** hub **Short links** card (SPEC §20).
3. **Response field `qr`** — include ready-made QR URL in shorten JSON.
4. **Click analytics** — increment `access_count` only (already in schema); defer graphs to Grafana/nginx logs.
5. **Custom slug**`&alias=` for operators; increases collision/abuse risk — post-v1.
6. **Separate PHP-FPM pool** — if redirect traffic is high; share pool initially.
7. **FE rate limit**`limit_req` on `/api/v1/shorten` at nginx.
8. **Draft demo token** — ship `examples/url-shortener/demo_curl.sh` with documented read-only token for CI smoke.
9. **PDF export** — run `python3 docs/_pdf_build/md_to_pdf.py` on SPEC+DR when PO wants signed-off copies.
10. **Atomic commits on `next`** — per [bottomline_reminder.txt](../../bottomline_reminder.txt): one commit per task row above for easy revert.
---
## 14. Remaining open items
Requires PO / infra input (hard blockers for **prod** only):
| ID | Item | Owner | Status |
|----|------|-------|--------|
| O1 | Git remote `git://f0xx.org/androidcast_project/url-shortener` | PO | **Done** |
| O2 | DNS + TLS for `s.f0xx.org` on FE | PO / infra | **Done** |
| O3 | BE docroot `/var/www/localhost/htdocs/apps/s/` | DEV + PO | **Default in SPEC**; confirm on first sync |
| O4 | Approve QR library license addition | PO | Open (before QR impl) |
| O5 | First production bearer token issuance policy | PO | Open |
| O6 | PHP shorten/redirect handlers + hub Short links UI | DEV | Open (scaffold only) |
Soft blockers cleared for BE nginx + MariaDB local dev. Hard blocker for prod traffic: **O6**.
---
## 15. Changelog
| Date | Rev | Change |
|------|-----|--------|
| 2026-06-11 | 1 | Draft → SPEC + DR; resolved hash, auth, schema, API paths; task graph + estimates |
| 2026-06-11 | 2 | Scaffold `backend/url-shortener/`; openapi.yaml; nginx snippets; curl appendix; O1/O2 done per draft |
**Source linkage:** [docs/drafts/20100611_3_url_shortener.txt](../drafts/20100611_3_url_shortener.txt)

File diff suppressed because one or more lines are too long

676
DRs/20100612_1_scaling.md Normal file
View File

@@ -0,0 +1,676 @@
# Platform scaling and multi-environment architecture — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-12 |
| Last modification date | 2026-06-12 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | DR |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20100612_1_scaling.txt](../drafts/20100612_1_scaling.txt)
**Specification:** [docs/specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md)
**PDF:** [20100612_1_scaling.pdf](20100612_1_scaling.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh` (Mermaid → PNG diagrams; requires `npx`)
**Date:** 2026-06-12
**Status:** Reviewed — planning / brainstorming (no implementation sprint yet)
**Severity:** Strategic
**Documentation index:** [README.md](../README.md)
---
---
---
---
**Documentation index:** [README.md](../README.md)
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Architecture options](#3-architecture-options)
- [3.1 Option A — Dev cluster (draft default)](#31-option-a-dev-cluster-draft-default)
- [3.2 Option B — Tighter cost (minimal split)](#32-option-b-tighter-cost-minimal-split)
- [3.3 Option C — Cloud-first hybrid](#33-option-c-cloud-first-hybrid)
- [3.4 Comparison matrix](#34-comparison-matrix)
- [4. DR decision — recommended path](#4-dr-decision-recommended-path)
- [5. Current vs target diagrams](#5-current-vs-target-diagrams)
- [Today (single BE)](#today-single-be)
- [Target phase B (minimal split + HA web)](#target-phase-b-minimal-split-ha-web)
- [6. Service split rationale](#6-service-split-rationale)
- [7. Environment promotion model](#7-environment-promotion-model)
- [8. RSSH and connection capacity](#8-rssh-and-connection-capacity)
- [Assumptions](#assumptions)
- [Estimates](#estimates)
- [8b. Network bandwidth (DR)](#8b-network-bandwidth-dr)
- [Baseline risk (today)](#baseline-risk-today)
- [DR sizing decisions](#dr-sizing-decisions)
- [Planning numbers (single-site prod, until CDN)](#planning-numbers-single-site-prod-until-cdn)
- [8c. Global availability and GEO segments (DR)](#8c-global-availability-and-geo-segments-dr)
- [Policy model (DR decision)](#policy-model-dr-decision)
- [Phasing](#phasing)
- [Example use cases](#example-use-cases)
- [9. Future capabilities — priority weighting](#9-future-capabilities-priority-weighting)
- [10. Simulation lab design](#10-simulation-lab-design)
- [11. Cloud provider notes (GCP / Azure)](#11-cloud-provider-notes-gcp-azure)
- [12. RBAC evolution](#12-rbac-evolution)
- [13. Risks and mitigations](#13-risks-and-mitigations)
- [14. Estimates](#14-estimates)
- [15. Task dependency graph](#15-task-dependency-graph)
- [16. Resolved open questions](#16-resolved-open-questions)
- [17. Remaining open items (PO)](#17-remaining-open-items-po)
- [18. Suggestions (PO / dev discretion)](#18-suggestions-po-dev-discretion)
- [19. Changelog](#19-changelog)
- [Related docs](#related-docs)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Executive summary
The project outgrew **one Alpine BE VM** that hosts consoles, APIs, MariaDB, Gitea, OTA, WireGuard, RSSH, URL shortener, and Docker builds. The draft asks for **clusters**, **dev/staging/prod**, **zero-downtime deploys**, optional **cloud**, and headroom for **RT cast**, **VPN commercialization**, **payments**, and **ffmpeg** pipelines.
**DR outcome:**
| Area | Decision |
|------|----------|
| **Timing** | **Post-alpha** strategic track; does not block alpha soak |
| **Architecture** | **Option B → A** phased: minimal VM split first, then full dev cluster simulation |
| **Edge** | Keep **Gentoo FE** as TLS/DNAT anchor; add LB upstream to N BE web nodes |
| **Data** | **Dedicated MariaDB VM** before scaling anything else CPU-heavy |
| **Build** | **First split candidate** — Docker off shared PHP VM |
| **Cloud** | **Hybrid** — object store + staging dress rehearsal before prod migration |
| **RSSH capacity** | Plan **100 concurrent SSH** / **5k heartbeats** per bastion pair before commercial VPN |
| **Bandwidth** | Document **WAN / FE→BE / per-service** Mbps & GB-month budgets ([SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress)); CDN before horizontal web scale for OTA |
| **Global + GEO** | **Worldwide reach** default; **deny segments** per content (e.g. no serve in `ZM`) via layered enforcement ([SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) |
| **Future slots** | Weighted backlog in [§9](#9-future-capabilities-priority-weighting); SFU before ffmpeg farm; **F10 GEO** with F3/F5 |
---
## 2. Problem statement
| Pain | Evidence today |
|------|----------------|
| **Blast radius** | Submodule sync wiped gitignored configs; one BE restart affects all services |
| **Resource contention** | Docker build spikes CPU/RAM during PHP console use |
| **No promotion path** | Single prod-like env; staging OTA path still pending ([20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)) |
| **Scaling ceiling** | RSSH + WG + future SFU on same VM as MariaDB |
| **Bandwidth ceiling** | Single FE WAN + FE→BE path; OTA/APK egress competes with consoles ([SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress)) |
| **GEO gap** | No per-content region policy yet; required before global recordings/commerce ([SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) |
| **Operational risk** | Topology changes on FE/router are hard to roll back ([bottomline_reminder.txt](../../bottomline_reminder.txt)) |
**Success criteria:** PO can deploy unstable work to **dev**, beta-test on **staging**, and promote to **prod** with rollback per service — without rewriting mobile apps or public URLs each time.
---
## 3. Architecture options
### 3.1 Option A — Dev cluster (draft default)
Draft proposal: **N VMs per environment**, each role class (web+OTA, DB, object store, WG, url shortener+Gitea, build, +N for new features).
```mermaid
flowchart TB
subgraph Cluster["One environment cluster (e.g. dev)"]
W["Web + OTA VM(s)"]
D["Database VM(s)"]
O["Object store VM(s)"]
V["WireGuard VM(s)"]
U["URL shortener + Gitea VM"]
B["Build VM(s)"]
X["Future service VM(s)"]
end
W --> D
W --> O
U --> D
B --> O
V --> W
```
| Pros | Cons |
|------|------|
| Clear isolation; matches future SFU/ffmpeg | Highest VM count / cost |
| Easy to simulate on FE XEN | Ops overhead for small team |
| Aligns with cloud MIG later | Overkill before alpha exit |
### 3.2 Option B — Tighter cost (minimal split)
**Phase 1 prod shape** (recommended start):
| VM | Bundled services |
|----|------------------|
| **be-web** | nginx, PHP (hub, crashes, graphs, builder **UI**, OTA **metadata**) |
| **be-data** | MariaDB only |
| **be-ops** | Gitea, url-shortener |
| **be-build** | Docker builder |
| **be-vpn** | WireGuard + RSSH bastion |
```mermaid
flowchart LR
FE["Gentoo FE"] --> WEB["be-web"]
WEB --> DATA["be-data"]
WEB --> VPN["be-vpn"]
OPS["be-ops"] --> DATA
BUILD["be-build"] --> DATA
```
| Pros | Cons |
|------|------|
| ~5 VMs vs 7+; fits “money-wise” draft | Gitea + shortener still coupled |
| Unblocks build/DB contention immediately | Second web node needed later for true HA |
| Low risk to current URLs | WG still single point until pool |
### 3.3 Option C — Cloud-first hybrid
Move **staging** entirely to GCP/Azure; keep **prod** on FE→BE until hybrid LB proven.
| Pros | Cons |
|------|------|
| Managed LB, DB, object store day one | Egress cost; UDP/WG complexity |
| Dress rehearsal for full cloud | PO learning curve; vendor lock-in mild |
| ffmpeg/SFU elastic scale | **Violates** “safest topology” unless staged |
### 3.4 Comparison matrix
Scores 1 (poor) 5 (excellent) for **current team size** and **alpha proximity**:
| Criterion | A — Full cluster | B — Minimal split | C — Cloud-first |
|-----------|------------------|-------------------|-----------------|
| Cost now | 2 | **4** | 3 |
| Time to first value | 2 | **5** | 3 |
| Zero-downtime readiness | 4 | 3 | **5** |
| Topology safety | 4 | **5** | 2 |
| SFU/ffmpeg fit | **5** | 3 | **5** |
| Simulation fidelity | **5** | 4 | 2 |
| **Global CDN / GEO** | 3 | 3 | **5** |
| **Weighted total** | 25 | **27** | 25 |
**Weights used:** safety and cost now > cloud elasticity pre-alpha.
---
## 4. DR decision — recommended path
```text
Now ──► Option B (minimal split) on-prem HVMs
├── Simulate Option A as DEV cluster (37 Alpine HVMs)
├── Add be-web-2 + FE upstream when metrics say so
└── Option C for staging only → then hybrid prod
```
| Step | Action | Owner |
|------|--------|-------|
| 1 | Document service boundaries (this SPEC/DR) | Agent / PO review |
| 2 | Move Docker builder to **be-build-1** | Sysop |
| 3 | Move MariaDB to **be-data-1** (hardest cutover) | Sysop |
| 4 | Stand up **dev cluster** simulation on intra network | Sysop |
| 5 | Staging promotion runbook + DNS prefix | PO + DEV |
| 6 | Cloud object store for OTA/APK | Sysop |
| 7 | **CDN + bandwidth dashboards** (SPEC §12.3) | Sysop |
| 8 | SFU SPEC + media workers (separate track) | PO spec |
| 9 | **GEO policy schema + edge rules** (SPEC §14) | PO + DEV |
**Not chosen now:** full Option A in prod; full Option C prod cutover.
---
## 5. Current vs target diagrams
### Today (single BE)
```mermaid
flowchart TB
R["Router 134.17.26.161"] --> FE["FE Gentoo 10.7.0.10"]
FE --> BE["Single Alpine BE 10.7.16.128"]
BE --> S1["PHP consoles + APIs"]
BE --> S2["MariaDB"]
BE --> S3["Gitea + url-shortener"]
BE --> S4["Docker build"]
BE --> S5["WG + RSSH"]
FE -->|"UDP 45340 DNAT"| S5
```
### Target phase B (minimal split + HA web)
```mermaid
flowchart TB
R["Router"] --> FE["Gentoo FE"]
FE --> LB["nginx upstream / cloud LB"]
LB --> W1["be-web-1"]
LB --> W2["be-web-2"]
W1 --> DB["be-data MariaDB"]
W2 --> DB
OPS["be-ops Gitea+shortener"] --> DB
BUILD["be-build Docker"] --> DB
VPN1["be-vpn-1"] --> DB
VPN2["be-vpn-2 optional"]
FE -->|"UDP DNAT pool"| VPN1
FE --> VPN2
```
---
## 6. Service split rationale
Draft asked to balance **cost vs CPU/RAM needs**. DR sizing rules:
| Workload | CPU | RAM | Disk | Pin to |
|----------|-----|-----|------|--------|
| PHP consoles | Medium | 24 GiB | Small | be-web* |
| MariaDB | Medium | **8+ GiB** | **Fast SSD** | be-data |
| Gitea mirrors | Low | 2 GiB | Medium | be-ops |
| URL shortener | Low | 512 MiB | Tiny | be-ops |
| Docker build | **Burst high** | **16 GiB** | Large ephemeral | be-build |
| WireGuard | Low | 1 GiB | Tiny | be-vpn |
| RSSH sshd | Lowmed | 2 GiB | Tiny | be-vpn |
| ffmpeg (future) | **Sustained high** | 832 GiB | Temp SSD | be-media* |
| SFU (future) | **High** | 8+ GiB | Low | be-media* |
| Recordings (future) | Low | — | **Large** | object store |
**Anti-pattern rejected:** MariaDB + Docker build + ffmpeg on one VM (draft implied bundle risk).
---
## 7. Environment promotion model
| Gate | dev → staging | staging → prod |
|------|---------------|----------------|
| Automated tests | `./gradlew :app:testDebugUnitTest` + BE smoke | + soak checklist |
| Manual | PO feature acceptance | PO sign-off + tag |
| Data | Synthetic | Anonymized copy optional |
| Rollback | Re-deploy previous image | Git revert + DB restore runbook |
Align app releases with [GIT_FLOW.md](../GIT_FLOW.md); infra releases may cadence independently with **config version pins** per environment.
---
## 8. RSSH and connection capacity
Draft question: *how many simultaneous connections per instance?*
### Assumptions
- Heartbeat poll **17 min** ([REMOTE_ACCESS_IMPL](../REMOTE_ACCESS_IMPL.md))
- Average SSH session **15 min** operator work
- Alpine **be-vpn-1**: 2 vCPU, 2 GiB RAM
### Estimates
| Resource | Limiting factor | Conservative | With tuning |
|----------|-----------------|--------------|-------------|
| **Concurrent SSH** | `sshd` `MaxSessions` / CPU | 50 | **100** |
| **Devices polling heartbeat** | PHP-FPM + MariaDB writes | 2 000 | **5 000** |
| **New SSH/min** | `MaxStartups` | 10 | 30 |
| **WG peers (lab)** | kernel + UDP | 200 | 500 / 2 nodes |
**Tuning knobs:** separate **heartbeat** PHP pool to **be-web** (off bastion); keep sshd on **be-vpn** only; connection pooling to MariaDB; read replicas for heartbeat read-heavy phases.
**DR recommendation:** Instrument **active SSH** and **heartbeat QPS** in graphs before selling commercial VPN (draft F4).
---
## 8b. Network bandwidth (DR)
Draft rev.2 adds **throughput** alongside connection counts.
### Baseline risk (today)
| Path | Risk |
|------|------|
| Single **WAN** uplink | OTA release or viral short link saturates all services |
| **FE→BE** proxy | All HTTPS doubles through FE NIC |
| No **CDN** | Every APK download hits origin BE |
| **Media future** | SFU + ffmpeg on same uplink = hard fail without PoP/CDN |
### DR sizing decisions
| Decision | Rationale |
|----------|-----------|
| **CDN before be-web-N** for OTA/static | Fixes egress Mbps cheaper than more PHP VMs |
| **Separate media NIC/subnet** when SFU lands | Isolate UDP/WebRTC from MariaDB LAN |
| **Egress budget alerts** at 80% ISP/cloud quota | PO notified before hard cap |
| **Simulate release day** on staging | e.g. 1000 concurrent OTA downloads |
### Planning numbers (single-site prod, until CDN)
| Traffic | Peak Mbps (conservative) | Mitigation |
|---------|--------------------------|------------|
| Steady HTTPS (API + consoles) | 520 | Normal |
| OTA drop (1000 devices × 40 MB APK) | **200400** burst 10 min | **CDN required** |
| Recording download (F3) | 50200 per hot file | Object store + CDN signed URLs |
| SFU fan-out 100 viewers | 50500+ | Regional PoP; not on FE WAN |
Full table: [SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress).
---
## 8c. Global availability and GEO segments (DR)
Draft rev.2: **global reach** + **segment isolation** (example: deny publish/serve in **Zambia `ZM`**).
### Policy model (DR decision)
| Rule | Choice |
|------|--------|
| **Default** | **Allow** worldwide for control plane and public APIs |
| **Content objects** | Attach optional `geo_policy` with `deny_publish` / `deny_serve` / `allow_only` |
| **Enforcement order** | CDN/WAF → BE API (trusted geo header) → object signed URL → app entitlement |
| **Operator UI** | Hub/console editor for geo segments when F3/F10 ship — not alpha |
| **Sanctions** | PO-maintained deny lists; not auto-synced from external API in v1 |
```mermaid
flowchart LR
REQ["Client request"] --> EDGE["CDN / FE edge"]
EDGE --> GEO{"Country in deny_serve?"}
GEO -->|yes| DENY["451 / blocked manifest"]
GEO -->|no| BE["BE + geo_policy DB"]
BE --> OK["200 / stream / redirect"]
```
### Phasing
| Phase | GEO capability |
|-------|----------------|
| **Now** | Implicit worldwide; no per-content rules |
| **CDN phase** | Edge geo block for **coarse** country bans |
| **F3 storage** | Per-recording `geo_policy` in MariaDB + object metadata |
| **F5 payments** | Entitlement includes `geo_allowed` |
| **Multi-PoP** | GeoDNS routes to nearest **allowed** PoP only |
### Example use cases
| Use case | Policy |
|----------|--------|
| OTA `stable` globally | `default: allow` |
| Beta APK EU-only | `allow_only: ["DE","FR",…]` |
| Recording licensed EU/US only | `deny_serve: ["ZM",…]` + `allow_only` optional |
| Short link to blocked catalog | BE returns 451 at redirect time |
**Open for PO:** whether **VPN exit** in denied country circumvents policy (DR: log + ToS; technical block on **VPN product** separate from HTTP geo).
---
## 9. Future capabilities — priority weighting
Weighted for **platform dependency** and **revenue enablement** (PO may override):
| Rank | Slot | Weight | Rationale |
|------|------|--------|-----------|
| **1** | F1 Multipoint cast (SFU/MCU) | Critical path | Unblocks WAN cast; already on [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md) |
| **2** | F2 ffmpeg transcode | High | Shared worker pool for F3, F9 |
| **3** | F3 Cast storage | High | Needs F1+F2 + object store |
| **4** | F5 Payments | Medium | Enables F3, F4, F7 |
| **5** | F4 Commercial VPN | Medium | RSSH/WG scale must be proven first |
| **6** | F9 3rd-party streaming | Medium | Uses F2 cast handlers |
| **7** | F6 Mobile add-ons | Lower | Can ship on-device first |
| **8** | F7 In-app transfers | Lower | Compliance heavy |
| **9** | F8 DNS / subdomains | Exploratory | Largest scope creep |
| **10** | F10 GEO enforcement | Medium | Required for F3/F5 global commerce; draft rev.2 |
```mermaid
flowchart TB
SFU["F1 SFU / multipoint"] --> STORE["F3 Cast storage"]
FF["F2 ffmpeg workers"] --> STORE
FF --> EXT["F9 3rd-party streams"]
SFU --> EXT
PAY["F5 Payments"] --> STORE
PAY --> VPN["F4 Commercial VPN"]
PAY --> XFER["F7 In-app transfers"]
PAY --> ADD["F6 Add-ons"]
DNS["F8 DNS / site builder"] --> PAY
GEO["F10 GEO policy"] --> STORE
GEO --> PAY
```
---
## 10. Simulation lab design
Sysop simulates **Option A** under dev without WAN changes:
| HVM | Alpine packages | Role |
|-----|-----------------|------|
| `dev-web-1` | nginx, php81-fpm | Web tier |
| `dev-data-1` | mariadb | DB |
| `dev-ops-1` | git, gitea (optional) | Gitea + shortener |
| `dev-build-1` | docker | Builder |
| `dev-vpn-1` | wireguard-tools, openssh | VPN lab |
**Network:** `10.7.0.0/8` only; FE routes `dev.apps.f0xx.org` to dev-web when ready.
**Deliverable suggestion:** `docs/SIMULATION_CLUSTER.md` + scripts in phase 1 — not blocking this DR.
---
## 11. Cloud provider notes (GCP / Azure)
| Concern | GCP | Azure |
|---------|-----|-------|
| **Managed LB** | ILB + NEG | App Gateway / Front Door |
| **Object store** | GCS | Blob |
| **Managed MariaDB** | Cloud SQL | Azure Database for MariaDB |
| **UDP / WG** | harder | harder — keep on-prem FE DNAT longer |
| **ffmpeg burst** | MIG (CPU) | VMSS |
| **Egress cost** | Watch OTA/APK | Same |
| **CDN + geo WAF** | Cloud CDN / Cloud Armor | Azure Front Door + rules |
| **Multi-region** | GCS dual-region; Cloud CDN PoPs | Blob + AFD PoPs |
**DR:** Use cloud for **staging cluster first** (Option C partial) — validates IaC without prod risk. **CDN geo rules** prototype on staging before prod F3.
---
## 12. RBAC evolution
| Stage | Who | Access |
|-------|-----|--------|
| **Alpha (now)** | Developers = operators | Max privileges; shared session |
| **Beta** | Split **developer** vs **support operator** | Tickets/RA without build admin |
| **Commercial** | Customer admin + platform admin | Tenant isolation; audit export |
New roles MUST NOT require mobile app changes until F5 entitlements API exists.
---
## 13. Risks and mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| MariaDB migration downtime | Prod outage | Replica + cutover window; practice on dev |
| Config loss on sync | 503 (seen url-shortener) | `ensure_*` scripts; secrets outside tree |
| FE upstream misconfig | Partial outage | `nginx -t`; weighted rollout |
| WG UDP cloud move | VPN breaks | Keep DNAT path until PO signs UDP plan |
| Over-scoping F5F9 | Never ship SFU | [§9](#9-future-capabilities-priority-weighting) order |
| WAN saturation on OTA | Global outage perception | CDN + [§8b](#8b-network-bandwidth-dr) budgets |
| Geo policy bypass via VPN | Compliance / licensing | Separate VPN egress policy; audit logs |
| Wrong deny list | Legitimate users blocked | Staging test + `X-Debug-Country`; PO approval workflow |
| Simulation sprawl | Sysop time | Cap dev HVMs at 7; destroy scripts |
---
## 14. Estimates
Person-days (**single sysop + DEV support**), planning accuracy ±40%:
| Phase | Work | Days |
|-------|------|------|
| **P0** | SPEC/DR + simulation design (this doc) | 2 |
| **P1** | Split Docker build VM | 2 |
| **P2** | MariaDB dedicated VM + migration | 3 |
| **P3** | be-ops split (Gitea + shortener) | 1 |
| **P4** | Dev cluster HVM simulation (5 VMs) | 4 |
| **P5** | Staging env + promotion runbook | 3 |
| **P6** | be-web-2 + FE upstream HA | 2 |
| **P7** | Cloud object store for OTA | 2 |
| **P7b** | CDN + bandwidth metrics on graphs | 2 |
| **P8** | GEO policy schema + staging CDN geo test | 3 |
| **P9** | SFU/media (separate SPEC) | TBD |
| **Total (through P7b)** | | **~21 days** |
**Severity:** Strategic — parallel to alpha; no alpha gate.
---
## 15. Task dependency graph
Legend: **1** = highest priority (PO: post-alpha infra). Assignee **Sysop** = PO today; **Agent** = documentation/automation; **DEV** = app/config changes.
```mermaid
flowchart TB
T1["1. SPEC+DR scaling docs<br/>Agent ✓"]
T2["2. Split be-build Docker<br/>Sysop"]
T3["3. Dedicated be-data MariaDB<br/>Sysop"]
T4["4. ensure_* config hardening all services<br/>Agent"]
T5["5. Dev cluster simulation HVMs<br/>Sysop"]
T6["6. Staging env + DNS prefix<br/>Sysop + PO"]
T7["7. Promotion runbook dev→stg→prod<br/>PO"]
T8["8. be-web-2 + FE upstream<br/>Sysop"]
T9["9. Cloud object store OTA<br/>Sysop"]
T9b["9b. CDN + bandwidth metrics<br/>Sysop"]
T10["10. GEO policy + edge rules<br/>PO+DEV"]
T11["11. SFU owner SPEC<br/>PO"]
T12["12. ffmpeg worker pool<br/>DEV post-SFU"]
T13["13. Payments / entitlements SPEC<br/>PO"]
T1 --> T2
T1 --> T4
T2 --> T3
T3 --> T5
T4 --> T6
T5 --> T6
T6 --> T7
T7 --> T8
T3 --> T8
T8 --> T9
T9 --> T9b
T9b --> T10
T10 --> T11
T11 --> T12
T12 --> T13
style T1 fill:#86efac,color:#14532d
style T11 fill:#e5e7eb,color:#374151
style T12 fill:#e5e7eb,color:#374151
style T13 fill:#e5e7eb,color:#374151
```
| ID | Task | Assignee | Blocks |
|----|------|----------|--------|
| 1 | Scaling SPEC/DR | Agent | — |
| 2 | Docker → be-build | Sysop | DB split |
| 3 | MariaDB → be-data | Sysop | HA web |
| 4 | Post-sync config scripts (all services) | Agent | staging |
| 5 | Dev HVM simulation | Sysop | staging realism |
| 6 | Staging cluster | Sysop | promotion |
| 7 | Promotion runbook | PO | prod multi-node |
| 8 | Second web node | Sysop | cloud OTA |
| 9 | Object store OTA | Sysop | CDN |
| 9b | CDN + bandwidth dashboards | Sysop | GEO test |
| 10 | GEO policy + edge | PO+DEV | F3 commerce |
| 11 | SFU SPEC | PO | media workers |
| 12 | ffmpeg pool | DEV | commercial storage |
| 13 | Payments SPEC | PO | F6F8 |
**Parallel now (non-blocking):** alpha soak, url-shortener ops, Gitea mirrors, ticket **SVC-OTA**.
---
## 16. Resolved open questions
| # | Draft question | DR answer |
|---|----------------|-----------|
| Q1 | How many VMs in a cluster? | **Option B:** 5 prod VMs to start; **dev simulation:** 37 ([SPEC §6.3](../specs/20100612_1_scaling.md#63-reference-cluster-layout-prod)) |
| Q2 | “Other” architecture approaches? | **Option C** hybrid for staging; not prod-first ([§3.3](#33-option-c-cloud-first-hybrid)) |
| Q3 | Cost vs CPU/RAM balance? | Split by [§6](#6-service-split-rationale) profile table |
| Q4 | Seamless scaling / LB? | FE upstream → N PHP nodes; cloud managed LB in phase 5 ([SPEC §9](../specs/20100612_1_scaling.md#9-zero-downtime-and-load-balancing)) |
| Q5 | Simultaneous connections / RSSH? | [§8](#8-rssh-and-connection-capacity) — 100 SSH, 5k heartbeats per bastion pair (tuned) |
| Q6 | Alpine simulation? | [§10](#10-simulation-lab-design) — intra `10.7.0.0/8` HVMs only |
| Q7 | Future MCU/SFU/ffmpeg/storage/VPN/payments? | Platform **slots** F1F9; priority [§9](#9-future-capabilities-priority-weighting) |
| Q8 | RBAC model change? | [§12](#12-rbac-evolution) three-stage |
| Q9 | Keep current topology safe? | **Yes** — [SPEC §18](../specs/20100612_1_scaling.md#18-safety-constraints-current-topology) |
| Q10 | Network bandwidth metrics? | [§8b](#8b-network-bandwidth-dr) + [SPEC §12.3](../specs/20100612_1_scaling.md#123-network-bandwidth-and-egress) — CDN before web horizontal scale |
| Q11 | Global availability + GEO deny (e.g. Zambia)? | [§8c](#8c-global-availability-and-geo-segments-dr) + [SPEC §14](../specs/20100612_1_scaling.md#14-global-availability-and-geo-policy) — default allow, per-content deny |
---
## 17. Remaining open items (PO)
| ID | Question | Needed for |
|----|----------|------------|
| O1 | Target **monthly infra budget** for phase B (5 VMs)? | Option A vs B timing |
| O2 | **Staging URL** pattern: `staging.apps.f0xx.org` vs path prefix? | DNS + TLS |
| O3 | **Cloud vendor** preference GCP vs Azure vs none for 12 months? | P7P9 |
| O4 | Accept **maintenance window** for MariaDB cutover (minutes of read-only)? | P2 schedule |
| O5 | **Commercial VPN** — same WG infra or separate product stack? | F4 design |
| O6 | **SFU** — Janus on `:8089` vs greenfield mediasoup? | F1 SPEC ([20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md)) |
| O7 | **Payments** — Stripe-only vs multi-provider? | F5 SPEC |
| O8 | **ISP/cloud egress cap** (GB/month) on FE WAN? | [§8b](#8b-network-bandwidth-dr) budgets |
| O9 | **CDN provider** (Cloudflare vs cloud-native vs none 12 mo)? | OTA + GEO WAF |
| O10 | **GEO deny lists** — PO-owned static JSON vs admin UI first? | F10 rollout |
| O11 | Recordings **451** vs silent 404 for denied regions? | UX + compliance |
*When blocked on O1O7, append to `tmp/YYYYMMDD_implementation_summary.pdf` per bottomline_reminder.*
---
## 18. Suggestions (PO / dev discretion)
| # | Suggestion |
|---|------------|
| S1 | Add **infra smoke** to `validate_be_services.sh` per VM role after split |
| S2 | Version **nginx upstream** fragments in `examples/crash_reporter/backend/deploy/` |
| S3 | Use **Docker** for PHP-FPM pools only after MariaDB split — avoid double complexity |
| S4 | Keep **Gitea** on be-ops until mirror count > 10 repos |
| S5 | Document **connection pool** sizes when RSSH device count > 500 |
| S6 | Re-read this DR when **SFU SPEC** lands — revise [§9](#9-future-capabilities-priority-weighting) |
| S7 | Add **WAN Mbps** graph series on FE before next OTA wide release |
| S8 | Staging test matrix: `X-Debug-Country: ZM` for geo deny paths |
---
## 19. Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial DR from draft `20100612_1_scaling.txt`; options matrix; task graph |
| 2026-06-12 | Rev. 2 — bandwidth (§8b), global/GEO (§8c), F10, task graph + open items |
---
## Related docs
- [SPEC — Platform scaling](../specs/20100612_1_scaling.md)
- [INFRA.md](../INFRA.md)
- [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)
- [20260608_ALPHA_PRIORITIES.md](../20260608_ALPHA_PRIORITIES.md)

945
DRs/20100612_1_scaling.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,426 @@
# RSSH routed egress (dev-only) — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R0 |
| Creation date | 2026-06-16 |
| Last modification date | 2026-06-16 |
| Co-authored | Cursor Agent (project assistant) |
| Severity | low |
| State | postponed |
| Document type | DR |
| Pre-requisite to | Finish WireGuard remote-access E2E ([REMOTE_ACCESS_VALIDATION.md](../REMOTE_ACCESS_VALIDATION.md), [VPN_DEMO_GAPS_20260613.md](../VPN_DEMO_GAPS_20260613.md)) |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20260616_rssh_routed_egress.txt](../drafts/20260616_rssh_routed_egress.txt)
**PDF:** [20260616_rssh_routed_egress.pdf](20260616_rssh_routed_egress.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh`
**Status:** Draft for PO review — **no implementation** until WireGuard track is closed
**Scope:** Developer settings only; optional future path; does **not** replace alpha RSSH (reverse tunnel, no VPN)
**Related:** [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md) · [20260602_REVERSE_SSH_proposals_summary.md](../20260602_REVERSE_SSH_proposals_summary.md) · [ROADMAP.md](../ROADMAP.md) (RSSH alpha)
**Documentation index:** [README.md](../README.md)
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Vocabulary and scope](#3-vocabulary-and-scope)
- [4. Current state](#4-current-state)
- [5. Goals and non-goals](#5-goals-and-non-goals)
- [6. Architecture options](#6-architecture-options)
- [7. DR recommendation](#7-dr-recommendation)
- [8. Reference design (if pursued later)](#8-reference-design-if-pursued-later)
- [9. Developer settings UX](#9-developer-settings-ux)
- [10. Backend and bastion requirements](#10-backend-and-bastion-requirements)
- [11. App components (sketch)](#11-app-components-sketch)
- [12. Third-party and licenses](#12-third-party-and-licenses)
- [13. Risks](#13-risks)
- [14. Effort estimate (order of magnitude)](#14-effort-estimate-order-of-magnitude)
- [15. Task dependency graph](#15-task-dependency-graph)
- [16. Open questions (PO)](#16-open-questions-po)
- [17. Changelog](#17-changelog)
<!-- /toc -->
---
## 1. Executive summary
**Question:** Can we add a developer-only, system-wide VPN-style path that routes part or all device traffic through the **RSSH** (reverse SSH) session?
**Short answer:** **Yes, technically** — but it requires a **new data plane** (`VpnService` + IP-to-SOCKS or TUN-over-SSH), not an extension of todays RSSH `-R` forward. It **reintroduces VPN permission and the status-bar key**, and overlaps **WireGuard full-tunnel**, which already implements route/app scope in the app.
**DR decision (R0):**
| Item | Decision |
|------|----------|
| **Timing** | **Postponed** until WireGuard remote-access work is finished and validated E2E |
| **Alpha RSSH** | Unchanged — outbound reverse SSH for operator → device; **no** device routing |
| **Near-term routing** | Use **WireGuard** + `RemoteAccessVpnRouting` (hub-only or full-tunnel) |
| **If revived** | Dev-only sub-mode **“RSSH + route traffic (experimental)”** via **SSH dynamic forward (-D) + tun2socks + existing route/app prefs** |
| **SPEC** | Do **not** write implementation SPEC until PO confirms after WG closure |
---
## 2. Problem statement
Developers may want:
1. **Operator access** to the device (shell/SFTP) — **RSSH today**.
2. **Device traffic** to egress via the hub (e.g. Minsk NAT) or reach hub-private IPs — **WireGuard today** (split or full tunnel).
3. In some networks, **UDP WireGuard** may be blocked while **TCP 443 SSH** works — motivation to explore routing **over RSSH**.
Without a clear DR, it is easy to assume `vpn-route-scope` / `vpn-app-scope` apply to RSSH; they currently apply to **WireGuard only** ([REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md)).
---
## 3. Vocabulary and scope
| Term | Meaning here |
|------|----------------|
| **RSSH** | Device-initiated reverse SSH; `-R bastion:port → device:8022` for operator access |
| **Routed egress** | Device IP traffic (some or all) sent through bastion/hub NAT to the Internet or private nets |
| **VpnService** | Android API for app-provided VPN; required for system-wide or per-app routing **from our app** |
| **Route scope** | Hub-only (`172.200.2.1/32`) vs full-tunnel (`0.0.0.0/0`) — `RemoteAccessVpnRouting` |
| **App scope** | All apps vs this app only — `IncludedApplications` / `addAllowedApplication` |
**Not in scope for this DR:** Commercial VPN product (F4), always-on VPN in Android Settings, routing for non-developer builds.
---
## 4. Current state
```mermaid
flowchart LR
subgraph phone [Device]
RA[RemoteAccessService]
WG[WireGuard :vpn]
RSSH[ReverseSshTunnelBridge]
LOCAL[RsshLocalSshServer :8022]
end
subgraph be [BE / bastion]
WG0[wg0 UDP 45340]
SSHD[sshd TCP 443]
end
RA -->|mode wireguard| WG
WG -->|UDP| WG0
RA -->|mode rssh| RSSH
RSSH -->|TCP -R| SSHD
SSHD -->|operator| LOCAL
```
| Mode | Transport | Device routing | VPN icon |
|------|-----------|----------------|----------|
| **WireGuard** | UDP → `wg0` | Yes (hub-only or full) | Yes |
| **RSSH** | TCP → sshd | **No** | **No** |
Existing prefs (`dev_remote_access_vpn_route_scope`, `dev_remote_access_vpn_app_scope`) are sent on heartbeat for WG; RSSH ignores them.
---
## 5. Goals and non-goals
### Goals (if implemented later)
| ID | Goal |
|----|------|
| G1 | Optional **dev-only** routing of device traffic via **TCP 443** SSH path |
| G2 | Reuse **route scope** and **app scope** semantics from `RemoteAccessVpnRouting` |
| G3 | Network self-test shows bastion, bind ports, egress expectation (like WG `vpn-ip`) |
| G4 | Clear separation from **alpha RSSH** (reverse access only) |
### Non-goals
| ID | Non-goal |
|----|----------|
| NG1 | Replace WireGuard for production/lab routing |
| NG2 | Alpha release blocker |
| NG3 | Avoid `VpnService.prepare()` — routed mode **will** need it |
| NG4 | SSH `PermitTunnel` / `ssh -w` as v1 (too heavy for Alpine bastion v1) |
| NG5 | Third-party “VPN provider” in Android Settings (no such API; we use `VpnService`) |
---
## 6. Architecture options
### 6.1 Option A — WireGuard full-tunnel (existing)
**Use when:** Hub egress or full WAN via Minsk NAT is needed **now**.
- Dev settings → WireGuard → route scope **All traffic** → reconnect session.
- BE `wg0` PostUp MASQUERADE already documented.
| Pros | Cons |
|------|------|
| Implemented | UDP DNAT + VPN consent |
| Kernel/userspace WG | Not RSSH transport |
**Verdict:** **Default for routing** until WG track is done.
### 6.2 Option B — RSSH + SSH `-D` (SOCKS) + `VpnService` + tun2socks
**Use when:** PO explicitly needs **TCP-only** routing and accepts VPN icon + extra stack.
```mermaid
flowchart TB
subgraph device [Device]
TUN[VpnService TUN]
T2S[tun2socks userspace]
SOCKS[127.0.0.1:SOCKS]
JSch[JSch session]
end
subgraph bastion [Bastion / BE]
SSD[sshd]
NAT[NAT egress eth0]
end
TUN --> T2S --> SOCKS --> JSch --> SSD --> NAT
```
1. Extend `ReverseSshTunnelBridge`: keep `-R` for operator; add **`-D 127.0.0.1:<socks_port>`** on same session.
2. New **RsshRoutedVpnService** (or branch in `:vpn`) establishes TUN with routes from `RemoteAccessVpnRouting`.
3. **tun2socks** forwards IP packets → local SOCKS → SSH stream → bastion → Internet.
| Pros | Cons |
|------|------|
| TCP 443 friendly | VPN permission + icon |
| Reuses SSH session | tun2socks dep + licenses |
| Reuses route/app prefs | TCP-over-TCP, MTU, battery |
| | Bastion egress policy + capacity |
| | JSch dynamic forwarding QA |
**Verdict:** **Reference design if PO revives** routed RSSH.
### 6.3 Option C — SSH layer-3 `tun` (`ssh -w`)
Both ends need TUN, `PermitTunnel`, root/capabilities on bastion, custom Android client.
**Verdict:** **Reject** for dev-only v1 (operational cost).
### 6.4 Option D — Hybrid (proposal doc)
**WG for packets + RSSH (or SSH over WG) for operator files.**
**Verdict:** Valid **long-term**; does not require RSSH alone to carry IP routing.
### 6.5 Comparison matrix
| Criterion | A WG | B RSSH+SOCKS | C ssh -w | D hybrid |
|-----------|------|--------------|----------|----------|
| Ready today | Yes | No | No | Partial |
| TCP 443 only | No | Yes | Yes | Mixed |
| VPN icon | Yes | Yes | Yes | Yes |
| Alpha RSSH purity | N/A | Weakens if default | Weakens | OK |
| Impl effort | Done | High | Very high | Medium |
---
## 7. DR recommendation
1. **Close WireGuard E2E** (peer reconcile, self-test `vpn-ip`, hub→phone diagnostics, full-tunnel egress check) — **current priority**.
2. **Keep alpha RSSH** as reverse tunnel only ([ROADMAP.md](../ROADMAP.md)).
3. **Park Option B** in this DR; PO reviews §16 and decides **go / no-go** after WG.
4. If **go:** spawn SPEC `rssh_routed_egress` (dev-only feature flag), not a change to core RSSH connect payload for alpha.
---
## 8. Reference design (if pursued later)
### 8.1 Control plane
Unchanged: `heartbeat.php` `type: ra`, `tunnel: ssh_reverse`, credentials for bastion + `-R` port.
Optional dev flag in heartbeat meta: `rssh_route_experimental: true` (telemetry only; no BE requirement for v0).
### 8.2 Data plane
| Step | Component |
|------|-----------|
| 1 | `ReverseSshTunnelBridge` connects; `-R` as today |
| 2 | Same session: `session.setPortForwardingD("127.0.0.1", socksPort)` (JSch) |
| 3 | `RsshRoutedVpnEngine` starts TUN via `VpnService` |
| 4 | tun2socks reads TUN, connects to `127.0.0.1:socksPort` |
| 5 | Bastion sshd forwards; **iptables MASQUERADE** on bastion for egress |
### 8.3 Routing semantics
Reuse `RemoteAccessVpnRouting.resolveAllowedIps()`:
| Route scope | TUN routes |
|-------------|------------|
| **HUB_ONLY** | `172.200.2.1/32` (or configured hub CIDR) |
| **FULL_TUNNEL** | `0.0.0.0/0`, `::/0` |
Reuse `appendInterfaceExtras()` for **THIS_APP_ONLY**.
**Note:** Hub reachability over RSSH may need **explicit route to hub IP via TUN** or SOCKS; validate that hub-only does not assume WG interface addresses.
### 8.4 Failure modes
| Symptom | Likely cause |
|---------|----------------|
| SOCKS up, no Internet | Bastion NAT / forwarding disabled |
| Tunnel up, wrong egress IP | Route scope hub-only (expected) |
| Intermittent stalls | TCP-over-TCP congestion |
| VPN works, operator SFTP dead | `-R` forward dropped; session limits |
---
## 9. Developer settings UX
Proposed (not implemented):
```
Remote access mode: [ Disabled | WireGuard | RSSH ]
— When WireGuard —
VPN route scope: Hub only | All traffic
VPN app scope: All apps | This app only
— When RSSH —
[ ] Route device traffic via bastion (experimental)
(shows route/app scope only when checked)
Hint: Reverse tunnel for operator access is always on in RSSH mode.
Routing requires VPN permission (same as WireGuard).
```
Network self-test (RSSH + experimental): `rssh-bastion`, `rssh-remote-bind`, `rssh-socks`, `wan-egress-via-vpn`, `rssh-connected`.
---
## 10. Backend and bastion requirements
| Requirement | Notes |
|-------------|--------|
| `AllowTcpForwarding yes` | Already for `-R` |
| Dynamic forwarding | Confirm sshd `GatewayPorts` / no restrictive `Match` block for `-D` |
| Egress NAT | iptables/nft MASQUERADE on bastion (similar to `wg0` PostUp) |
| Capacity | Routed dev traffic adds bandwidth; separate from §12.1 RSSH SSH counts in scaling DR |
| Audit | Log experimental flag; no change to alpha session schema required |
No new nginx HTTP paths; optional stream unchanged.
---
## 11. App components (sketch)
| Component | Process | Role |
|-----------|---------|------|
| `ReverseSshTunnelBridge` | main | SSH session; `-R` + `-D` |
| `RsshRoutedVpnService` | `:vpn` | TUN + lifecycle |
| `RsshTun2SocksEngine` | `:vpn` | Packet relay |
| `RemoteAccessVpnRouting` | shared | Route/app scope |
| `DevVpnStatusProbe` | main | Diagnostics |
Do **not** fold into `WireGuardVpnEngine`; separate engine behind common `VpnServiceClient` interface if refactored later.
---
## 12. Third-party and licenses
| Candidate | License | Action if adopted |
|-----------|---------|-------------------|
| **badvpn** / **tun2socks** | Various | Evaluate; add to `third-party/`, mobile + BE license files |
| **JSch** | BSD | Already in app; verify `-D` support |
| **Apache MINA SSHD** | Apache-2.0 | Local server only today |
Update [COMMERCIAL.md](../COMMERCIAL.md) and `app/src/main/assets/licenses/` before merge.
---
## 13. Risks
| Risk | Mitigation |
|------|------------|
| Duplicates WG | PO go/no-go; default to WG |
| Alpha scope creep | Dev-only flag; off in release if needed |
| VPN consent UX regression | Document in FR; separate from “no VPN” RSSH story |
| Bastion abuse (open egress) | Dev whitelist + session TTL + rate limits |
| Maintenance burden | Two routing stacks |
---
## 14. Effort estimate (order of magnitude)
| Phase | Work | Duration (indicative) |
|-------|------|------------------------|
| P0 | WG E2E closure | **Current sprint** (PO priority) |
| P1 | Spike: JSch `-D` + manual SOCKS browser test | 23 d |
| P2 | tun2socks + `VpnService` + route prefs | 58 d |
| P3 | Bastion NAT + self-test + docs | 23 d |
| P4 | Unit/integration tests, soak | 35 d |
**Total if pursued:** ~34 weeks **after** WG done — not parallelized with WG closure unless PO overrides.
---
## 15. Task dependency graph
Legend: **blocked by** upstream task.
```mermaid
flowchart TD
WG1["1. WG peer reconcile on BE deploy"]
WG2["2. WG E2E phone ↔ hub"]
WG3["3. WG full-tunnel egress validation"]
PO["4. PO review this DR"]
GO{"5. Go routed RSSH?"}
SPIKE["6. JSch -D spike"]
SPEC["7. SPEC rssh_routed_egress"]
IMPL["8. Implement dev experimental mode"]
WG1 --> WG2 --> WG3 --> PO
PO --> GO
GO -->|no| PARK["Park — use WG only"]
GO -->|yes| SPIKE --> SPEC --> IMPL
```
| Priority | Task | Owner | State |
|----------|------|-------|-------|
| **1** | Finish WG (peer sync, ping/handshake, self-test, full-tunnel) | Dev/PO | In progress |
| **2** | Review DR R0 | PO | Pending |
| **3** | RSSH routed egress | — | **Postponed** |
---
## 16. Open questions (PO)
| # | Question | Default if silent |
|---|----------|-------------------|
| Q1 | After WG E2E, is TCP-only routing still a requirement? | **No** — stay WG-only for routing |
| Q2 | Accept VPN icon for an experimental RSSH routing mode? | Required for Option B |
| Q3 | Should hub-only over RSSH target `172.200.2.1` without WG? | Clarify use case |
| Q4 | Bastion egress NAT for dev traffic — allowed on prod `be-vpn`? | Lab-only |
| Q5 | Single SSH session for `-R` + `-D` or two sessions? | Prefer **one** session |
---
## 17. Changelog
| Rev | Date | Change |
|-----|------|--------|
| R0 | 2026-06-16 | Initial DR from PO question; postponed pending WG closure |
---
## Related docs
- [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md) — WG vs RSSH modes
- [REMOTE_ACCESS_VALIDATION.md](../REMOTE_ACCESS_VALIDATION.md) — CLI/mobile WG checks
- [VPN_DEMO_GAPS_20260613.md](../VPN_DEMO_GAPS_20260613.md) — WG demo gaps
- [20260602_REVERSE_SSH_proposals_summary.md](../20260602_REVERSE_SSH_proposals_summary.md) — original transport comparison
- [specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md) — `be-vpn`, RSSH capacity

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,375 @@
# Codec2 / ultra-low-bandwidth voice — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R0 |
| Creation date | 2026-06-17 |
| Last modification date | 2026-06-17 |
| Co-authored | Cursor Agent (project assistant) |
| Severity | low |
| State | approved for planning (implementation post-alpha) |
| Document type | DR |
| Pre-requisite to | [ROADMAP.md](../ROADMAP.md) §6.1 Opus/Speex JNI encode/decode; F1 SFU SPEC (post-alpha) |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20260617_codec2_ulbw_voice_integration.txt](../drafts/20260617_codec2_ulbw_voice_integration.txt)
**PDF:** [20260617_codec2_ulbw_voice.pdf](20260617_codec2_ulbw_voice.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh`
**Status:** PO-reviewed (inline draft comments incorporated) — **no native implementation** until Opus/Speex JNI sinks land
**Scope:** Voice-only / ULBW audio cast on P2P (LAN, BT, mesh); future MCU/SFU relay hooks
**Related:** [OPUS_SPEEX_VALIDATION.md](../OPUS_SPEEX_VALIDATION.md) · [ndk/README.md](../../ndk/README.md) · [20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) § SFU · [specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md) F1
**Documentation index:** [README.md](../README.md)
---
---
---
## Table of contents
<!-- toc -->
- [1. Executive summary](#1-executive-summary)
- [2. Problem statement](#2-problem-statement)
- [3. Vocabulary](#3-vocabulary)
- [4. Current state (mobile + BE)](#4-current-state-mobile--be)
- [5. Codec landscape and PO decisions](#5-codec-landscape-and-po-decisions)
- [6. Goals and non-goals](#6-goals-and-non-goals)
- [7. Mobile peer negotiation (today)](#7-mobile-peer-negotiation-today)
- [8. Proposed mobile integration](#8-proposed-mobile-integration)
- [9. Future backend: MCU / SFU](#9-future-backend-mcu--sfu)
- [10. Protocol extensions](#10-protocol-extensions)
- [11. Third-party and licenses](#11-third-party-and-licenses)
- [12. Testing and validation](#12-testing-and-validation)
- [13. Task dependency graph](#13-task-dependency-graph)
- [14. Risks](#14-risks)
- [15. Open questions (remaining)](#15-open-questions-remaining)
- [16. Changelog](#16-changelog)
<!-- /toc -->
---
## 1. Executive summary
**Question:** Should androidcast integrate **Codec2** for ultra-low-bandwidth (ULBW) **voice-only** casts when normal AAC/Opus paths are too heavy for the link?
**Short answer:** **Yes, as a post-alpha product slice** — not a lab experiment. End users on marginal links (BT P2P, slow mesh, very poor LTE) should get **intelligible audio** (“something rather than nothing”). **Codec2 @ 3200 bit/s** (and lower modes where needed) fits that goal better than Opus narrowband @ ~6 kbps + FEC in POs practical experience (lower sustained bandwidth, less audible noise on those links).
**DR decision (R0):**
| Item | Decision |
|------|----------|
| **Primary use case** | **Voice-only** ULBW cast; not the audio leg of full screen cast (8 kHz telephony; no lip-sync with HD video) |
| **Codec2 modes** | **3200 bit/s** default target; **700C** for extreme BT/mesh; user/dev sub-mode selection |
| **MELP / TWELP** | **Out of scope** (patent / slim OSS path) |
| **AAC** | Keep as platform MediaCodec path; no license conflict for screen cast |
| **Opus / Speex** | Finish **JNI encode/decode** first ([ROADMAP.md](../ROADMAP.md) — cross-compile done, sinks **TODO**); negotiation/scaffolding exists today but wire path is **stub → AAC** without `libopus.a` / encode JNI |
| **Codec2 timing** | After **6.1** JNI baseline; behind dev flag until E2E validated |
| **SFU / MCU** | Plan **transcode at ingress** (Codec2 → Opus) for mixed WebRTC rooms; separate F1 SPEC — signaling reuses extended audio caps |
| **MSG_AUDIO_SELECTED** | Add in handshake when Codec2 implementation starts (SETTINGS_VERSION bump) — aligns P2P multicast and SFU data channel |
---
## 2. Problem statement
Screen cast defaults to **AAC** (32128 kbps class). Negotiation already advertises **Opus** and **Speex**, but without native encode/decode the sender often still ships **AAC** while labeling Opus in diagnostics.
For **ULBW** links:
- Opus @ 6 kbps + FEC/NACK can exceed the sustained budget PO has seen on BT/mesh and adds noise versus Codec2-class vocoders.
- Users still want **audio cast** (commentary, coordination, voice relay) when video is impossible — a deliberate **voice-only** mode.
Future **multipoint** (F1 SFU/MCU) cannot rely on pairwise discovery bitmasks alone; room-level **negotiated_audio** must be defined before Codec2 legs join a relay.
---
## 3. Vocabulary
| Term | Meaning |
|------|---------|
| **ULBW** | Ultra-low bandwidth; sustained **&lt; 34 kbps** audio payload target on worst links |
| **Voice-only cast** | No video track; discovery may send `videoCodecs = 0` |
| **Radio mode** | Dev/user profile preferring Codec2 in AUTO when both peers advertise `AUDIO_CODEC2` |
| **P2P negotiation** | `CastProtoHeader.audioCodecs` bitmask + `AudioNegotiator` (today) |
| **Room negotiation** | Signaling API + server-published codec (SFU future) |
| **SFU** | Selective forwarding unit — relay without per-subscriber transcode |
| **MCU** | Mix decode → PCM → encode; needed for heterogeneous legs |
---
## 4. Current state (mobile + BE)
### 4.1 Audio codecs in app
| Layer | Opus / Speex today | Notes |
|-------|-------------------|-------|
| `CastCodecFlags` | `AUDIO_OPUS=4`, `AUDIO_SPEEX=8` | Masks advertised in discovery |
| `AudioNegotiator` | Works | AUTO scoring via `CodecPriorityCatalog` |
| `NativeCodecBridge` | **Probe only** (`nativeIsOpusAvailable`) | No encode/decode JNI in `opus_bridge.c` / `speex_bridge.c` |
| Wire path | **OPUS_STUB / SPEEX_STUB → AAC** | `PassthroughCodecPolicy`, `AudioCodecFactory` |
| Build | `scripts/build-native-codecs.sh` can link `.a` | [ROADMAP.md](../ROADMAP.md): JNI sinks **TODO** |
**Clarification for PO:** Prior agent work completed **negotiation, settings UI, priority catalog, and native autolink scaffolding** — not full Opus/Speex on-the-wire encoding. Task **6.1** remains open for JNI sinks + E2E per [OPUS_SPEEX_VALIDATION.md](../OPUS_SPEEX_VALIDATION.md).
### 4.2 Video vs audio negotiation asymmetry
```mermaid
sequenceDiagram
participant S as Sender
participant R as Receiver
Note over S,R: Discovery: audioCodecs bitmask in header
S->>R: MSG_HELLO, CAST_SETTINGS
S->>R: MSG_CODEC_CAPS (video MIME list)
R->>S: MSG_RECEIVER_CAST_SETTINGS
R->>S: MSG_CODEC_CAPS
R->>S: MSG_CODEC_SELECTED (video MIME only)
Note over S: AudioNegotiator.resolveForSender(peerMask)
Note over R: No MSG_AUDIO_SELECTED today
```
### 4.3 Backend (gray)
- `SfuRelayGate.PREVIEW_ENABLED = false`; `api/sfu_health.php` returns disabled.
- Janus on **:8089** is **other stack** — androidcast signaling stays on `apps.f0xx.org` PHP ([INFRA.md](../INFRA.md)).
---
## 5. Codec landscape and PO decisions
| Codec | Bitrate | Role in androidcast | DR decision |
|-------|---------|---------------------|-------------|
| **Codec2** | 7003200 bps | ULBW voice-only | **Integrate** (post-6.1) |
| **Opus SILK NB** | ~612 kbps | General narrowband when link allows | Finish JNI; keep in AUTO above Codec2 only when bandwidth sufficient |
| **Speex NB** | ~215 kbps | Legacy / fallback | Finish JNI |
| **AAC-LC** | 32+ kbps | Screen cast default | Unchanged |
| **MELP/TWELP** | ~2.4 kbps | — | **Rejected** |
**PO rationale (2026-06-17):** Opus @ 6k + FEC is **heavier and noisier** than Codec2 on target links; Codec2 **3200 bps** meets product goals; intent is **end-user value**, not codec shootout lab work.
**Maintainer risk:** Codec2 upstream is in active re-development; pin submodule tag (e.g. 1.2.0) and mirror on Gitea like `third-party/opus`.
---
## 6. Goals and non-goals
### Goals
- Voice-only cast mode with **Codec2 encode/decode** on Android (NDK).
- Extend **peer negotiation** (`AUDIO_CODEC2 = 16`, optional `MSG_AUDIO_SELECTED`).
- Document **SFU ingress transcode** and room caps for F1 SPEC.
- LGPL compliance + license files.
### Non-goals (R0)
- MELP/TWELP integration.
- Codec2 as default for **screen cast with video**.
- SFU passthrough-only design (closed Android mesh edge case only).
- Alpha ship — hidden until E2E + PO QA.
---
## 7. Mobile peer negotiation (today)
| Step | Mechanism | Codec2 impact |
|------|-----------|---------------|
| Advertise | `CastCodecFlags.localAudioMask()` in discovery | Add bit 16 when native probe passes |
| Resolve | `AudioNegotiator.negotiate(senderMask, receiverMask, pref, settings)` | Add `CODEC2` to enum + `CodecPriorityId` |
| Score | `codecs.json` + `CodecPriorityCatalog` | ULBW profile: CODEC2 lowest `bitrate_score` |
| Wire | Audio frames (today AAC ADTS) | New codec tag or `MSG_AUDIO_FRAME` subtype |
**Gap:** Receiver does not receive explicit audio selection — sender-resolved only. **Must fix** before multicast + SFU (§10).
---
## 8. Proposed mobile integration
### 8.1 Settings and enum
```text
CastSettings.AudioCodec.CODEC2
CastSettings.codec2Mode: 3200 | 1300 | 700C (default 3200 per PO)
CastSettings.VoiceOnlyMode: boolean
```
### 8.2 Native stack
1. `third-party/codec2` submodule + `scripts/init-third-party-submodules.sh`
2. `scripts/build-native-codecs.sh``libcodec2.a`, `ANDROIDCAST_HAVE_CODEC2`
3. JNI: `isCodec2Available()`, encode/decode **8 kHz mono** frames
4. Link via existing `libandroidcast_codecs.so`
### 8.3 Priority (ULBW profile)
Suggested AUTO order when `NetworkAdoption.ESTIMATED` or **radio mode** on:
**CODEC2 → OPUS → SPEEX → AAC → PCM**
### 8.4 Milestones
| # | Deliverable | Depends on |
|---|-------------|------------|
| M1 | Opus/Speex JNI encode/decode + E2E | 6.1 |
| M2 | Codec2 probe + round-trip unit test | M1 |
| M3 | Voice-only UI slice (dev gate) | M2 |
| M4 | `MSG_AUDIO_SELECTED` + SETTINGS_VERSION | M2 |
| M5 | SFU signaling fields in F1 SPEC | Owner SFU spec |
---
## 9. Future backend: MCU / SFU
```mermaid
flowchart TB
subgraph clients [Android clients]
A[Sender A Codec2]
B[Sender B Opus]
C[Receiver WebRTC]
end
subgraph be [BE post-alpha]
SIG[Signaling API PHP]
SFU[Janus / media worker]
end
A --> SIG
B --> SIG
SIG --> SFU
SFU -->|transcode| C
SFU -->|fan-out Opus| B
```
### 9.1 Negotiation shift (P2P → room)
| Layer | P2P today | SFU future |
|-------|-----------|------------|
| Caps | Header bitmask | `join` JSON: `audio_caps.bitmask`, `codec2_modes[]` |
| Pick | `AudioNegotiator` local | Server `negotiated_audio` |
| Auth | PIN / LAN | Session RBAC + room token |
### 9.2 Codec2 on media plane (DR preference)
| Mode | When | CPU on be-media-N |
|------|------|-------------------|
| **Transcode ingress** | Mixed room (Android + browser) | **Recommended** — decode Codec2 → Opus egress |
| Passthrough | All Codec2 clients | Low CPU; no WebRTC viewers |
| MCU mix | Conference voice | High CPU; radio bridge legs only |
**Signaling sketch** (F1 SPEC — not implemented):
- `POST /api/sfu/room``ingress_caps: [opus, codec2]`, `egress_caps: [opus]`
- `POST /api/sfu/room/{id}/join` — returns transport params + `negotiated_audio`
Mobile: `SfuSignalingClient` fills `CastSettings.resolvedAudioCodec` from server; **LAN fallback** if signaling down.
---
## 10. Protocol extensions
### 10.1 `CastCodecFlags`
```text
AUDIO_CODEC2 = 16 // next free bit after SPEEX=8
```
### 10.2 `MSG_AUDIO_SELECTED` (recommended at M4)
```text
payload: uint32 audio_flag (little-endian) + uint8 codec2_mode_id
```
Symmetric with `MSG_CODEC_SELECTED`; reusable on WebRTC data channel.
### 10.3 `CastSettings.SETTINGS_VERSION`
Bump when adding `sfu_room_id`, `relay_mode`, `server_negotiated_audio` (SFU phase).
---
## 11. Third-party and licenses
| Component | License | Action |
|-----------|---------|--------|
| codec2 (libcodec2) | LGPL-2.1 | Submodule + Gitea mirror; dynamic link in `.so` |
| Opus / Speex | BSD | Existing |
| ffmpeg (SFU transcode) | LGPL/GPL | BE media worker SPEC only |
Update mobile `LICENSES` / notices when submodule added ([bottomline_reminder.txt](../../bottomline_reminder.txt)).
---
## 12. Testing and validation
| Phase | Tests |
|-------|-------|
| Unit | `AudioNegotiatorTest` CODEC2 mask; `CastProtoHeader` round-trip |
| Native | JNI c2enc/c2dec parity arm64-v8a |
| E2E | Extend [OPUS_SPEEX_VALIDATION.md](../OPUS_SPEEX_VALIDATION.md) — voice-only, UDP/TCP, diagnostics kbps |
| SFU | Post-alpha: 2× Codec2 Android + 1× WebRTC viewer via transcode |
---
## 13. Task dependency graph
```mermaid
flowchart TB
A61["6.1 Opus/Speex JNI sinks<br/>Developer · P5"]
C2["Codec2 NDK + voice-only<br/>Agent · post-alpha"]
AUDIO_HS["MSG_AUDIO_SELECTED<br/>Agent"]
SFU["F1 SFU SPEC + signaling<br/>PO · post-alpha"]
TRANS["SFU Codec2 transcode worker<br/>BE media"]
A61 --> C2
C2 --> AUDIO_HS
AUDIO_HS --> SFU
SFU --> TRANS
style A61 fill:#b45309,color:#fff
style C2 fill:#7c3aed,color:#fff
style SFU fill:#e5e7eb,color:#374151
```
**Numbered priority (PO overrides AI):**
1. **6.1** — Opus/Speex JNI (alpha LAN quality path; unblocks honest ULBW comparison)
2. **Codec2 ULBW** — this DR (parallel after M1 green)
3. **F1 SFU SPEC** — room negotiation + transcode
4. Alpha blockers **5.1 / 5.2 / 4.rssh** remain above on [OPEN_TASKS_GRAPH.md](../OPEN_TASKS_GRAPH.md)
---
## 14. Risks
| Risk | Mitigation |
|------|------------|
| Codec2 upstream churn | Pin tag; Gitea mirror |
| 8 kHz-only confuses users expecting music | Voice-only mode UX; no video leg |
| SFU CPU cost | Ingress transcode budget in scaling SPEC |
| Opus/Speex “done” confusion | ROADMAP + this DR state JNI sinks explicitly |
| BT latency 150300 ms | Document; jitter buffer 80120 ms |
---
## 15. Open questions (remaining)
| # | Question | Owner |
|---|----------|-------|
| 1 | SFU commercial story: always transcode vs optional passthrough mesh? | PO |
| 2 | Ship Codec2 in AUTO on ULBW profile or explicit “radio mode” only? | PO + UX |
| 3 | `700C` exposed to end users or dev-only? | PO |
**Resolved by PO (2026-06-17):** voice-only ULBW product intent; Codec2 3200 OK; skip MELP/TWELP; Opus @ 6k insufficient for target links; AAC platform use OK.
---
## 16. Changelog
| Rev | Date | Change |
|-----|------|--------|
| R0 | 2026-06-17 | Initial DR from draft; PO inline comments; Opus/Speex JNI status clarified |

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,40 @@
# Live Cast Control Plane (P0-P2 kickoff)
## Scope implemented
- New BE API `POST/GET .../api/live_cast.php`:
- `create_intent`, `heartbeat` (60s KA), `join_event`
- `GET action=active|analytics|session|list`
- Public (no login): `session`, `join_event`, education `create_intent`/`heartbeat`
- New BE repository `LiveCastRepository` with auto-schema:
- `live_cast_sessions`, `live_cast_join_events`
- Expiry rule: sessions without heartbeat for 60s become `expired`.
- Short links + QR per session (join + direct) via internal short-links service.
- Analytics payload includes `live_cast` section; shell label **Analytics**.
- Web pages:
- `/live/join` — viewer entry, join_event tracking, player placeholder
- `/live/education` — 5-minute browser screen-share demo
- `?view=live_sessions` — session tree (read-only, auto-refresh)
- Cookie consent banner (`cookie_consent.js`) gates GA4 until user accepts.
- Mobile:
- `LiveCastControlPlane` — start/heartbeat/stop from sender/receiver services
- `LiveCastPeerTracker` — polls `active` sessions; green border on discovery list
## Non-goals (remaining)
- WebRTC/SFU media plane and real HTML5 playback.
- Push notification delivery (Web Push / FCM).
- Gravatar / refined avatar chrome on web lists.
- Short-link click counter migration in url-shortener submodule.
## Data model notes
`live_cast_sessions` stores owner/device/platform, state transitions, short URLs, codec/BW hints.
`live_cast_join_events` stores open/join/leave events with platform/channel/direct flags and basic anon dimensions.
## Follow-up phases
1. Integrate SFU/web viewer when media-plane branch is ready.
2. Browser/mobile notification fan-out on cast start.
3. Gravatar + green-border on web peer lists.
4. Shortener click stats joined into live session analytics.

View File

@@ -0,0 +1,178 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 8 0 R /F4 9 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 762.0394 116.381 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 751.0394 124.813 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 740.0394 108.381 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 729.0394 108.373 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
9 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
10 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R ] /Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
12 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 20 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Outlines 15 0 R /PageMode /UseNone /Pages 20 0 R /Type /Catalog
>>
endobj
14 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Live Cast Control Plane \(P0-P2 kickoff\)) /Trapped /False
>>
endobj
15 0 obj
<<
/Count 4 /First 16 0 R /Last 19 0 R /Type /Outlines
>>
endobj
16 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 17 0 R /Parent 15 0 R /Title (Scope implemented)
>>
endobj
17 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 18 0 R /Parent 15 0 R /Prev 16 0 R /Title (Non-goals \(remaining\))
>>
endobj
18 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 19 0 R /Parent 15 0 R /Prev 17 0 R /Title (Data model notes)
>>
endobj
19 0 obj
<<
/Dest [ 10 0 R /Fit ] /Parent 15 0 R /Prev 18 0 R /Title (Follow-up phases)
>>
endobj
20 0 obj
<<
/Count 3 /Kids [ 10 0 R 11 0 R 12 0 R ] /Type /Pages
>>
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 350
>>
stream
Gas309kseb&-h(iregH[(*#.+hJ6skH*ASb@?K(l<nD1gqYN?Oqk95ea?YoVH'0kG$pMp1#M^\c-4JHr/_H4TRc6%:mfd&n>":b;AJ%6OAL]3UZ:uf`isa".?A/Xl:03adIjq0?J%#_TS[?aOQWn-o)_`4O;6gqb''466Z,1a*3+C\3Nr+9`fljW)@:%7/E2KmC2k.NukCWf`/(,Gmf*0+i,i^PaB6$4KVmu;VgN[M"g1N9U^:]BH$WE:RIGa#Bq(U5hHf_Z8[mI8;U"@PW?GC&4FiuH(5<'HMa)JoW!u*;-n@4Hj?Im&rWQ8F1kni'A5TJl-Zm3U2:DYDs,FBteMGNHDF+j~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 491
>>
stream
Gatn"?#SI?'Sc)J/)JHk;CMj0Z$/3N\XKBio4=_R3]-Aa_)%n_qnl0.D/Co"W]Cb`S-.)a4s9?21p:a,HtdQ/,*5(]#V&r-jr;#mMa%S17i$uIQ/VtSer7e`aS3!0nOc=u_.6hl?%H]9l@L_X(AU[N_%9,cB8/B%KU<%g`]Q5=n;'!tA:9=oBRZ`P1pMOB]>QAu48AB"-WG/A1t-r^@Betl_:_#imIgm5;1B;YHgnMV?sLWr3SN>3^@Su0bnV.-ohcXq9etqMeiNSflo!l@mqMejpaS6>NjjcE_qr>ti5DGR1r1gM&80NR+KJG2lB:X_J"r%'P<K:9lf6I11a(a2[e;K[(F:^<hmi+-DH[_&AfB;V\BWrj\#eIr/VHU#D<c_.lhj]"0oSol4FZ(hnD)]l*@'"h,fISSKJ@%FZD0>g`jbh!7eW\^9G^+.`9[Ad_Fm=5<h=g-5D^hg)-+3t68:o.bt0MkmNDPi@.>&E$M(k~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1744
>>
stream
Gatm<D/\-!&H:Nn0d"Lpf\"gn;H9V?Qh7:NHYGgs01aLE*q6W3Y3WLSOS\!Uei&UL&DVet0]66_1G@'1ZWPSLQW:a)$,FLsb\IS;)obg\*5d?t%:"nMr-W#).im4Y%,IKWT'%!)rlCd]FIP0llXs?9N"`F4^"L',bO<pND@a#jFRB7!,4Hk;0gUq3GQQpo0`FFDqEhB&ecrSADR2f/l&N2bp%7pSFC=mni5G1Kpl#Tj\:X>,O&4OdYN`<<?0Cq=qm#tUC(,i*Qn7?2biea40U3SkPh(cYLN^sHJ9C8HTkP%GJM>0,jnQV?=<_Y`3c-L0OCbsu)]B)mdM_Zkr,Y%^H+)nJjPg4'X/E`!Smre0MhZ?pHPY[IMYHs&a'VNEGa\.grDKR.h=q_+eHT783]-K<Kcfo:JgD\\ElQ)ZjK&Zt#.6)tNMo$IM.$uMmIBY@g3bX(0*fMb*Jm$!VGcqhB$2On)p6q^Z0#r;U03$TSk$SR>]s*Y@D^`[M-q?j9P\f9e7hnZo!mAXaf*o(I4uQ[)Y:$0;GMegMAXZI"h+o8/94kU+/Udq9l;$TR:$[mlDN,e]5c;5iPBL*./5</gKdrHGi_lc_0mT8*+COL.!Q,,mNBJbjU%&C#F'ZJ$"&ci:12?bkh=?R!'oe^h.j.1CsE-!"54\8)YWcfr'8DjZ","d$K<6t[Bh]YV67$:[!1R6`\GEUa*>#FXr"4b]HT.aiLu\8L-E.,6e-dB"4oW?k*JfneEMJ6VJP"^3ktn6$5"joT#-5*OCp->_8YaTd[2V#$!dUkA/o(0_@L;"e8h3(CH:;;N8%(:-uoGTP7+5QT2Zk#=%+Z-#VhGnOS8uMcG6&5`p_(PhB@OeqA!.S_G#obrY0OPLaapPP%Qc!:dpS3(-^B'6_f,[jU[@_&6N3I4qiP-3U&B4BY!K'?=nS'H:CL_UGf12_mol'aE\2J:(.L39QSa=J<p'\+0QmH-g>^#obOu^mZFPneZ(8K&PU3R0b_&[q)Pi"F5:M'Q-0l=R7sP1"B!Gih*u]m[M%d>IF0&q7kWng41C%!=:7O!"\io5)9:&K]G`f&lZYdSG;.KN"^bm&/eAO[Xo_090;72qlL$^FlouqIQD-I)N%f&(-&`40^,AZ+SR]$^]CD_CJ,L)p%&khV\k%@.O@DKMc@:m>h709eq\8N8N,eaOTo?N6<!5PMI*1d>3CGH,P`&+'pTnmk$.kN9-7<XQg"rE2qN.sB,Ee-g;F2uiK_gHHb4I5S+;]])f1HufENR'N;7^+q9b>ZKL>HL87k.()/??WW6W]Dhh/^:tXe;]`',f5cf`Im(gqhnH"oT6+#07A#6BoJJ@c;Pnk5X:ZXf*,N0t2AV-[h3.]g+1u#\Jlr_+^lnS-J9><3%*`D&PD;SS.TgdqU4`YBMqt3?UfnoAU]W1pXP]Y2eq0&cA4Vbu;\!Io7!BE+R&1jp9MslMS&1cO;#R4_(f:^0qqdR4A%"1IW[!-;(O9:m@)Xj.<qo9&*bjf;-W2d4J6e,GnWZL=P2!h;3%@=9a-u8n^7.OMmQUU@la@Le-r,am2ujk>VA4#e8="(G=GFU1`p<nI*uc""pT6d7h[hiMkg2RHWqR++;YX:j(t;R//')p$q;d)khU_Fi<^F,O43`e9Y>W>^pG#5!G!(.9riVQ,%+*Wq)C)qP]]Qct$9TY3US$SjP:ra0N>Y1p7X_[IKA6W;d3QKMaZbCZl5DVGY^GL*b\OT/o:<M&gj45>cB;I/~>endstream
endobj
xref
0 24
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000488 00000 n
0000000635 00000 n
0000000782 00000 n
0000000929 00000 n
0000001044 00000 n
0000001149 00000 n
0000001355 00000 n
0000001597 00000 n
0000001803 00000 n
0000001890 00000 n
0000002206 00000 n
0000002280 00000 n
0000002379 00000 n
0000002497 00000 n
0000002608 00000 n
0000002706 00000 n
0000002781 00000 n
0000003222 00000 n
0000003804 00000 n
trailer
<<
/ID
[<c4fec4b99072874c8420e5c9ea80366d><c4fec4b99072874c8420e5c9ea80366d>]
% ReportLab generated PDF document -- digest (opensource)
/Info 14 0 R
/Root 13 0 R
/Size 24
>>
startxref
5640
%%EOF

504
GIT_FLOW.md Normal file
View File

@@ -0,0 +1,504 @@
# Android Cast — Git flow (green `master`)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-23 |
| Last modification date | 2026-05-23 |
| Co-authored | |
| Severity | medium |
| State | completed |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
This document defines how we develop, integrate, release, and hotfix. **Cursor agents and humans follow the same rules.**
| Field | Value |
|-------|-------|
| Version | 1.4 |
| Published | 2026-06-08 |
| Author(s) | Anton Afanaasyeu |
| Markdown source | `docs/GIT_FLOW.md` |
| PDF | `docs/GIT_FLOW.pdf` |
| Diagram sources | `docs/_pdf_build/git_flow_diagram_*.mmd` (render via `build_git_flow_pdf.py`) |
---
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Goals](#1-goals)
- [2. Branches](#2-branches)
- [3. Branch diagram (steady state)](#3-branch-diagram-steady-state)
- [4. Daily development](#4-daily-development)
- [4.1 Fast track on next (alpha sprint)](#41-fast-track-on-next-alpha-sprint)
- [5. Release: next → master](#5-release-next-master)
- [6. Hotfixes — chosen strategy](#6-hotfixes-chosen-strategy)
- [Default: hotfix from master (option 1) ✓](#default-hotfix-from-master-option-1)
- [Exception: fix only on next (option 2)](#exception-fix-only-on-next-option-2)
- [7. Protection rules (CI + policy)](#7-protection-rules-ci-policy)
- [7.1 How to protect branches (not local hooks alone)](#71-how-to-protect-branches-not-local-hooks-alone)
- [8. Sync cheat sheet](#8-sync-cheat-sheet)
- [9. Agent / Cursor checklist](#9-agent-cursor-checklist)
- [10. Comparison to classic Git Flow](#10-comparison-to-classic-git-flow)
- [11. First-time setup (maintainer)](#11-first-time-setup-maintainer)
- [12. Private primary + GitHub publish (dual remote)](#12-private-primary-github-publish-dual-remote)
- [12.1 Add GitHub without replacing origin](#121-add-github-without-replacing-origin)
- [12.2 What to push where](#122-what-to-push-where)
- [12.3 Daily habit](#123-daily-habit)
- [12.4 Where branch protection lives](#124-where-branch-protection-lives)
- [12.5 One-way mirror (optional automation)](#125-one-way-mirror-optional-automation)
- [12.6 Suggested order of operations (your situation)](#126-suggested-order-of-operations-your-situation)
- [Related](#related)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Goals
| Goal | How |
|------|-----|
| **Always shippable `master`** | Only merges that passed CI/tests; tagged releases point here |
| **Safe integration** | Day-to-day work lands on `next` first |
| **Simple mental model** | Two long-lived branches + short-lived topic branches |
| **Recoverable hotfixes** | Production fixes branch from what users actually run |
---
## 2. Branches
| Branch | Role | Direct commits? |
|--------|------|-----------------|
| **`master`** | Production line; **green** (build + tests pass); release tags (`v0.1.2`) | **No** — merge only |
| **`next`** | Integration / release candidate; absorbs features until a release is approved | **No** — merge only (exception: alpha sprint direct commits when maintainer/agent explicitly commits here — [§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)) |
| **`feature/*`**, **`fix/*`**, **`topic/*`** | Short-lived work; fork **`next`** | Yes (normal dev) |
| **`hotfix/*`** | Urgent fix for **released** code | Yes; merge target **`master`** first |
**Naming:** `feature/cast-resolution-presets`, `fix/crash-upload-500`, `hotfix/v0.1.1-pin-crash`.
If `next` does not exist yet on the remote:
```bash
git checkout master
git pull
git checkout -b next
git push -u origin next
```
---
## 3. Branch diagram (steady state)
```mermaid
flowchart LR
subgraph long["Long-lived"]
M[master\nshippable + tagged]
N[next\nintegration]
end
F[feature/*\nfix/*]
H[hotfix/*]
F -->|PR / merge after CI| N
N -->|release merge after CI| M
H -->|merge after CI| M
M -->|sync after release or hotfix| N
```
---
## 4. Daily development
1. **Start from `next`:** `git fetch && git checkout next && git pull`
2. **Create topic branch:** `git checkout -b feature/my-change`
3. **Commit** in small logical chunks on the topic branch.
4. **Before merge:** run project checks (Android: `./gradlew :app:testDebugUnitTest` or CI equivalent; backend: relevant tests/lint).
5. **Merge to `next`:** prefer merge commit or squash per team habit; **do not** merge unfinished work.
6. **Topic branch cleanup:** optional locally (`git branch -d feature/...`). On the server, prefer **delete branch on merge** (Gitea/GitLab/GitHub) so merged `feature/*` / `fix/*` are removed on `origin` automatically.
```mermaid
sequenceDiagram
participant Dev as Developer / agent
participant F as feature/*
participant N as next
participant CI as CI / local tests
Dev->>F: branch from next
Dev->>F: commits
Dev->>CI: test
CI-->>Dev: green
Dev->>N: merge feature → next
Dev->>F: delete branch
```
### 4.1 Fast track on `next` (alpha sprint)
During intensive alpha work, the team may commit **directly on `next`** instead of short-lived `feature/*` branches — to move faster and avoid branch overhead.
**Trade-off:** integration history on `next` is harder to read; **mitigation: atomic, revert-friendly commits.**
| Rule | Why |
|------|-----|
| **One feature (or one logical slice) per commit** | `git revert <hash>` removes only that slice |
| **Do not mix** unrelated areas in one commit (e.g. RSSH + graphs + stream dump) | Avoids reverting good work when one feature misbehaves |
| **Commit message names the feature** | e.g. `dev: stream dump writer (FR 0.1)` — easy to find in `git log` |
| **Tests/docs for that slice in the same commit** when small | Keeps revert self-contained |
| **Prefer revert over edit** when a feature is rejected | `git revert` on `next`; do not leave half-removed code across commits |
**Good split (three commits on `next`):**
```text
dev: network self-test — fix BACKEND head/uplink probes
dev: diagnostics — full advertisement info card
be: config.example mail stub for 2FA SMTP
```
**Bad (one commit):**
```text
alpha wip: self-test, diagnostics, mail config, ingest script, docs
```
**Revert one feature:**
```bash
git log --oneline -20 # find the commit
git revert <hash> # new commit undoing only that change
```
Agents: only commit to `next` when the user asks. When they do, follow the atomic rules above. Default remains **topic branch from `next`** for longer or risky work.
---
## 5. Release: `next` → `master`
**When:** `next` is feature-complete for the version, soak/QA agreed, CI green.
1. Ensure **`next`** is up to date and **all tests pass**.
2. **Merge `next` → `master`** (merge commit recommended for traceability).
3. On **`master`:** final smoke / CI.
4. **Tag** on `master`: `git tag -a v0.1.2 -m "Release v0.1.2"` then `git push origin master --tags`.
5. **Sync `next` with `master`** so both lines share the release commit and any last-minute release fixes:
```bash
git checkout next
git merge master # fast-forward or merge commit
git push origin next
```
6. Continue new work: branch **`feature/*` from `next`** again.
```mermaid
sequenceDiagram
participant N as next
participant M as master
participant T as tag v0.1.x
Note over N: CI green, QA ok
N->>M: merge next → master
M->>M: CI / smoke
M->>T: annotated tag
M->>N: merge master → next (sync)
```
**Versioning:** tags on `master` only (`vMAJOR.MINOR.PATCH`). Bump per [SemVer](https://semver.org/) conventions you adopt for the app.
---
## 6. Hotfixes — chosen strategy
### Default: **hotfix from `master` (option 1)** ✓
Use when the bug affects **what is already released** (code on `master` / latest tag).
1. `git checkout master && git pull`
2. `git checkout -b hotfix/v0.1.2-crash-on-stop`
3. Fix + tests
4. Merge **`hotfix/*` → `master`**, CI green
5. **Tag** patch on `master`: e.g. `v0.1.3`
6. **`git checkout next && git merge master`** — bring hotfix into integration (resolve conflicts if `next` diverged)
7. Push `master`, `next`, tags; delete `hotfix/*`
**Why not hotfix only on `next` (option 2)?**
`next` often contains unreleased features. Fixing there delays shipping the patch and risks dragging unrelated commits into production when you later merge `next` → `master`.
### Exception: fix only on `next` (option 2)
Use when the bug **does not exist on `master`** (introduced on `next` only).
1. `git checkout -b fix/...` from **`next`**
2. Merge to **`next`** only
3. No tag until the normal **release** merge `next` → `master`
```mermaid
flowchart TD
Q{BUG IN RELEASED\nmaster / tag?}
Q -->|Yes| H[hotfix/* from master]
H --> M[merge → master]
M --> T[tag patch on master]
T --> S[merge master → next]
Q -->|No, next only| F[fix/* from next]
F --> N[merge → next]
```
---
## 7. Protection rules (CI + policy)
| Rule | `master` | `next` | topic branches |
|------|----------|--------|----------------|
| Required CI (build + tests) before merge | ✓ | ✓ | ✓ (before PR/merge) |
| No direct pushes of WIP | ✓ | ✓ | — |
| Only maintainers merge to `master` | ✓ | optional | — |
| Tag only on `master` | ✓ | — | — |
| Force-push | **never** | **maintainer only, rare** | allowed on topic branches |
See **[§7.1 How to protect branches](#71-how-to-protect-branches-not-local-hooks-alone)** below.
### 7.1 How to protect branches (not local hooks alone)
**Short answer:** use **server-side** rules on `f0xx.org` (or whatever hosts `origin`). **Local git hooks** on your laptop are optional helpers only — they do not stop you or someone else from pushing bad history from another machine.
| Layer | Where it runs | Stops bad pushes? | Good for |
|-------|----------------|-------------------|----------|
| **Hosting UI / branch protection** | Git server (GitHub, GitLab, Gitea, …) | **Yes** | Most teams; use this if available |
| **Server git hooks** (`pre-receive` / `update`) | Bare repo on server | **Yes** | Self-hosted `git://` / SSH without a web UI |
| **Local hooks** (`.git/hooks/pre-push`) | Your PC only | **No** (easy to skip) | Personal reminders, format/tests before push |
| **CI** (build on push/PR) | CI runner | Indirect | Prove tests passed; pair with “required check” on server |
#### What to enable on `master` and `next`
1. **`master`: never allow force-push** — production history must not be rewritten.
2. **`next`: force-push off by default** — enable only for maintainers when you intentionally clean integration history (squash/rebase of already-merged work). Prefer fixing history on `feature/*` before merging to `next`. After a rare `next` force-push, anyone with a clone must reset: `git fetch origin && git reset --hard origin/next`.
3. **Delete branch on merge** on the server — merged `feature/*` / `fix/*` disappear from `origin` without a manual delete step.
4. **No direct push** (optional but strong): changes only via merge from `feature/*` or from PR/MR.
5. **Require CI green** before merge (if you have Actions/GitLab CI/Gitea Actions).
6. **Restrict who can push** to you (and CI deploy key if needed).
`feature/*` branches stay **unprotected** — push freely.
#### If your host has a web UI (Gitea / GitLab / GitHub)
Look for **Branch protection** / **Protected branches**:
- Add rules for `master` and `next`.
- Enable: prevent force-push, require pull request (optional), require status checks.
No git internals required — the server rejects illegal pushes.
#### If you only have bare git on a server (`git://` / SSH)
Ask whoever administers `git://f0xx.org/android_cast` to install **server-side hooks** in the **bare repository** (not in your clone):
- `hooks/pre-receive` or `hooks/update` — script receives branch name + old/new SHA; **exit 1** to reject (e.g. block direct push to `refs/heads/master` unless user is deploy; block all force-push to `master`/`next`).
Example policy in words: “reject push if ref is `master` or `next` and user is not in allowlist” or “reject non-fast-forward updates to `master`/`next`”.
Local clone hooks live in `.git/hooks/` and are **not copied** when others clone — so they are **not** team protection.
#### Local hooks (optional, for you only)
Useful on your machine, not security:
```bash
# .git/hooks/pre-push (chmod +x) — example: warn before pushing to master
protected_regex='^(refs/heads/)?(master|next)$'
while read local_ref local_sha remote_ref remote_sha; do
if echo "$remote_ref" | grep -qE "$protected_regex"; then
echo "Blocked: push to master/next — use feature branch + merge (see docs/GIT_FLOW.md)"
exit 1
fi
done
```
Skip with `git push --no-verify` — another reason server rules matter.
#### CI without branch UI
Run tests on every push; you still **choose** not to merge until green. Branch protection + “required check” automates that on modern hosts.
---
## 8. Sync cheat sheet
| Event | Action |
|-------|--------|
| After **release** (`next` → `master` + tag) | `git checkout next && git merge master && git push` |
| After **hotfix** to `master` + tag | Same: **merge `master` → `next`** |
| Feature finished | merge **topic → `next`** only |
| Started wrong base | rebase topic onto latest `next` (coordinate if already shared) |
---
## 9. Agent / Cursor checklist
Agents working in this repo **must**:
1. **Never** commit or push directly to `master` or `next` unless the user explicitly asks for that merge/release step.
2. **Branch from `next`** for features and non-production fixes (`feature/*`, `fix/*`).
3. **Run tests** (or state clearly if not run) before recommending merge to `next`.
4. **Releases:** only describe merging `next` → `master` and tagging after user confirms QA.
5. **Hotfixes on released code:** branch from `master`, merge back to `master`, tag, then **merge `master` into `next`**.
6. **Set active branch** metadata when using a named topic branch (project convention).
7. **Do not** force-push `master`; avoid force-push on `next` unless the user explicitly requests integration history rewrite.
8. **Fast track on `next`:** when the user commits directly on `next` (alpha sprint), use **atomic commits** — one feature per commit so `git revert <hash>` can drop a bad feature without touching others ([§4.1](GIT_FLOW.md#41-fast-track-on-next-alpha-sprint)).
---
## 10. Comparison to classic Git Flow
| Git Flow | This repo |
|----------|-----------|
| `main` | `master` (green, tagged) |
| `develop` | `next` |
| `feature/*` | same, base = `next` |
| `release/*` | optional short branch; default = test on `next` then merge to `master` |
| `hotfix/*` | from **`master`**, merge to **`master`**, sync to **`next`** |
We intentionally avoid a permanently divergent `develop` without a green production branch: **`master` is always releasable.**
---
## 11. First-time setup (maintainer)
```bash
# Ensure next exists and tracks origin
git checkout master && git pull
git checkout -b next 2>/dev/null || git checkout next
git push -u origin next
# Optional: protect branches on hosting (GitHub example)
# gh api repos/{owner}/{repo}/branches/master/protection ...
```
---
## 12. Private primary + GitHub publish (dual remote)
**Canonical repo:** your server — today `origin` → `git://f0xx.org/android_cast`.
**Daily work:** always `git push` / `git pull` against **`origin`** first.
**GitHub:** secondary remote for visibility, issues, and (optional) GitHub Actions — not the source of truth unless you decide otherwise.
### 12.1 Add GitHub without replacing origin
```bash
# Create empty repo on GitHub (no README if you already have history)
git remote add github git@github.com:YOUR_USER/android-cast.git
# origin stays: git://f0xx.org/android_cast
git remote -v
```
Keep **`origin`** = private server. Name the public remote **`github`** (or `publish`) so commands stay obvious.
### 12.2 What to push where
| Ref | Private `origin` | GitHub `github` |
|-----|------------------|-----------------|
| `feature/*`, WIP | ✓ always | optional (omit for cleaner public) |
| `next` | ✓ always | your choice: public integration or keep private |
| `master` + tags | ✓ always | ✓ publish releases you want visible |
| Secrets, `local.properties`, logs | never in either | never |
**Typical OSS-style:** push everything to **`origin`**; push only **`master`** and **tags** to **`github`** until you are ready to open `next`.
```bash
# After merge to master on private server
git push origin master --tags
git push github master --tags
```
Push `next` to GitHub only when you want public integration:
```bash
git push origin next
git push github next
```
### 12.3 Daily habit
```bash
git fetch origin
git checkout next && git pull origin next
# ... work on feature/foo ...
git push -u origin feature/foo
# merge to next on origin (PR or local merge + push)
git push origin next
# When ready to publish a release line to GitHub
git push github master --tags
```
Set upstream on topic branches to **origin**, not github:
```bash
git push -u origin feature/my-change
```
### 12.4 Where branch protection lives
| Server | Protect |
|--------|---------|
| **f0xx.org (`origin`)** | `master`, `next` — server hooks or admin UI (primary enforcement) |
| **GitHub (`github`)** | `master` (and `next` if public) — Settings → Branch protection |
GitHub rules only affect pushes **to GitHub**. They do not protect your private server unless you also configure **`origin`**.
### 12.5 One-way mirror (optional automation)
On the private server (cron or post-receive hook), after a successful push to `origin`:
```bash
git push --prune github +refs/heads/master:refs/heads/master +refs/tags/*:refs/tags/*
```
Or use GitHubs “mirror repository” importers only for initial import; ongoing sync is usually `git push github …` from CI or hook.
Avoid `git push github --mirror` unless GitHub is a full duplicate of **private** history — it copies all branches and deletes remote branches you deleted locally.
### 12.6 Suggested order of operations (your situation)
1. On **f0xx.org:** create **`next`** from `master`, push `origin next`.
2. On **f0xx.org:** enable server protection for `master` / `next` (admin).
3. Create **GitHub** repo; `git remote add github …`.
4. First publish: `git push github master` and tags you want public.
5. Enable **GitHub branch protection** on `master` (block force-push).
6. Continue feature flow on **`origin`** only; publish to GitHub when you merge/tag on `master`.
---
## Related
- [README.md](README.md) — documentation index (all guides)
- [ROADMAP.md](ROADMAP.md) — product tracks
- [.cursor/rules/git-flow.mdc](../.cursor/rules/git-flow.mdc) — agent enforcement
- `docs/_pdf_build/build_git_flow_pdf.py` — PDF generator

627
GIT_FLOW.pdf Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,142 @@
# Android Cast — Graph Visualization Pivot
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-02 |
| Last modification date | 2026-06-02 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | DR |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Date: 2026-06-02_
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Context and constraints](#context-and-constraints)
- [Executive summary](#executive-summary)
- [Option comparison](#option-comparison)
- [Role-based graph packs (suggested)](#role-based-graph-packs-suggested)
- [User graphs](#user-graphs)
- [Slug admin graphs](#slug-admin-graphs)
- [Platform admin graphs](#platform-admin-graphs)
- [Estimates (engineering)](#estimates-engineering)
- [Recommendation under your constraints](#recommendation-under-your-constraints)
- [Implementation slice (proposed)](#implementation-slice-proposed)
- [Source linkage](#source-linkage)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Context and constraints
- Stack: minimalistic LAMP with nginx + php-fpm + MariaDB.
- Main constraint: **no data duplication/ETL pipeline** (no cron-based mirroring or parallel stores).
- Need role-hierarchical analytics: user → slug admin → platform admin.
## Executive summary
- **Grafana is viable** if it can query MariaDB directly and enforce tenant/role boundaries safely.
- **Custom PHP dashboards are safer initially** for auth/session reuse and strict slug scoping.
- Recommended approach: **hybrid**.
- Define a canonical SQL metrics contract once.
- Ship custom BE dashboards first.
- Add Grafana later only if direct DB + RBAC integration is clean.
## Option comparison
| Option | Pros | Cons / Risks | Stack impact |
|---|---|---|---|
| Grafana (direct DB) | Fast polished charts, panel ecosystem, alerting | Auth/RBAC integration effort; slug filtering discipline required; one more service to operate | Adds Grafana runtime (no ETL required) |
| Custom PHP + nginx | Native shared login/session, direct BE internals, deterministic tenant logic | More UI/chart development effort | No new runtime dependencies |
| Hybrid (recommended) | Fast practical delivery + future flexibility | Needs shared metrics contract discipline | Minimal immediate risk |
## Role-based graph packs (suggested)
### User graphs
- Unique devices per day (registrations/device IDs).
- Avg cast duration by day/week.
- Avg receive sessions by day/week.
- Success ratio (session start vs completed).
- App version distribution.
### Slug admin graphs
- Everything from user level, aggregated by slug.
- Active vs passive users/devices.
- Avg app bandwidth (send/recv) in 1d/7d windows.
- Install source pie: Play Market vs OTA vs custom.
- NTP/time-source usage split and correction savings metric.
### Platform admin graphs
- Everything from slug admin level, cross-slug view.
- Crashes per slug/device/user over time.
- Crash trend by app version / Android API / fingerprint.
- Crash-to-ticket linkage counts with issue tracker links.
- Top unreliable slugs/devices (rate-based).
## Estimates (engineering)
| Track | Initial delivery | Hardening | Total |
|---|---:|---:|---:|
| Grafana direct DB | 4-7 dev days | 5-8 dev days | 9-15 dev days |
| Custom PHP dashboards | 5-9 dev days | 3-6 dev days | 8-15 dev days |
| Hybrid phase-1 PHP then optional Grafana | 6-10 dev days | +4-7 dev days (optional) | 10-17 dev days |
_Assumptions: current MariaDB crash/ticket/session structures are available; no major schema rewrite._
## Recommendation under your constraints
1. Keep **single source of truth** in MariaDB (no ETL copy pipeline).
2. Build **SQL metrics views/contracts** first.
3. Implement **custom PHP dashboards** first for guaranteed auth and tenant correctness.
4. Add Grafana later only if direct DB + RBAC mapping is validated without complexity growth.
## Implementation slice (proposed)
1. Define metrics dictionary + SQL views by role scope (1-2 days).
2. Extend heartbeat with `time_source` + NTP correction savings fields (0.5-1 day).
3. Build user + slug-admin dashboard pages with 1d/7d selectors (3-5 days).
4. Build platform admin reliability board with crash/ticket links (2-3 days).
5. Optional Grafana POC on same views (2-3 days).
## Source linkage
- This markdown file is the editable source for:
- `docs/GRAFANA_vs_others_graphvis_pivot.pdf`

View File

@@ -0,0 +1,327 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 15 0 R /F4 16 0 R /F5 17 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 762.0394 129.725 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 751.0394 115.477 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 740.0394 112.821 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 729.0394 176.853 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 718.6019 100.53 728.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 708.6019 122.2125 718.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 698.6019 135.1275 708.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 688.0394 130.157 699.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 677.0394 189.741 688.0394 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 666.0394 159.501 677.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 655.0394 98.59704 666.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
17 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
18 0 obj
<<
/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R ] /Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
20 0 obj
<<
/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
23 0 obj
<<
/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
24 0 obj
<<
/Outlines 26 0 R /PageMode /UseNone /Pages 38 0 R /Type /Catalog
>>
endobj
25 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 Graph Visualization Pivot) /Trapped /False
>>
endobj
26 0 obj
<<
/Count 12 /First 27 0 R /Last 37 0 R /Type /Outlines
>>
endobj
27 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 28 0 R /Parent 26 0 R /Title (Context and constraints)
>>
endobj
28 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 29 0 R /Parent 26 0 R /Prev 27 0 R /Title (Executive summary)
>>
endobj
29 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 30 0 R /Parent 26 0 R /Prev 28 0 R /Title (Option comparison)
>>
endobj
30 0 obj
<<
/Count 3 /Dest [ 18 0 R /Fit ] /First 31 0 R /Last 33 0 R /Next 34 0 R /Parent 26 0 R
/Prev 29 0 R /Title (Role-based graph packs \(suggested\))
>>
endobj
31 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 32 0 R /Parent 30 0 R /Title (User graphs)
>>
endobj
32 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 33 0 R /Parent 30 0 R /Prev 31 0 R /Title (Slug admin graphs)
>>
endobj
33 0 obj
<<
/Dest [ 18 0 R /Fit ] /Parent 30 0 R /Prev 32 0 R /Title (Platform admin graphs)
>>
endobj
34 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 35 0 R /Parent 26 0 R /Prev 30 0 R /Title (Estimates \(engineering\))
>>
endobj
35 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 36 0 R /Parent 26 0 R /Prev 34 0 R /Title (Recommendation under your constraints)
>>
endobj
36 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 37 0 R /Parent 26 0 R /Prev 35 0 R /Title (Implementation slice \(proposed\))
>>
endobj
37 0 obj
<<
/Dest [ 18 0 R /Fit ] /Parent 26 0 R /Prev 36 0 R /Title (Source linkage)
>>
endobj
38 0 obj
<<
/Count 6 /Kids [ 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R ] /Type /Pages
>>
endobj
39 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 867
>>
stream
Gat=*gMWc?&:Ml+b[\+[?'"LZ*ja[>V,aqWVPm9@7Ar2JWrQ9h;K]a'D,S5^'*8aP(A=7SB?&s>!jrQ)n)ji3CC&P$#fn,n!-5q"]SGt7jNdrn/\*"P(R0PMMGPta(2&C/opI0i+C61inpr4*!fiAW9IqRZh'`OBl[f-EkCPGO!%G8J(/5s2iF87qH"LNqM:Mk.Ak^$H*j&c-afIGKdeo(3PGRclCpW+/KPqE._H!)B.XnVJ+k73^W)X4_JOJ_1?.Bu.45K*%YaM2fiuTgjo.5I)g%9IaF#r3]k?7>ToC`%#l.qg$I-!bZ,d)7_#SW(L?^8Ks#1Bt`[`O\kmDjS&SN<,c/9=&n121eS+P3j-[%-Q`C`#<G6V9ZTh6oYte',Wg`9K1_4GT8qk4R"l6pGV#O;Rb([BT[4OmVda;b>08DT/kqSWrd_m#''Ojmbr0JV6sdk:f+hbDkpD2pnetHU=G*!/K3K38G:P=[!so1T8d>mLI]7PXasL]fEgt+;g9>(ACMW7;XuUe&'Gh`ZD*oF;f%;^"XppW9@"D!PRM60ZrGk[6N6;>E2Ir^"`1C/2dn-4=5UKSd#A6a!X'_MTY58"4jJKh>Npt2`d*l^"'YB5#1toc^j:PCm(@uiTL\5(5$4@4Rl%AYt8<h?G&/l]gq\X/:>X42i19;-pCZ6UlX]2*?b;hs)/S11W>']RrmB;S\;bQqu46(lLW)F0&a&TU<N_mlD>$.L<1#OG@,"nZ'rmB$CX>h7KkA?:h*H4U2#?t+_6Sj/Hi6h?45^LFL>[[#io_Z9b+Z4W+(07G!hHAM*rGd'h]62<"ccZkK"'fr&cbS<?OCmd'oJs'1<u>jn80Gi>Q7-&7G&>WpkU7~>endstream
endobj
40 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 680
>>
stream
Gatn$d;GF-'Sc)J'^)cm1`%*@_sXh4R3kX4f<W"ZOUDZh!]IAZg:mLo8Y`E4"fW%G+L([UF8-1]U9NTb?Sha)F1>C]^]a3c@#?Anhr$PfrL)W^8=CZe-4pF,3JugTL3_`9MGb'70q^L9&PiGtKObdLX_-O[pkZ`uj@s$D"+u],E06(U>(TmE@C2BT]1^4p6iO6$rs[X25)\:5O8!l-ktiNEkb!1bUBdL9?GLh:e2R]%:$$?m.#ZBOSHRP%NNc2'k]&WurC?1=d@p?4'im>i8'o@^hZ$1DXf:FZHf8KAPniZj>#B)V/2(70<RQ.q/h]^#iU_aW"bMnM-f1JuLPuG<?V]=gc^5tGe&(S=Xo_HY8rm"\fsD)F[I@sARr^Gb2VFO<?8]UC4pk1h8kWl`Y/Z-GnRSqIb-3E8WCntIO\V5d(Jgp7kC"itUJi%YD;&.t,JQ0X[45^#;/8mAgpk>?P'AIG]?lGNYEdt%8rc/8Naa'g*dr?"bYWU&&DWESB+2]$j"%V%*65kh.IJi/rBA#`g\G=XID4FGQ15f-h.hc<::XB*G/g;SKI%c]S1KE/p(]T40QmgIs#6PdYZ(*99E`*B5('9_elX$^H"j[b_saAQ)!fPL(*H@,jpYQ;pE^gVR3?bKh>U>T&p&`Tn4lN+&rb;5a9r&N_#EB0ln:a_etW~>endstream
endobj
41 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
42 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
43 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2979
>>
stream
Gatm>8UK2O&&.4.i0[8T=k'VHP\r;UT!jikmO]5EU2;gB8kMkWmlun;XLP\ig%0;WAC:Sdi!!H&-=GgTUAp9PJ2[k'Iiq!-==f"_L'*?0n:#BMl\[M\*S9c^f6"e=4i9#q+5_;8p#(S?2>h((Y:ILJ8a$c=s!^uM.$Sh"fS0cl'Xb0]f1MFqKhl[dNgB#9/CX0%-N:DZ^u?^mr_hHu4f3a@"MRG31c,ZjG+IY4Hlqfj"p5ea_tL_D&O%pSS%-KtG_+h-?9#1*2$2+0"ub+gC&-W$#1F]g2'Bd/%GM?Yiu9PYg[OO>^Bd^)6M8\qo+..9?KtN(I1+YFo;lK;9;#Dr;4bm-:nEqmK_Njb5ps!%YV-uke7tb45k$=.mYUDMpQA",2j!ThV_mA<j:QdNrHniG]Ndh4in86p$,sKC/W1<mqb.0kn(nm+PV^S%#2/7jWm^.?Z\_`N1YSO8OW2YFPmR-OlR[gYQ5&>Y89dNqSnIZhG[A2oQLKhjgRH*aiEj2B&pZB^TT[9=CE[`7ZPDO&$7]_PYQSL)$RY=LN4>W!KU4aX$U[?NT!6n:1I4I:`g\4j$T-P21ZT!EP)8npcMeEAlTN!o[feIc<^:/:m5aVf&m?o/Ok(n=Ylu<s3"H#XJ$Z('q1aI%dZfXV#\f<IBlr_Ge),a0dH\LnCcHi6reMW0"C=D_1\as)Z#-070c)AjPHDhXbKso7K9Be&/*G'[n;p^SN164<8XD!]Nj5Hu*Ob+`pNp+Q@&Iu<cQ.U,Xrh[6@L6!fXcrh0!$t,<D"mCX(BRd*la^P(,/c`#$qYkO"=B2P3mqQ'(C6ilGI+ADIJ8^iH()#i7e"d!&35a$J#%kmp-J1mmlFtN,NMa%^LKp\5<r5YjI&L0pTL23A]eCYjYTYFBXp6`fGKTAd^.SOM13\WM!>h%o=c:J9.ja864#T`\D#`X0sipXr.7e4B*]JF<Xg6<35'u`Df4HA30nA,FA>o!N"SV#A0KP6cHS]U(0VOZ(7#"SoB`D4j<LNG/C(SdB:+6]]/CLLka?l"L@8<>UZl!;D[V!K*uYp"4I?^";$>(Wn4<5Mfb-9X/N.=O)#?2sp4"u=6AYs(o^#hqo54q"ltt./enV^jo`q%q+n1Nh*)EaYhAd)ADqDE[C-Y,4E+m*E:;.D;EFr`if(;Z)*0$)68XRCS/g$.HD`b\lR%\T@q3ksta4Q9l^%;,V.,Z(hKKfIN:+YXfD:*NVh').a`G*)3cB+\;8UHUa:g$,I,DTN6;NB=-Pk#QpWf/7+]PGaFE[seK^&j`W;.@3um7Z%O]X2rLQWmImr^\8MM5@;$Xij_BdZPuFLD0uP\Vtd,GU!:N=,#do?XnOoh:=Da`KH&e<s:dC]Kj>7ZI@3AL,C\aat?\O.P#eeCAs(n\'q;+F24W>*NZ5+e"[7'lQ4u+`)qY94QrUp[,u`Q.Msm3Ne1GpnZ"#Dh+b.b.9?kP`>DT0_d?EiUAL4ECNWk36.+r$RVnG;b5X[ic?3ru%NC_p"d4tQM6,0W0O<h-*&@6;76o'(7=)6FalIuq$(3:++3g')_ShI;NCah:(E1@.MZ%3l'P"&L;0;X\/??&h41HjUp7";9KU9aVJ^0b6oh^X7CkHYuQuu$fP/:r%::L)_]'[MCOKt6`/cd4=Ns?s/&-)U@1'5XC>QXde^\rS7/F0:3)<n<XitF1D]jUP@2kRd"me6<s*'OR`7rJF#g'-O\]`idu(Q`V.(@\;0+N16H84i(f3_>VPet,mbh*\Yf4&0X7[G3o%b?p=86cX<YUp7u'5`TE")^a"RDlSSrk6b=1MD^^6kYWOuA@5Sq/:'[.l4gJ?n<a8Cdi&Cl#,-@:YJLf-.F>q%%$0bj1X&VB=`jX5`j$7ZR1LmC">#>\=)<!l=`H=+;.WhSlZN!tDVS(-p>);HcKd8E/*so&cbrg^q]E7=[J:JGO(2/Yge2LZ<5;b]FG6cRR[gC3lWe1\mo>%4OeAhL@^0Y+AZ8;S7d=k=@6-BgBS8X_e1O/jR.%EuYmM-C:alp7O`$RMknC>lI5_TSN=aRNeGeo:Be.em?7lCZ.QtsJ'QZMD]NQIR0^6V-/ef`T@08$m(q$i8VHrE`JmpRpY=bi2VK.V._1.&5MP9N)ZhTBtj]Vn%(=*RKS,4!mp41)&5DCPW`VIt2Xo+)Cq_:HG,RMQ);FA*KZQ$DIW1NZF1(BV#(Uk;ARNdg?b.AM/?P[j,`VsJ/>*Ym69Ie+Ug<U1\ilW1kFG$hF)a6&I4i]\"hV:8`&WaqsAbhZRC*-Nj=a,M$8E<[oo]mIAT#R.L>R^A9Jn"IY=$e4E_:2`-5"D/0q/M?Gk/B%+\j.\U<%>Z0?J_6jD&/$Lq>Td;7snL7*LS=[$Q_)/,iQ`+f=K5LI"je7_A2Y-7hsKMNdZqh5C;Bf,.CBbr))QLgbC,^OPo+DYfI3N14ak1q@l3lYtL+7gL/%(H/>K_8R2-pd5q*.#^3[+8lJWUC;ZG\<".-pFtad:8nL.E3AHonaN;8o'-,_H@PDN.-dTJJ0^5$)a)DD$:T)t$km)^>5<e?.fb&;-aYNjh!djs@Tip!*Sme$,9AeEd8_ZIR1>0nu:O?Za&81.+p-c1R&Q@`SkI\(Yo_G%VVOnsU&C%*VS=7/$chM[U"^O7Q4(t@-Jd\HJDpQ'&/sq*:0\-Dk-W&(3X]T'fR1\hDEm6A\onOnV0Xae<m@EJT-uVT!$1=36i+4!^=NK4>%?7I8+/fcm7KsGhIZT:qYrB,.Vbr)RDVsD=>43<0WP&I_C)=*lp(m/^X?g#;l0ogt><Y/?CR1"kdZA#DQ8S;W.+b]X%KF7'gMW,^VE/bY%JF,\mpp#("cQ!Waa$T,kIV%UW$4)pY.s!^2j>\Icb5&aLQqe^OeG9\Q;StK:P+pC=Ln:RamN'UVM"8<]cK[D=cB/"MuDGh2nf^X6#:nPs)V8(21J&e2C5o3PAC%"$'=RX8Cn1=@\r"jMfdj'-3p^Q%gJ!-1]~>endstream
endobj
44 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 972
>>
stream
GatUr>BALP&;B$5/']AR#(m>uUi3&#0@R:&>992@KI&mCD7PfPiSSij"&3LX3-77bK.kZ^B?s_ud&2NHT322"N7!!q^d="C@)HB.nMRJh-U1DZp<I(\`+d^C,"^6+)Oq?&3P%lpZMds8&7Jgop&4W-]jg7Cm+7"qTYghYPd8.T`]r[5fNdlI<A&:uX&gX3cuV1QEm*iEgI4/",sH=DpuMZ+SPg-Y2)8HS:oR(LWg&^rTRLMNb-QSoj"^NY%Jn-9LB0#..MmjDI/eMP69:ksMm\^l[[67,)4ij8=N+M_le6<tamhD&!dlDr>SaVeLTK1O3aL\+r=&n=bF.K^4ALaD2H[&jiMD`9J5]C^9*$D53^r$I,;WCnCYKO;,nG[dYs)@E*b8>]B,G#%"Pt"[k9WXDSM\])^4D_?.$"E"`>QBqN8C5c<CN4*U2KI*$+a@!::[3-H"sP,]lG&CN4sAV2XFlEP\D!+`*q;?,-68YCrO+!gaQOGk)(]Rku:,Rg.F([D6^^>iE51iEgLfAl.?qSW6D#UZQ[#pF)/g*[XlgiKp4#^aLWN4A7OAjWo*i[<*e=DVZ`<K)`Nu9e_Sq>2aUESd]#Q.Riue8f/%dqhs:bSlcduA!)#eI\Zf$CD;Sp=TN:a(l1sk4%sCl\A]^c]eFV4bb=+]!CnE[dHm*?*-54'\^ri2"[ILJRDU`"i3'%#B?o>##AW$%bTE-o,;>EoI4sg_XNLC_7T]]9t.ii<chKmn[4*UdCS0"kM'];VaRlB6NJt"E3:cH%K>:71MBFmBcBqGe-pZ@IsKqNUtfI@<%Bbe=5T<*LY&dlL=VHUg\XjD8<fp`UI-eqTd$K_'LpNr.9q76_B@uj^kS)b[=XcX*&.Wf9Ln%Oe<N]rN$CHL&LBC'_KGdUd,40@"9]:]f.^Yj]SaB0)8n@Ofr^\rEhDuZFl^l8Oi^sXJ3NTu>)ZmP7[kTTq8+.W61*_Sub~>endstream
endobj
xref
0 45
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000501 00000 n
0000000648 00000 n
0000000795 00000 n
0000000942 00000 n
0000001088 00000 n
0000001236 00000 n
0000001385 00000 n
0000001533 00000 n
0000001681 00000 n
0000001829 00000 n
0000001978 00000 n
0000002094 00000 n
0000002172 00000 n
0000002278 00000 n
0000002484 00000 n
0000002776 00000 n
0000002982 00000 n
0000003188 00000 n
0000003394 00000 n
0000003600 00000 n
0000003687 00000 n
0000004005 00000 n
0000004080 00000 n
0000004185 00000 n
0000004297 00000 n
0000004409 00000 n
0000004579 00000 n
0000004672 00000 n
0000004784 00000 n
0000004887 00000 n
0000005007 00000 n
0000005139 00000 n
0000005267 00000 n
0000005363 00000 n
0000005459 00000 n
0000006417 00000 n
0000007188 00000 n
0000007416 00000 n
0000007644 00000 n
0000010715 00000 n
trailer
<<
/ID
[<1a9859941ac21a0ffe6521926edbbd6f><1a9859941ac21a0ffe6521926edbbd6f>]
% ReportLab generated PDF document -- digest (opensource)
/Info 25 0 R
/Root 24 0 R
/Size 45
>>
startxref
11778
%%EOF

272
INFRA.md Normal file
View File

@@ -0,0 +1,272 @@
# Android Cast — production infrastructure
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-26 |
| Last modification date | 2026-05-26 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Read this first** after a context reset, before changing nginx, deploy paths, or URLs.
| Doc | When |
|-----|------|
| [docs/README.md](README.md) | **Documentation index** — TOC for all guides |
| **This file** | Topology, hosts, ports, sshfs, agent rules |
| [examples/app_hub/DEPLOY.md](../examples/app_hub/DEPLOY.md) | Hub landing only (BE `listen 80`) |
| [examples/crash_reporter/backend/README.md](../examples/crash_reporter/backend/README.md) | PHP, MariaDB, tickets, ingest |
| [docs/GIT_FLOW.md](GIT_FLOW.md) | Branches, `next`, no direct commits to `master` |
| [docs/TICKETS_ROADMAP.md](TICKETS_ROADMAP.md) | Tickets, Gitea, alpha delegation |
---
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [1. Topology](#1-topology)
- [2. Port 8089 — not the androidcast vhost](#2-port-8089-not-the-androidcast-vhost)
- [3. Public URLs](#3-public-urls)
- [4. Disk layout (BE)](#4-disk-layout-be)
- [5. Nginx — source of truth in repo](#5-nginx-source-of-truth-in-repo)
- [BE apps.conf today (typical)](#be-appsconf-today-typical)
- [Hub location (correct)](#hub-location-correct)
- [Reload (BE only for hub deploy)](#reload-be-only-for-hub-deploy)
- [6. Local sshfs workspace (not in git)](#6-local-sshfs-workspace-not-in-git)
- [7. FE nginx — two server names](#7-fe-nginx-two-server-names)
- [8. Verify](#8-verify)
- [9. Backend app (crashes / tickets)](#9-backend-app-crashes-tickets)
- [10. Rules for agents](#10-rules-for-agents)
- [11. Changelog (infra doc)](#11-changelog-infra-doc)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## 1. Topology
```text
Internet / LAN
┌─────────────────────────────────────┐
│ FE — Gentoo HVM │
│ Public: https://apps.f0xx.org │
│ TLS termination, nginx reverse proxy│
└──────────────┬──────────────────────┘
│ proxy_pass http://artc0.intra.raptor.org:80
│ (default location /)
┌─────────────────────────────────────┐
│ BE — Alpine VM 10.7.16.128 │
│ nginx: /etc/nginx/conf.d/apps.conf│
│ PHP-FPM unix socket │
│ MariaDB: androidcast_crashes │
└─────────────────────────────────────┘
```
| Role | Host | Notes |
|------|------|--------|
| **FE** | Gentoo, `apps.f0xx.org` | Almost all app traffic → BE **:80** |
| **BE** | Alpine `artc0.intra.raptor.org` | App vhost lives in **`listen 80`** for FE traffic |
| **Jump SSH** | `f0xx.org:222` → Alpine `:22` | `~/.ssh/config` host **`alpine-be`** |
| **Git origin** | `git://f0xx.org/android_cast` | Integration branch **`next`** |
---
## 2. Port 8089 — not the androidcast vhost
**Do not** configure or document the project hub or crash console on **`:8089`** for this installation.
| Port | Service |
|------|---------|
| **80** | **Canonical** androidcast vhost on BE (hub + crashes when FE uses `location /` or explicit `:80` upstream) |
| **443** | BE TLS (used by some FE `proxy_pass` rules, e.g. `apps.f0xx.org``https://artc0:443/.../crashes/`) |
| **8089** | **Other** stack (e.g. **Janus** `/janus/`) — **not** androidcast nginx vhost |
Wrong FE pattern (remove if present on **`artc0.f0xx.org`**):
```nginx
proxy_pass https://artc0.intra.raptor.org:8089/app/androidcast_project/; # BAD for hub
```
Correct public path for hub:
`https://apps.f0xx.org/app/androidcast_project/` → FE `location /``http://artc0.intra.raptor.org:80` → BE `listen 80` locations.
---
## 3. Public URLs
| URL | Component |
|-----|-----------|
| `https://apps.f0xx.org/app/androidcast_project/` | Static hub (`examples/app_hub/index.html`) |
| `https://apps.f0xx.org/app/androidcast_project/crashes/` | PHP crash + tickets console |
| `https://apps.f0xx.org/app/androidcast_project/build/` | PHP APK builder console |
| `https://apps.f0xx.org/app/androidcast_project/crashes/api/upload.php` | Device crash upload (no session) |
Legacy hostname **`f0xx.org`** may still appear in app `settings.json` examples; production ingest/scripts prefer **`apps.f0xx.org`**.
---
## 4. Disk layout (BE)
Web root prefix:
`/var/www/localhost/htdocs/apps/app/androidcast_project/`
| Path on BE | Repo |
|------------|------|
| `.../androidcast_project/android_cast/` | Full git tree (crash backend, app sources) |
| `.../androidcast_project/index.html` | `examples/app_hub/index.html` (copy or symlink) |
| `.../androidcast_project/ota-artifacts/v0/` | OTA publish tree (`/v0/ota/` on public URL) |
| `.../android_cast/examples/crash_reporter/backend/public/` | PHP document root for crashes |
`config.php` **`base_path`** must be exactly:
`/app/androidcast_project/crashes`
---
## 5. Nginx — source of truth in repo
| File | Purpose |
|------|---------|
| **`examples/crash_reporter/backend/nginx.apps-port80.fragment`** | **Replace** BE `server { listen 80; ... }` — hub + crashes + build |
| `examples/crash_reporter/backend/nginx.apps-builder.frag` | Build-only blocks (also included on BE 443 via `apps_builder.frag`) |
| `examples/crash_reporter/backend/nginx.fe-apps.f0xx.org.snippet` | FE hub/crashes → `http://artc0:80` (preserve URI) |
| `examples/crash_reporter/backend/nginx.apps.conf.fragment` | Reference blocks (443-oriented comments) |
| `examples/crash_reporter/backend/nginx.vm.conf` | Full VM example |
### BE `apps.conf` today (typical)
- **`listen 80`** — often **empty** (only generic `\.php$`) → **public hub 502** until port-80 fragment is applied.
- **`listen 443 ssl`** — may already contain hub + crashes (direct HTTPS to VM).
FE **`apps.f0xx.org`** uses `proxy_pass http://artc0.intra.raptor.org:80` for `/` — so **hub must be on BE port 80**, not only 443.
### Hub location (correct)
Use **directory** `alias`, not `alias` to a single file:
```nginx
location /app/androidcast_project/ {
alias /var/www/localhost/htdocs/apps/app/androidcast_project/;
index index.html;
}
```
### Reload (BE only for hub deploy)
```bash
ssh alpine-be 'sudo nginx -t && sudo rc-service nginx reload'
```
---
## 6. Local sshfs workspace (not in git)
Edit live configs from the dev machine via mounts under repo **`tmp/`**:
| Mount | Remote |
|-------|--------|
| `tmp/FE_gentoo/etc/nginx/` | Gentoo FE nginx |
| `tmp/BE_alpine/etc/nginx/conf.d/apps.conf` | Alpine BE apps vhost |
| `tmp/BE_alpine/var/www/localhost/htdocs/apps/...` | BE web tree |
Treat `tmp/` as **local mirrors** — sync/reload on the real host after edits.
---
## 7. FE nginx — two server names
Mounted copy: `tmp/FE_gentoo/etc/nginx/nginx.conf`.
| `server_name` | `location /` | androidcast crashes | Hub `/app/androidcast_project/` |
|---------------|--------------|---------------------|--------------------------------|
| **`apps.f0xx.org`** | → `http://artc0:80` | Must **not**`https://artc0:443/...` (static download) | Hub/crashes blocks must use **`http://artc0:80`** — see `nginx.fe-apps.f0xx.org.snippet` |
| **`/v0/ota/`** on FE | → **`http://artc0:80`** | BE :80 serves static tree | In consolidated **`/etc/nginx/apps.conf`** — see `patches/fe-nginx-APPLY.md` |
| **`artc0.f0xx.org`** | → `http://artc0:80` | Legacy → **:8089** | Legacy → **:8089** — **remove** hub block; prefer apps.f0xx.org |
**Agent default:** fix **BE `listen 80`** first. Change FE only when user asks or to delete wrong **8089** hub overrides on `artc0.f0xx.org`.
---
## 8. Verify
```bash
# BE directly (simulates FE Host header)
curl -sS -o /dev/null -w "%{http_code}\n" -H "Host: apps.f0xx.org" \
"http://artc0.intra.raptor.org/app/androidcast_project/"
curl -sS -o /dev/null -w "%{http_code}\n" -H "Host: apps.f0xx.org" \
"http://artc0.intra.raptor.org/app/androidcast_project/crashes/"
# Public
curl -sS -o /dev/null -w "%{http_code}\n" \
"https://apps.f0xx.org/app/androidcast_project/"
```
Expect **200** (or **302** → crashes only if you intentionally redirect root).
---
## 9. Backend app (crashes / tickets)
- PHP 8.1+ FPM on Alpine; MariaDB DB **`androidcast_crashes`**.
- Migration **`003_tickets.sql`** as **MySQL root** if UI says table missing (app user cannot DDL).
- `Database::requireTicketsTable()` must open PDO before `SHOW TABLES` (fixed in repo).
- Ingest smoke tickets: `php scripts/ingest_alpha_smoke_tickets.php --url=https://apps.f0xx.org/app/androidcast_project/crashes`
---
## 10. Rules for agents
1. **KISS** — prefer **one** BE change (`listen 80` fragment) over FE + BE + 8089 guesses.
2. **Never** point androidcast hub/crashes docs at **:8089** for this install.
3. **Do not** use `alias` to `index.html` as a file path (breaks trailing slash / CSS).
4. **Do not** commit `tmp/` sshfs trees, `*.log`, or workspace files unless user asks.
5. **Do not** commit to `master`/`next` directly — use `feature/*` → merge to `next` ([GIT_FLOW](GIT_FLOW.md)).
6. User may be unwell / slow to deploy — give **short** copy-paste steps, not long runbooks.
---
## 11. Changelog (infra doc)
| Date | Note |
|------|------|
| 2026-06-04 | Edge probes: `examples/network/wan-snapshot.sh`, `monstro-netdiag.sh` (conntrack/iptables/CPU) |
| 2026-05-21 | Documented BE-only hub on port 80; 8089 = Janus only; FE `apps` vs `artc0` split |

388
INFRA.pdf Normal file
View File

@@ -0,0 +1,388 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 18 0 R /F4 19 0 R /F5 20 0 R /F6 21 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 762.0394 87.48504 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 751.0394 191.101 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 740.0394 98.58904 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 729.0394 111.925 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 718.0394 165.293 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 707.6019 153.06 717.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 697.6019 131.37 707.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 687.6019 165.9825 697.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 677.0394 172.845 688.0394 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 666.0394 161.725 677.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 655.0394 75.02904 666.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 644.0394 166.181 655.0394 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 633.0394 117.277 644.0394 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 622.0394 135.949 633.0394 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
19 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/BaseFont /ZapfDingbats /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
21 0 obj
<<
/BaseFont /Symbol /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
22 0 obj
<<
/Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
23 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R ] /Contents 48 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
24 0 obj
<<
/Contents 49 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
25 0 obj
<<
/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
26 0 obj
<<
/Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
27 0 obj
<<
/Contents 52 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
28 0 obj
<<
/Contents 53 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 46 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
29 0 obj
<<
/Outlines 31 0 R /PageMode /UseNone /Pages 46 0 R /Type /Catalog
>>
endobj
30 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False
>>
endobj
31 0 obj
<<
/Count 15 /First 32 0 R /Last 45 0 R /Type /Outlines
>>
endobj
32 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 33 0 R /Parent 31 0 R /Title (1. Topology)
>>
endobj
33 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 34 0 R /Parent 31 0 R /Prev 32 0 R /Title (2. Port 8089 \204 not the androidcast vhost)
>>
endobj
34 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 35 0 R /Parent 31 0 R /Prev 33 0 R /Title (3. Public URLs)
>>
endobj
35 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 36 0 R /Parent 31 0 R /Prev 34 0 R /Title (4. Disk layout \(BE\))
>>
endobj
36 0 obj
<<
/Count 3 /Dest [ 22 0 R /Fit ] /First 37 0 R /Last 39 0 R /Next 40 0 R /Parent 31 0 R
/Prev 35 0 R /Title (5. Nginx \204 source of truth in repo)
>>
endobj
37 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 38 0 R /Parent 36 0 R /Title (BE apps.conf today \(typical\))
>>
endobj
38 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 39 0 R /Parent 36 0 R /Prev 37 0 R /Title (Hub location \(correct\))
>>
endobj
39 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 36 0 R /Prev 38 0 R /Title (Reload \(BE only for hub deploy\))
>>
endobj
40 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 41 0 R /Parent 31 0 R /Prev 36 0 R /Title (6. Local sshfs workspace \(not in git\))
>>
endobj
41 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 42 0 R /Parent 31 0 R /Prev 40 0 R /Title (7. FE nginx \204 two server names)
>>
endobj
42 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 43 0 R /Parent 31 0 R /Prev 41 0 R /Title (8. Verify)
>>
endobj
43 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 44 0 R /Parent 31 0 R /Prev 42 0 R /Title (9. Backend app \(crashes / tickets\))
>>
endobj
44 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 45 0 R /Parent 31 0 R /Prev 43 0 R /Title (10. Rules for agents)
>>
endobj
45 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 31 0 R /Prev 44 0 R /Title (11. Changelog \(infra doc\))
>>
endobj
46 0 obj
<<
/Count 7 /Kids [ 22 0 R 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R ] /Type /Pages
>>
endobj
47 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 857
>>
stream
Gat=*9lldX&A@sBlnQc7MH6?g9[]^mSK+)cl)>K=a9`^d/r"KGmjm@)5&+Ts0VC_[Hi(2(Pnnr)#HR;K!OrNqjq.V3"=l`'"9_Ni&Bt-tn8kIg7!Xq&crq+*I:7,=Qr&i`aanD<;=+D3P5,/&J"9_;4IFM-,h<)#Pg0DAc2^a;d&5ZZ#8(4sVGWr(&_r/r!e)r&@?Y>$\Lc_kKfEB"[Qh79AT#4In.*8lbe"Qin[!Y(WE)Q#UKURf'gf\&X6)h<Fe`/koaGs`:N6!5&QWG3Cn89-p50q6^;?Fh2d'I'CR#"-UaF=7Q)sZ^j]N[0="2-!j+GWP*K3Y7lk>+I=jKX[%BQ]aVgI\hN#nHU.RF17[RS[9bl/p,qE1smKNF],jVKpdR(#[4#$r#pm/J'2SUA2;[-Hqs)lp<@MDi3V[E$q$UQ^gn&rE9[/n+_oY"R5RDPLLi%kNE\,7J&9i,)W0Abh.i>X4hq2A^:5@2a$-VECDpi/dD,L\o&<`*8_rkCm9YN<<)OA2i<,-EKt=-nP4!HUR=E",numVfL)4J8R6t%8ZE5:U2BT`QK\<Qu6Wf!lT3$BCi?bDDNJ<V='uM!r1iA/Bimkj'm5hhK82(6ueV'GR,[DBMj`BdHSShlKX&Sgh[3J>aXl0DqVEi9Nj]dgPjKBRD1H!\(!nEq?Zg+qnCC4a0^r:3pF%*he`ArIh-aPdbIF^G7BIFLpdP%Vjk+A[\fQ;E]$42Lc&_G0PD!E#3C%oIY6#gLi*d(K2<"DF:b04n.(&ZFEM0F#S_U">7PlPWRpuoW_-3gLdDX<X<h$mC'FREGA[1EIGUHq@$"V,o^'E;,a!mh<FB,Vgjneq!mLWE@g7OJ~>endstream
endobj
48 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 804
>>
stream
Gatn%c#T:-'SZ:1MR.E9:ltGaFB:EpZnn+DlI)iJ;*Yt5/5\u6lF3t4:>.$tcj[gNnR1>Il096o;$?lm_e64Z(-+!K'U0cE5U-mb^W&%$rLD:,a=&B,)@$j6k9L>>aM6nf-:,ek3!9Po,iC*WO'*5qR-i,t-[h/O!V3qU?tTqgHA-VsmTKp&U!UqRjm30A+3<@4g0adNRXPm^5sIKR;l%;>l(CcXEo#t\DiZ,bUMMDc7W>Df6Q2a<2N2W(mGM^cTh;)LZ4PUG^=Lkpi!Rr4\3eMa\8.Q8hq$ChQ@cfh$Em&l"BUT=TbU(GE7iANbK\!3!h)eO&/3i8)tiO(+BBRX0f[XMK<*BJ(tn-c7)Am:d0P]LO&3Lh=q->Y=GnInf3.)8i`U@PNkRL@fJS@ra&FJ/VWIVq\3m;)i>Sg\8YM-+Kc<ID:.qmYXVU#+'kl,=;/([:j-?ocol1ZYO/K9Z6?)>8d_a_LZ?X`.B05'([$g[IpAW`'j3!TS;_`9DeIq/.X5/D_Q0)Bk.']*`,+ca3<&?&?GA3L\`*Sp$Cb!to?m1Du<+V\shQ]Q&>$>1J2?(.mUD>/OP;Sn]@!u]O617m[*S9rs2I4r+D=:sdeCNeqlO2.>ap=6C(f^ik3!adpH]H\_"<g@_"$C6:cn(l8IX_Cp)"J79'_20RL]':#_sd-kB!qV'p>pRDp01;,s0Rd>S[-fOm.P1JqaNb?@sp-J%ee7lI@kBSfto/09qdGJ)bXM]POOB+T&BGC`;*BKN$RS=d9lNca9bT>.^5;YPbu>lk0+d's.b5"D\40DO8~>endstream
endobj
49 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9$AT&MD_`PtL;ZBnUJQ&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:ps&&=X(PCH'Wj;^H^&o:u0B4;^:%sp:ZE&Wa6R#'r_sXjY5-C*Ut>641HTb(JK2s*r~>endstream
endobj
50 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`Sh(bM!;,fa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#A2MA-a(rHVR:D)FhU3a#MVn+@6p(6,SFQ!,JH2MH_7OtUjbJt$1U9TU8;~>endstream
endobj
51 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3259
>>
stream
Gb!l!=`<%c&q9SY-t'<1B#\PjS%%S&Nh&-IBB%Rt%2LX*cr<YI6BN7No's$#IB.XMUj1%O@"G^u,155JlFOR@Wbr/!rmLFGB*3*2#O*64b$_pdWY-'/=)]lYqWit"1KBFuVC&?O=?9lrC>aG/?WnhE)DUdhIiT\D=u'SUKD>_M/3^8!,m.(bPgeN'"g].c0-FE00eD-/:4@ci[rc3GZl.N>KL]F&'DMl`:*AON:QFCb08#(5ZaTgtl`WSOqg8e[*dYX1KQaY]O!0>;+#d7U8SQJ"(#jY@8YDP@<i?56V,d^AZH3eT!MW]-Cm[HrNEIaMeK4P]RO'5g#bt(K2cRU._=CYQ.=Uk;peo9LP*/b;q3f!nCP-^dFoYge)Pi99e*5l)5.[_G0ecN*@XHt.J15-r/mP+/35TN)!g\(\FriW6VIU#Y\uZu?p?S1E%D()VKemmFTc56/d42asi5F'g`Td2r$aNnD$`(8OPoslX+_B.j$MCd^E'LT@2t4ZV(]:fP.jGG&43$dEkI5tM"LdL0C?mKsgpn=,1j,g)k?1W.Hs"Bc`+rPH#@ZNoX-[<u#B>@3*X7c%Lu\.@5O9--&(/toPmN(h,$Y%_DaG^!fjT-?hEFKn(Y;`\s#%r\"h.5od\\0@[h%EMr^,sjJa^:fh<aJ0Ea,QS/Nl$f\;u*rYjpO&4d%Vm@*+t\Nn=";(CK>foO'c<\\FNkdF4Lj?X=hl-TcA9o<Vpt>\#:BNI'Q/b%MS)p\$tKoDOI?rO],V?Um[dn/-)pZcMj22+IZcVXuU\26;$2IK0a7i_G(cE3jPJ(m:Qfil4G0M#-IdO<3O+hYqLL^V.DXN4(m_*bX9MO^)#[]pis[6e;^t2J-ujYL3l]L#ki&s4\,?RA"@0R\)P-Nt?.j(!1d.92kWH_/24U=8so)r[dO#<G&V%_+\2R^o(WAZ[$*^T8EZniKXFk.L%S&<sJ/n(r3&l!<-%.@+).ci);PLR$F(+/@p5M(]C1=[h?XD(*fj-%J"U>&7TD#"J$C@d76JGQD2`t$'`Msfu(Z\$.BWqB7`TPB(`A^&<U.reic=.&FWab/9IFb9Og4!<0T`#HR;S+1sP<WJ2So)2"hYV70G>GA:F&Rh-)4X+u=CsC<8t'GC9C=*=59MZ'0hoY3j<uLgZ*Ue$:CAKo3',!S&ob59N4PI%Gik=/UN`gh2.%q/3%Z?gu65XSV,ZGET+Qp8O,M),IGY-8+uI7>@\r,N8t1\GYM;B_e'pK(832[h#(rSA70W_Y?M3^a(7"q>$E]kR]tEp\a:ld-)G#:Nu*aX:3!=i/pMaLG0(,?XD"TF`s:g_@H?Y.`-UIa)S+eU#.8<AE*GWA<Y(EP9:Kq50;d8lT*hH(49h7]`c!`aUl+lK6$".rsqT@=WSle?Kh5*0M&-9$po)'&J"g0-qPHN"=0Qu;Yj[>!e3cW=h':V5;#2SYM[")rWHH_I>0]m@-=G)%QS&`!A#Fh^g/pU'CSj"cf>$@n(2h]R[mW'';ep]ge4tBb=6'M*$9"Mr4fX?VUt)l5u3nD24_gCC`Pe81?bdbA;a9L)@+Xf1J%3hqsdCM;0@AJhn+eTm7a_P1WZe%#1c]IYkFsc4nU^HQaH46]Dtm`Ep5X(1A%U)G>-KO-#*'sfJOjn7Em;BnBd1_:$6.6FC%e7,(#c/ROW7#lGL1tJ3PO];=ps*F!#B<G5\ZTS[;IRBJVU!CYKmf?%:XI6Fa_B*7jmm]0gF->^5fGA#q--,aW:*h+:ALf1sdrrGRP2nN)@1m]*hae:(+.8ltcLTmIk%f,UdCjh7-K&@mU!7`<.LibFbRb"WGo(?Ua"N,UF8_?eJ[lM.);>X]O)I7QG-SaEUJ]02]ZQelUU3Np-No2Bup<)YP^MC"JtjUr81ca5?[bD1JTZaHQ+M\o.UDL/5e_bF(qDL*_.>#(I;O^J<pK.+S'bd=Yg^^h&Kf"?l3ZQ4%qA"a_p;;0VVI+!6\#hI6&`[np7#dJ>m?0!@\l2%XXq^1%"lg9<A94>/o*[=^;?uOd`&J@"$M_."RgiWKS_0h``s6tT8,$3kG5duJ1N,FWt'XZCMFK;g_<29>*TP@3W&;;cs3)>u:pQe=1Y1)1<-U::P.`WU;<6CoASc*+3f6-l_f(K4'F_n7LZhD71pol0iB>VK7K;'oE=sNf-R'DWp#eL.5b,.Rj5'f8>%R&E`pJU%n5*jlVE>!8<dsjtI\Lp]YjhMP!RTB0DCbF.JfA-kCZ@I]H/EA`pBW30>Y$]Zm3'E0^##830)"3g[A]c)"NuD!@L7j(*-`@l=&0g4:DHa7$%+AKDh[.>n'TG?/rf"I0eC6!dX3PoG9NA>d7Ink"[sNjCW(Hh4-LL+Db>f"'ebmf-<a#=+/!l790h)V&)"KX+;pK1!0blI2%]VM.f\r29J5WblNgau`STofj-DArU5,D\b61fQA:n"<,2#fE?r07Q4:n2)b55Iae2o-^DGDS>Na!/:eVsg\ggl"KTGDDAjE5E$a>kR=@P`EYG7c-5oCHS)Pk[I07(1.PWUu)4P#%sjs.1X12^*Z7K#R4/EQVP`fL_TFeSNB.];m*gN<jT8$Pb)B?l&eD\9<21I)"lmt*UC9PQ+BcX'm$L4,Fr"RK"/*WXc$-4M!ID[R@tpH*_sF\WujHmGTFgZpJ/pVA!Oq)gqBc@'rr=SFe9U/9$7MSM[tO&k@<aF^d6UIR9JQ+!5$L)m+[ND#@Ma;<j0nN>5QtTF;aD@4,DFHaY3^;5)]Q?PC4Bd_SF2tr*Z`mW][5f8e0K=7MR?3gF'Ji3hMTOUNa%6..5n`9CMe@Fg>^^!PLHgl+P9-`S.BbP(V*O0QVoA7b(M`]4-&fr`aA@XJji[>ACf;oS9R7/OmfCC)qZlSmMEt.0goROnE@Vfa_;%)g[(K+Q5@$;4pXtQ[-kFoq'o$@te@N#Co6ZN/=L`n&GNNZNbnRAK2gGkHtE_$COZ)596`0>*"iO?DO/GTbFD(7Z!g^pB<"CAu:T'a6hjX/&&<[QaE%GgRGmbIKpL@NK(o$iat\^glB;RME&Oh]H6YiEW&m2^;YZ\Uf$KF^Pa/j@dE+LUr?nC.<Zmo`%;'AKdB4;W)>c'3s+_Efd&%I](*2%MfIj?X'[?-F#X(.T)B:<POtkocYgM0Cds8hj#@XuJ)q1P"[S;dbC(F_i\amSYaq>j.uK9(46b.QUo1+<8`36;G`K.+d[R8R0&tD8qD>%'%aE<?G"BTj+N_GeAU0_R&8EC/A/`k"MG9Eudm*u0`WqP'""bArS,~>endstream
endobj
52 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3454
>>
stream
Gb!#^D/\/u')q<+0aOqad]6DrIrRXu#J9CI\T8e#^]=KFRTrEX.$@k6QA$I]rFVDUqlAc\G;U;.g?$WHZa2I6bX_pEIq8)Q]*3G'%uY3@A-MC&+DXAtqtG&PJ+;-EI%4nQ6rIS:9LZudC^jah;'K/#WiJBagQ5):LoDo5-3IIZK_V"8JYq0iOVIu\Z3lQ-l=X@bru@pREn;!LQIDN&qtQ=dNNT>IB<8-^Q'_!6Z*$>`!E4NEZli8_:bu3!nO`G/>X49eCs)uurr2988N.37nF#R!gjaqQTD=Ybj%m4eU+4*CN,r%7UN+dc\,%jEo)?ntdKFPn<?*/miIi81I*ZquoH@ho-gIT)B+OpX`V;VE$'qWDYBN84CHmN;;$tL7bFE)FO,j+*L>FTA`%`ObNk$uL^@%8iJnfb8*W)O#T?/G9#34I_V7)'a)26"6/""^sB,_(4=l@:@2S7#,\,r3=a!-BO&2?]cgV==a%g&D;h[4l^Flj0,rKZE?al5m\E9t.)?6&#Q%fa7m$h<X>nF<`D-\'qO;9'Q@N2eFtYi(KS0=pYnh>:VOL(KL/@7W/309/\mjTECna\p7s8,HP19l^\T^,l?FP^DC!\\5NW0628@)Q(<E."rDiKM[IO*<Q7ts*qU&Q==U?%*d[R^KqHlp:67AQ<k5s!r+/JV!@,@m:3,c=..IL$jMO1!']KH(^);jpll;^l`U`T;a,^Q]eY+6fmogL)6@(sio-iQmu9-MFCJ@`KI<oN`&)%FH+PHF$;lfsRT>F3i>K`Ms4qt>h'cP(CD(RF_K;p(F;l"pkTodD`[K9*\(r>Z4AnH1F49&dfkXTa_NJ0jW6_p%9_1T"^4dI!7@g$Cho6So%GN)8g5i.?)#<r6?.VnM1+X11>Qn%Rn?95A5/U8qb[VoKBSAW[LXF]aJ[ONKS,)9:<QZC,5-d;rHt5h:@N-O+`O#k!$D4&^B<5=t9ASpn2MUo^b)U#sA--i>#BYPZ4FQ2C*K-\g\b:,cA.<kCcF>PD#"!4%"KtZXk9H9\<m3#P_O>i>E@>X;,`!pE:P0WAE*V+UORK?@SgL/tM)+i-(iN=kJgJ)2N-4Wt`B!]F6WsP@cN=UTosjSqD]m?jp@e\h8>ck./HBqE',\3$.OBA\"jNer35B#8\XsPr,YjFhk[iOWk><lWqk.XQ4S__cA$5/cWU*L&'&+5Lf,$]^U3!IS=\6rc,UssBP9fe\QqjkYbr?H[lRQj?H2,I3190%](#>ejP^VCVFd&]mj^Ndge<MX<WQgQ87YPt0)uOOa@LN@rcUnKSqVp#6"iQTWr?U*'6g0Q5.&.r?_88gl,3KR0;'6dV)f<.#),?M&WGu"F0F`o;XT2hI[a.PZs82je4^LO'H%`A_8cC]+pm)T*R;[3kc4I!C;Fu>"##sim>mt-1Y70@<-nE;.?591EZJo-&?<XFG"u-NjmA4e`o&b<d_]MuN>&<;ob2seqghSDi'c6'.H1B;kd`?J>B\DVnp0B$_YiSbCaX2%pE_$78V#<il9C8%NBf>W7Jn:.<M^TIFpgBeA2FP#oH>$$FM4h/O+_@%L2P_mj4B;/\j/(l'dVW/F[U/QqoqA6@a#od1F'V\4C@/mf%tNQt\Kugr>s-$s'4,beI-Qi6co!*JSJL`nd!:^IP+WKa/XhD-0^<lfoQF[.?lDl)/BaIE1X'7bk5PAR,?bblNofRUqm!5?,O.ht>2-UU[d[ej?+9<!b1N)rJ=5$\!HZ+'JSO6jI[DmG0IKU?`n.e<)^R-E1eT6fn0+Q@fEhd9cT*eA_l]YMpqK'2V'(^7cY!TTL=QUm\APgBXY@UPWK0+1@Mn,q3lMhI]%#o@`_Z,cKJrABN%&5<S)$3p^itr:bU-$E<pDs0L[+r(1JFAeV#?`e?G@S")+>Ls3[q@b>DG4ed8_jc!(M^R3.k@bF`0DC^(fW4n3=/c=?JT[Z/>!dr;C":-0\m&HBB^MS4W0QDe_q%.O^OhcuSbo%>@Oc%&->!d2TJm>:CnhCkR!hL`?rM%kiVjI"'?8IIT#cM@NRd;75IOrmX;uHd4$22@+ZrLNJQ#oqu?`73JNFbZ"<,7h?arGKqYBNta;f@q$*9N?P9i(dV0PmRP\^Lgo+2rrc%H-Ce3%0aPGT#)$<4ZV"UIZ-Q"#aXoj*0+b_i[6NpHX2oM/DB6_Ms-;cFQm?0&O]CoaN/Q(!dLF.tJ&9:eD@h/WrbWkgTF"(bbkFL?Wd2_8D;96KrDu%`<%4\Id=.ljV(8=+.b3sU1=f)S`[bFM/1>42b2B\5cNfL)KP^>BL%"4`9$].<.FtK/'K;!:Ws@^<BbW.4l=S_uPWm8)=gbp,=!7^.RKsdDkJr#JG^&hBq4l(r6n[gXlL]P8$91ItD-q48:ek$#(jP%[k?I)\ot:fi3(!J@d.nP4DJ`/k7<@fafreZISES,&>dp*PSa'7A;/>Et>,JY$<,L;*k@Lbc8\I]r@NqFA1BZp6@-F"o4XV<+A!YMOD]OiYS>8^pKrr/AB.RLDY*Taj6g-/.fZ\%g%l,Oc!,c3&@!]qF@Q[70&8fB_Lhn%/Tj(Bj@`!tafd):KaRi7%&KE=DTsL<<+&ZYIX'>Gq)$n89$=!&ZZKj'QHtcWBWY?7ikki3?6a/,p'kPFs78c!DFuNd0C-tRXVo<%Lm<Yb?mRt=c@\<#WZVuA3VqqfV/=XRE+O=0(bWmLs9;\dsMt<2bA>"9u"@k81ZeHC#?"lZLA,<blR()5D<`8J%4QpV<@8:O,e_k:G,6lK?m*)(,lMf\F`'=/GUJ>/K%<Wp[nENL%TJO-G<]&P;-Zgh7_mr/\=gl[qm2OeOMZ%4W>4@/Jh]bY>eD5MY&mo/27Ji0:K;$_ADO>'eE)Z#C*.%pblE`"1WRDe>,(NPE4Nrt"MX8%0Ii<5eU"em,h1iXC2=OV>li&R#'o"n%n:,$5g,Yr@=3dO]!h#&@;94JXFao0^d%GgqH((6kJaYT9Tf]nn)3C?,rg"k<hi=8W79a&j%>ct)Df9ROs+bEGHI7qj?<-`WU;/hG/`m]Fn#YV]`FeT'g2RX9ok/=nMOI]A"QLDYU>K%]VR\Q21:iI:>$n&WP];'tguTK2)/m7+=<Kc#'e($n2PDk1'j45sBgl&<Z>/2jX6`^K[6.,aa1c@;b?B&m\>\D\'\1ZU/B2sX:fmfA0-9]Sh@*2Gd"iB?m`fTbK@i.IWSF[6Z\u:TFT@K*S6_4_[[J]FF#1"8pG%$lKI`Z@.=ltbS8_2J&g]$Kq[H<?R#?-P?jMc0YP``(TIGRXMo'U8@a0k6_DhR5Q8.8a7j!Ge%R]!O:!%rY9JqEWU\-"!QuEfS:/!&f!e4,1Y7=L"\ttXXf$<(l$XfQRKR!\.O$5-^rgeZ:*a\g7Kfe#o2tH1ajYD2eo,DpsK3@t\rMmVajg%`O-`2W\_1:\:P/.OInLbe1Hpq%@22Q-X:<?]q)3N^kb^]ka2(<M7?g3F"P5~>endstream
endobj
53 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1947
>>
stream
Gatm<>Ar7W%"?O++:8Ou\ja'hH\XbcCV+bfh7=oM?31h@UEj/_bE\T(E,bgRTkl)tm:8q4%[p%-;\]%ir(+d.'E>0)1J.ZX7pqih?r7b8_/b>6UN8C9oko+W/a9d`$XD-]hm&#PLCUKQ]t0GU.V-\ZJkn"eA3I75Qa7`JZrWYU+`UqTiZ8SSqVN_)HA=]"hRk9-"2eNPLeJFoKk*TW]F^533AXd/)_@IL&PjUJMQ:!>?T]fs^"4t7"f$Lmms_X*#n5;p"p0_i^GE/aJiE++n($<!s&C<kr1EL=gOm<km%"J`*!!!>A84:jk`,:D4C5&Ch5X"$2<S^ulafP6D+2q%k97[OI9RdRPPDlRlc?1EYY@H"J_R(7Uf@CH;h(F+aV*ek0&mE>j+eVVJC4_`KN=02e26JQon#Adqa.a1Cm2<Hl>*T7?>eO?;qH@G\p=K:r#i<+&`P6%M?RDSGZMa,@gV"VHkK(hVA,aA^Eu`!>E9*`M.:JDbR<7go+kl&V%KK0_Ma`39u''sLFg$qfgM_^_Ql#UJ0N#1Kc7^ThEESH8[rA^/EQ&)2R[uT/l+,D+P%Jk\\(2ki'W[J)1Ml-9ZHG_&F`[%NbFZ^BkC^hF=VZA!:0IC>2ZhJr$%_(VF0+HcSY$i)FC09PBLOmeP<l;a5ON!1+1e]Qpj6b5d<f_"hRMhUp3g;2T'LuTIgkE&@!c""WSPRDj=1k)udm6[G3oSBGshIi'?kf"m5QXnpR;C*r"tOC,hJ2;un3+)-e52KkjEA]WH:#qXl4p4rg.9&%`!-N?@ou9+1&7(Y^re#Fh6lltq,6b@V`r<iNUfbPF23)kG;r:m$_.65adRi//iVS6_E'<]P_?/".IeOIb:DN+nbAs,>PNYtXG!Ae3?n$+u?`&2<mn8<>JYI)'@8Ea)Z,"sNNfDtn_N=eo=_J"7%\ks;9Z3=Z8+6h;M^D<\rJLuo+UKk](pAeHWTD?r9DA1H7`7=VRFMEBVTj;?`e7;l!\#[t1"8o_7-<k<ai$3',8TH,l'E3uKZCbDpq5pM@s^:7LH&55KfJ9BT.JTdjL2TE:p`sN2M("gg?ER5):&%CA7n45gqj[^Z5:5;ALf'Wi@LpgEU:&"gY([3u@Ah]4,E-#((0fQ6]B/`AuP/W:??^2aafIB)qLKb@PlNXtVg=[*q8>Ks)[(S$=gh9>124nW%3*1%Qo(s[EZds`;JAJu:nj_^li[$>e()_S$WX1D&ZW5",rpC*O^'oa6]MhN(:5'N:To;3X1fdNo%WE`)X@]oNB>m2XA,h#q,?$4WNk3fF1MkkHF>g]k3WJqs].C>fh6YHQn3_slAWPKN1&CVacOgAIK"0Fp:3`@d)PUA^a4nF.F!9/R.XrH!3\GsO*4&Mg15WdZPDed2M!`X5FpRd=c""%pdZqU@)]cAXO(F6bZ9Z4c%/F6Dk9%]RUcm28>]:[`Yq7[4C:cA847l32!91UIF=_iFc#B*:UV.hE(A2'c;[Eq&dsY2ps(W.dBo&G(PmAhh3RIc.(STWQ74Z(5fpc#&B$no?gBP^!b[5k.VU`Mo<Y1QGN"[AlL^ga@;Ll67hCkUu\%,V=eRgaVH].9mk'#7MhS@'ro=Ql/R%_B.4o)2nlNGBNB=_m8SL""5964WF,NFKT?/qMf"-p,2Ml7b5^+,mMr9_qJ#1j1JdS^KM_'EX!mM^N5':Ji6+hBCIAC]/?1-T%Dpd8BBg81?:F9g=L!lYt.GU0#;T#L1]E-OJn:Yn(gk]:9pCkDiC]4ra@&"d"5W6XPooYf=P(i]%`"*CHPYa3&FZisI$`5NY`53)NANs*sV>RT9Ch`pdqC[[UtTDkuk]LlR64trK#bpU!Fhf90_okKRJ)@u=Pq#L5`nGobm#F'sC7'sh]7BQ(6-5q'oE65o6G:^r3T`+i$;Tf8pMX+/W`4YG7!>6Z#M/-6[fpMV"[Mo/PaO'f#PjeU8~>endstream
endobj
xref
0 54
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000513 00000 n
0000000660 00000 n
0000000808 00000 n
0000000955 00000 n
0000001102 00000 n
0000001248 00000 n
0000001395 00000 n
0000001544 00000 n
0000001692 00000 n
0000001840 00000 n
0000001989 00000 n
0000002137 00000 n
0000002285 00000 n
0000002433 00000 n
0000002549 00000 n
0000002655 00000 n
0000002739 00000 n
0000002817 00000 n
0000003023 00000 n
0000003336 00000 n
0000003542 00000 n
0000003748 00000 n
0000003954 00000 n
0000004160 00000 n
0000004366 00000 n
0000004453 00000 n
0000004771 00000 n
0000004846 00000 n
0000004939 00000 n
0000005077 00000 n
0000005186 00000 n
0000005302 00000 n
0000005473 00000 n
0000005585 00000 n
0000005704 00000 n
0000005819 00000 n
0000005953 00000 n
0000006081 00000 n
0000006185 00000 n
0000006316 00000 n
0000006431 00000 n
0000006540 00000 n
0000006643 00000 n
0000007591 00000 n
0000008486 00000 n
0000008716 00000 n
0000008944 00000 n
0000012295 00000 n
0000015841 00000 n
trailer
<<
/ID
[<6a21447e613b14d0d348f555435c2451><6a21447e613b14d0d348f555435c2451>]
% ReportLab generated PDF document -- digest (opensource)
/Info 30 0 R
/Root 29 0 R
/Size 54
>>
startxref
17880
%%EOF

162
OPEN_API.md Normal file
View File

@@ -0,0 +1,162 @@
# Crash console — open ingest API (design)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-23 |
| Last modification date | 2026-05-23 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
Triage **real device crashes** vs **synthetic / bulk test traffic** when exposing upload or query APIs beyond the Android app. Builds on RBAC ([`examples/crash_reporter/backend/README.md`](../examples/crash_reporter/backend/README.md)).
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Problems](#problems)
- [Payload extension (ingest)](#payload-extension-ingest)
- [Upload API (POST …/api/upload.php)](#upload-api-post-apiuploadphp)
- [Console / query API (read)](#console-query-api-read)
- [Classifying synthetic traffic (heuristics)](#classifying-synthetic-traffic-heuristics)
- [Security](#security)
- [Android](#android)
- [Triage tags (implemented v1)](#triage-tags-implemented-v1)
- [Implementation order](#implementation-order)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Problems
- Load tests and CI may POST many similar JSON blobs → drown signal in the reports list.
- Third-party integrations need stable auth and filters without console login cookies.
- Similarity grouping already uses payload fingerprints; need **provenance** to exclude known harness traffic.
## Payload extension (ingest)
Add optional top-level fields on upload (backward compatible):
| Field | Type | Meaning |
|-------|------|---------|
| `ingest.source` | string | `device` (default), `open_api`, `ci`, `manual` |
| `ingest.client_id` | string | Stable id per API key or CI job |
| `ingest.run_id` | string | Correlates a burst of reports from one test run |
Android app omits these (defaults to `device`). Harness sets `open_api` + `run_id`.
Store in DB:
- Column `reports.ingest_source` (indexed) **or** JSON path in `payload_json` with generated column later.
- Prefer column for list filters and MariaDB permissions story.
## Upload API (`POST …/api/upload.php`)
Clients may send **`Content-Encoding: gzip`** with a gzip-compressed JSON body (same schema). The BE nginx upload `location` should use `gunzip on` + `gunzip_types application/json`; PHP accepts plain or gzip.
Phases:
1. **Phase A (now):** Accept extra JSON fields; persist inside `payload_json`; console search includes `ingest.source` in blob.
2. **Phase B:** API key header `X-Crash-Api-Key` → maps to `company_id` (RBAC); rate limit per key.
3. **Phase C:** Reject or quarantine when `ingest.source=open_api` and fingerprint matches org blocklist.
## Console / query API (read)
Thin JSON endpoints (session or API key):
| Endpoint | Purpose |
|----------|---------|
| `GET /api/reports?ingest_source=device` | Default triage view |
| `GET /api/reports?ingest_source_ne=open_api` | Hide harness |
| `GET /api/reports?group=fingerprint&min_count=N` | Bulk duplicate detector |
| `GET /api/reports/{id}` | Detail (existing shape) |
UI: filter chips on reports toolbar — **Devices only** / **Include API traffic** / tag `synthetic`.
## Classifying synthetic traffic (heuristics)
Mark report or tag `synthetic` when any:
- `ingest.source` in (`open_api`, `ci`) **and** user has not promoted it.
- Same `ingest.run_id` + fingerprint count &gt; threshold in 10 minutes.
- Identical `report_id` prefix pattern from known test fixture.
Manual: console **Clear synthetic tag** / **Promote to real** on detail page.
## Security
- API keys scoped to `company_id`; no cross-tenant list.
- Separate write-only keys for CI upload vs read keys for dashboards.
- Log upload failures to existing crash event log; no stack traces in response body.
## Android
No change required for v1. Optional later: dev setting to set `ingest.source=manual` for dogfood builds.
## Triage tags (implemented v1)
Workflow labels (multi-tag per report, any combination):
| id | Meaning |
|----|---------|
| `in-progress` | Someone is working on it |
| `fixed` | Fix landed |
| `rejected` | Won't process |
| `done` | Closed / EOL for this issue |
| `not-reproducible` | Cannot reproduce |
| `not-a-bug` | Expected behaviour |
| `duplicate` | Duplicate of another report |
**API**
| Endpoint | Purpose |
|----------|---------|
| `GET /api/tag_catalog.php` | Workflow presets + editor suggestions |
| `GET /api/report_tags.php?id=N` | Tags on one report |
| `POST /api/report_tags.php` | Body `{"id":N,"tags":[...]}` — replace custom tags |
| `GET /api/reports.php?tag=fixed&tag=duplicate&tag_mode=and` | List filter (AND default, OR optional) |
Smoke test: `examples/crash_reporter/backend/scripts/test_tags_api.sh`
## Implementation order
1. ~~Workflow tags + list filter~~ (done)
2. Document fields; parse and store in payload (Phase A ingest).
3. List filter `ingest_source` in `ReportRepository` + UI chip.
4. API keys table + `X-Crash-Api-Key` on upload.
5. Grouped bulk endpoint for duplicate review.
See also [CRASH_REPORTER.md](CRASH_REPORTER.md) and `examples/crash_reporter/ROADMAP.md`.

301
OPEN_API.pdf Normal file
View File

@@ -0,0 +1,301 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 13 0 R /F4 14 0 R /F5 15 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 762.0394 79.46904 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 751.0394 139.949 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 740.0394 180.413 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 729.0394 140.389 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 718.0394 181.269 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 707.0394 74.58104 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 696.0394 73.25304 707.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 685.0394 149.717 696.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 674.0394 121.269 685.0394 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R ] /Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
18 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/Outlines 24 0 R /PageMode /UseNone /Pages 34 0 R /Type /Catalog
>>
endobj
23 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False
>>
endobj
24 0 obj
<<
/Count 9 /First 25 0 R /Last 33 0 R /Type /Outlines
>>
endobj
25 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 24 0 R /Title (Problems)
>>
endobj
26 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 27 0 R /Parent 24 0 R /Prev 25 0 R /Title (Payload extension \(ingest\))
>>
endobj
27 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 28 0 R /Parent 24 0 R /Prev 26 0 R /Title (Upload API \(POST \203/api/upload.php\))
>>
endobj
28 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 29 0 R /Parent 24 0 R /Prev 27 0 R /Title (Console / query API \(read\))
>>
endobj
29 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 30 0 R /Parent 24 0 R /Prev 28 0 R /Title (Classifying synthetic traffic \(heuristics\))
>>
endobj
30 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 31 0 R /Parent 24 0 R /Prev 29 0 R /Title (Security)
>>
endobj
31 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 32 0 R /Parent 24 0 R /Prev 30 0 R /Title (Android)
>>
endobj
32 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 33 0 R /Parent 24 0 R /Prev 31 0 R /Title (Triage tags \(implemented v1\))
>>
endobj
33 0 obj
<<
/Dest [ 16 0 R /Fit ] /Parent 24 0 R /Prev 32 0 R /Title (Implementation order)
>>
endobj
34 0 obj
<<
/Count 6 /Kids [ 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R ] /Type /Pages
>>
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 852
>>
stream
Gat=*gMY_1&:N^lk)i&@Za3a1Oet_#$FREHI>V!,Y^ds7;qn+jhfipHHjLW/X^L7]4_N'ac<_'\D-6_^6\$g2gtX+s")eJoL]D>tGM_QP#X;o.8X(I!!3J,Pg_CT;c%J-:9+HWeMJ<BPQHU'La_G?Hn_clh(KqU`;!'AIb8h_%6tsfGh-7\i2Y#6Rbi:+22[3S*dh$Fl"@9CMBa3%An*i)=Qo%o603;65]n1G;P9u.:r+Oh>h$T"?FA%Lo6>6+1h42R"Em-nT\Y8i7%*&$5*5\n7]:XSS/eTd/4WOU"-HskkJGKE1fR2D[.)lbjcGW14N(_@\&r'*5*(WU//?^X<hT@j;*h,M(636Jf7+(VMmNogJ4Kl>L=.JI8(F>fF\(EGES>%LckP\ipq0jc,1"<?W<pkd<Y2"G&"_MheP#*>7'\+MR^\uF&[$^1C]YHTqThZ,R=Eca\Aa;>?&^(@-;&2YU?2B\U^73sCSHj&)4>jX@bZg/5EHGO^"l.`R<=_jBD.<Fe8qhtk[?pTChVeq]ASOQnK^fQ.C2b)'^-0]XGS)KZq2bT2Hg2>r(nUMIiYU-^HL+Y2gZQ6M4ue^<o4F..HU$t;3-T$O_(P)q&.["4F4P+qLD'n)Cm0+/Qo+1A4?*LOj7t;a;$P_N[YO:er0`tV,?sQ),MYNMmr_#<VM2DGOB2DRbs#.chr2ikX#hd1gq[u>U3-QT%+EqKg6<7=q4"'q@MN&F0W:ISKlS+T\&.>56rsZE5c>#5)@01`GQYct\sfos":DhLXW2Q[-t@Zfoh&/]LdD?i>t_Oie-aoXle6H;qRAJ"`?;]<kh(NS8Knb;i[HlW\,k,.";q(B)BO1s~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 620
>>
stream
Gatn$6#YL#'S,ZJ(%7OX5QDb-GO)\<W\giGlJ3XYbU*X3!\TRXq=f5Z]P;4,c)N<?,Or"Qmf<qTT\*BHXS2]:.Tfq3?lZ1(YajcNI>4e;p+U!obZk]^#b$BDMU8HKnN@9o>4>3r5dF9c<<,mJQT9]&RGg*X1PSc*p;l.oKCb*WYOmmH!PaBC]4>0((Jt'`l<VI0MX;BjdPp[%':qM;B?5Dq13gb+a$B6:18mD)]PbO__M"]W%T[Z:2C><O`<_U-QqHFDgO!0XDpVm;HWS%@;mF]O>o`\PBt*IX-iTDFERsBLGL>H-=MVhjK]=0kN,D+A0KGQB:<.&"8)+u<8!gY2fc!c8lS\C:q:6O;NAq\DM,?!mfU]"(S<!W)atfRrHKgmVD)6cHA5b4`@.hN)d[SlhaM,Vc`>cZNnArVN\Q!r+Q((!WS[s.?TeTu/J<V<XP][&j-O<"q@0.>Sb"Be6AcKo7Lm)<QWct8p5^,!]+k5X;gb,eEI)j)IqZ\R>.u:(j]m&&`7sOU`*it"UVe^Oak/i8^Br_oc6ehI!0U>COH3`0S>9>&7Pq#Wk@nOV]rhTD'H6n]kDre))7&C5?E!NlqK!D%GB$R[@jZa`Pr<?%@M=U~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
39 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3382
>>
stream
Gatm>D,]IS')p1[kYG8gDr^WKa\Yo;^p#oebmQ=<lg=jTP[Z/"#KQC+2NYP^s*cQQ0ugI3-#>;'E7EH3GH:W*8$]qK+%uPB<ra%8)#\&tA]&U"Q><uar<-3NPFj4'-0]`15P@beJr+EuVAnH?-f6rg!j<C7H%J1`ojF'fdhPu^iQ;)\LgrH<WIFUD##cS,"bfmBrkN-XC`[Of&r>a]*)[&&&PFT'M5l1/4Gri*@\)XSR`?S1?reN`OAh9/cH1GTL[E]cQ8AdAS1tVR$Ri<6OU)eZT*==l35<!>0V`EL7Y#p(GoCZ<7mOB(J;YUH+os6Kmhb/RWoN=s0Xr'$A"Z0<)"a5tZ@n*t>/<A")<3a=UDX@[BGulE?_)]AHtBCRf%f[gND6<aODfP.[ga]=Xa[&?'>nlRGDC]e"K"39+lF2Ihqp;EAXJM>oa?"S_j^@a*<MhkppRk'^c";:nG<CEM;_7s*CI?N1%rU^^ArT[7\K.^QLnLRT(6/k4ZAr+L9k#L=$jf0VJ/$_+IaF@UZ"tTjXs]LGf"(I%.:2>J5=T\V+V1=ka%ReYTdkmq0G-A5LMEFGSMTZ_]Q?p^VDC/Z[.7KmI"f:J*h<"';1s^V:aBD`>oenb54iX'p-?Xf5OH64D?59W3oBe0oq&,iV=B>OjaI]H-B.e.A[W4S=nSe'_tY+@NHo,Y66u,q2uIQKIFr=+CPX_6-.\G9SLd1K1WsOSb(YLaje7I9-_A7mfd>m-A)[t0UtRI-j19HOgPNRF)7O4j1%-i<\CEfc8*ZdBhHg8[UWCa0QMk=&Q*dDV7DnFVc8IHXOC=P)g!`5_s+3:d-$pr!j)NJ"1]82hjnKjm+?ZT_sI,Wo*AiJ-9VC#1M>ApToV#r?.(2F=)n.U+CjaOi=T7CQ(dZ>^YGBol^aV3/d%M#2B/tYbd$P[%N*L02hNh%(l]lF-0LcWCS$O/RM`JEq_5rVa!o5^_(OhF-/qV06k%V:$gl:*-'[6;07LN+6m[&h9LG,ZQ.b\=CT%/)m8k<%Pj3a^3ptOjjnXWKR$/aUSusm'gTP*^%*Q&1,Pq@48W%E=lFQf*oWN]Yfjg!Rh<'OT-85QQGhBd,>#e`.=Npk7cF6m)Tr^u7gDs&39jLKjAP\XhX,\t^I_fk8d5`Wggt9A.$f%tFVY;?o>OQCMKmP+"GnhUh?PSe*is8;28PIV251]4-XYGhk`o0)<QK\k;Ak'=aU@:G"&t<@Qs1r;/1h@c.-si]Q>12[830-50j%IBP9aJTZ+:.[$!duu1J]aLh'rD(:<WKY@NZ%bGs*pOU@lnWm$eh61Im:c>o<aAGX-[d`:C5gV#.+-=q<Dbe7BRf]h$k]f7KYhk)Xs`RJH%M:)"AOSpf'.RSl=?7N`86m,<;W.C0NUJ%or51^4bkWA&Fe'r<V2Bag2Una.0i9WDQau>K(8;"It(XKH;;a:+Ke\U/bJV_8clf$(?dc!NQ9#Ir>]Q2T(H?#,Lh&TN9RYaQ>BnR)9N<I^nT3.5+%3HL"P@5#Aq'8[1msem<5:UPoTTNE"KUR"TeuIAYEJBcFB6hA0/B#U6LL>[2'O$4Gbk&1lY;cOn@<Ju06E5F!ot9ipb-Y%%j[hd_SX---OnEt#h6^iKq!ZM^\;%9[n&7\EO@>f\V4MM4k1MEjP3F_5<uN>Wc?_#GrOY"T7.S:`9rmqgGlTrX5H3,E-KXL;'\fg#eB]3#R99lV8WF/`'@:aN-7C",'>-,Z`8c%fA7a<L8bGHgj<S96jn;,'Caj>h*qFG@*<AQVjd%0Tg2&E/eTLl;=4/6=L-+N;4dGuG0ZL/(jLMTPR"!c,h%P#^4*2)"DLMA54RgFmD4O&GO47oV!L<(l4kZr)UBT>Adm)^&Q[1K7A?&DSJnB6(O$*p!0]7M%O5hV9Xg<hgS,`q@?":9Zt2=@PA;W,^c9%>TZ'VBTg>o3<_3es;hjFtS]b1V(V\gjh-1S.Rn,Pap9OJ=qRN1(^G=Gq26uj7gR3cnpnadBSW,L4!\/B^B24;\^oi;A*E.C9?UT_b3Q^%aVJ+peSJ=^O-/=Zm&XS/lqXf]&(`"BOXPohOU6h%,i$d0tf)*MmLu3UD"X9GYX;<0=TI+Sc59j?8B9KP;J=42C``lQZp+26PCad<DtuX[2`\?_#>.uJ&2Fb%a?lEA(/>,jmAfZ/fSXN.4Ca[mnBhCUWR\8gI1F4%/Cd)gm/Ar<96TKj2u(YW#3SK)\^ml`VA:rb+tN/fgE`#o/ElLm:L/-o_V\gVPNkqTgPt4KCnHbG&5&TrF>]<.<rj#S+=q(>-uAA=*<it=4jE=3#,j"a"+t%^/6$>>cQAjYNjCDAQ8eWH#3-je#-LEpq[rumY0JpXiRhiFb^W(+(]&daCG`H7\=&A>*^AQh_uaU)K,&Rnm`E4pE;'8r;Du\b<2;&\WDT!;,LdY$5(N;"]V>A?UV!&(HhP4*;r[NA;C%l:>uD//:GOV,#9/rP%:pUdd&\1(eu%J!^:<Q*jo63j3d(fQVmS%W"@^TPT#fcW#r#goPtt4m@ZN,rZb:\EX^"Vk#_;/P!?/-<TPHYb;`E*()t7CB.TG_Nhr3c[%YYJ(!WP!Z3B;YD;jdngo='&F7_VIS1NC`VER+,SG9>/<hE:`@[2Th2OL\5j4;9Fe\';1QaG]!Y=#"9?#8E%c$-C7YaBp=bTW+k!@fSX,(a3^EXDhj\$#@kRff^W/W-2+]'a%q2[ipFfb!PZ8&Xtd@gWg01.;#0XdGo&@NE[.j'SA>45fm6Z2rbglD4*WIW(s'Q#iTj6AtaMng12Cac]20d_]*!Va[b2QAfMMXbOQM.$c)$8\/'9Nn%[1cJrP/[pb@i9,bH*+]^uj[cI[j3jZ<O\*PAcT_#<=]3A$OR:$ohh!JXW2lb\uN[3pdGr;NPO+B,;'4HWo9OiM%`9pje^6!J1_a9s`CCCi7hs(8C]d,jr\8A!.lL-OaJIliK`%[P0#]ebe)jVR&FE8.?H:p&gMZ,!Q'HP?7<A%cfSmk^<gnTt^N*_qs,*k#SGbh[`/2(\8pS`@d@eJ9T3I25(%8gH7W3DMt\-;Me9'rm;;Q7`/p`NM"9ZaQ`i$(NYVi$ScRC:ob)"tRJF]b@m"toN2mJ@(CAk=[)imN8k0h8^:[X#4gpq""_Tm6?VnqN_uCJhZ0^#>Tq=d-j&De&MkbHo0XYm=t,DOo)O\$9,q0c*rl.u+sc,WcU]4*iEa0Wt#pd^m<T.K^`9L#l[#5Kjf7KIWM>6r%-Dojg1LGXIX(AShBH_?kL3AJV2$51[<X@Q!>VLG=')"//Ym]HRZ*q!CB/O2`K,_ht"V$=Zrae6d<!2(eX+ii'I#WSZ>C,"CKo<o?@Bo7];(:4t9%Q=q\dErs5tUOb/&Ga:3fIQ-aML\e4(![n!QGsS`$~>endstream
endobj
40 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1689
>>
stream
GatU3D0+\p&H9tYfZAo7g`=7ph]SI`LV%M>g,-n`3nKds_`1bK'4\S%\KoX<IX>/)0t+uXbmaeG>&!_6m<YB>"U.+Iq40,=5)u?6R0$qRScRAt#3/>nNa)OjBXZ@Q&OiNWR#:g>YMloX.R1b^jl)pt"\nG!7[X8tW^=L0p9$j\L19j5W'7O^AoH.qP$UL?5d/K,l;d]dkf't,o]#4l,t`3>\0O.O1HB4Dk+G`<.I#e,GL/H[\7:@&ZrB=umVb0n`t-*Ni*gj=is-F9P<-J:B<@N"N#7FL;`u_4^D0Bo4:Y*l:Ek%*]K$6.S7nrT'b%FJ%l<C:g\]CM;HuQgfm*eRo'@CN<RA.NGQ[r\VbnJU[<.`XIhgh$&R\/-]-<,H7[p-\0ol,Cf@!kir5fZs\"O2IlV!V%!29X/M\Cj8G&o.;?;pcnLcPRWPHj/IF77]LV3[+H0:%B*?tEr:"'?*FE5:boa7K8NCFLn[+$TQn:;T/$=Pk)1#J1J\EULcZlV+FZcCm0cI^[Z6@KmShdXh]Z![?8rAU_V5LDK,/;q!0pT;1undGZOR`<n.pAociG-h=i"kOUtl<m(#+5)h'NY=JF=#]R-`+DDPn3*HWMQjNm7XTV0b"TaWa!ZVpXpg"2f>7(EOV#n[5Bb[@!q&fe<V%POX-Upm50[P\P9TpgbFlNOuY7m3RfG[pUpa!?XH3q>8EWY8_\@L7\265<F@'j<l-p=Hm.M+H\?m)>lqH?Y"VRF6X32@YDp@ffs+oMZsp*2PqkY7KI[\G:e#k!2==K*-^[P&V=4%lb;[B].h3:O32=&ddiEV.7&cM8#o>/&IFXSDu,L#n1PnJ?W`qJG$VI)RFFk)(Ns?lnkeK_N9!-ULiAj8#4#OX%ME[&c0'IWFo*OuGZS6g<FQNdZ5]ho/b'9.l7PJ8F4U>IV#ak"O143P)c8h<[rIlH"pt[?7E5rE+W%p.&Q+5b^5.dJim]Z&_Y_4rd/37jicu6N6s;>E^LL_]<6ao\R!Q*2[]mM'^79qan*sNm[tiXbmTU]@,:`S4FQ)[!'#uI+pl]%lmi@gO`tA'K/1pI^V]JdC>1JFM\;qNZ0/=hR*SiB41>!'t_iu\/p4jA`d4bh=5]/17%>PB57=b2s'OVh.@-MW%gnTD61i_/!76J->U"Wf'8^*=c'3tgW?Z;KWPYucot=rm_#*!nSYIA$`YjF8usO,Q!s!DoB^WL*]ZVjSPH1<"+RbF^<=F^1pFj9,Y^NFFRQ?r,PUPB3"6il$G1>"nto/9:J&(KRUJ3ch))P`:>ZDu;Re7q(\(g&85&A>Q:rb>J^(ZJX)!=lbKk-G'1+3D#UT!:RMU24-rtM8SartfR.Ts(Zn]Ct89A$S]ce"Xi(QgSks!h`Xr[KE^(l;@HX@_r&7KqDGW>BU$Hkn<2[^"I".YVRF*C;m!7Y]@W#B938t]8o.LDB7R"T_#aKA%DMRLH/=[#Fj:B$!27:3&VhSUEO0O6Ye*NrH(n978FJ;ks>ns9p*O4QlY?,?AMkEC9b3Wn:(r5RVZ`D"7fN%q:6PpqLOW_VJ7&@!lAF0YTJ4R;-mbB6PiaBmaDr_s,]df5?]_D*e$:r'!Vi:`MZBR"S])@o>KBd`^[ZeUMB#A:d^DK&\=<8YP?VFIP!5kV\U/X)`-GdHP<i/k+1K_FoT^iZ;Lg.RrO)peb`A-Z<<"7Lfn7f~>endstream
endobj
xref
0 41
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000502 00000 n
0000000649 00000 n
0000000796 00000 n
0000000943 00000 n
0000001090 00000 n
0000001238 00000 n
0000001387 00000 n
0000001535 00000 n
0000001683 00000 n
0000001799 00000 n
0000001905 00000 n
0000001983 00000 n
0000002189 00000 n
0000002464 00000 n
0000002670 00000 n
0000002876 00000 n
0000003082 00000 n
0000003288 00000 n
0000003375 00000 n
0000003695 00000 n
0000003769 00000 n
0000003859 00000 n
0000003982 00000 n
0000004116 00000 n
0000004239 00000 n
0000004378 00000 n
0000004481 00000 n
0000004583 00000 n
0000004708 00000 n
0000004810 00000 n
0000004906 00000 n
0000005849 00000 n
0000006560 00000 n
0000006788 00000 n
0000007016 00000 n
0000010490 00000 n
trailer
<<
/ID
[<17f9a5b944b42ce125306ff3734bcaaa><17f9a5b944b42ce125306ff3734bcaaa>]
% ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R
/Root 22 0 R
/Size 41
>>
startxref
12271
%%EOF

199
OPEN_TASKS_GRAPH.md Normal file
View File

@@ -0,0 +1,199 @@
# Open tasks — dependency graph
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-08 |
| Last modification date | 2026-06-08 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | pre-requisite to |
<!-- doc-meta:end -->
\newpage
\newpage
---
_Last synced: 2026-06-04 (RSSH full stack on `feature/rssh-alpha`, RA control HTTP, cluster acl0 DNS). **DNS 2.x off graph** (postponed)._
**Standing rules:** [../bottomline_reminder.txt](../bottomline_reminder.txt) · **Documentation index:** [README.md](README.md) · **Service map:** [20260608_BE_SERVICES_and_infra.md](20260608_BE_SERVICES_and_infra.md)
---
---
---
---
## Table of contents
<!-- toc -->
- [Removed from graph (done)](#removed-from-graph-done)
- [In progress (feature/rssh-alpha)](#in-progress-featurerssh-alpha)
- [Postponed / off graph (DNS + mail)](#postponed-off-graph-dns-mail)
- [Priority tiers (open only)](#priority-tiers-open-only)
- [Dependency graph](#dependency-graph)
- [Task index (open)](#task-index-open)
- [Critical path](#critical-path)
- [Numbered priority legend (PO overrides AI)](#numbered-priority-legend-po-overrides-ai)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Removed from graph (done)
| ID | Note |
|----|------|
| **0.1** | Stream dump |
| **3.x** | Auth register + verify + TOTP + migration `008` |
| **4.wg** | WG lab — BE `wg0`, FE + router DNAT persist |
| **RBAC** (core) | Admin panel + lockout clear |
| **Browser** | JS syntax validated; `validate_be_services.sh` |
| **1.1 / 1.2** | Graphs drill-down, columns, full-width |
| **5.3** | RSSH API smoke — `test_rssh_api.sh` |
| **Infra doc** | `20260608_BE_SERVICES_and_infra.md` + PDF |
| **RA control** | `:ra_control` HTTP + WebSocket shell + adb JSON `ra_control` block |
---
## In progress (feature/rssh-alpha)
| ID | Owner | Blocker | Status |
|----|-------|---------|--------|
| **4.rssh** | Agent | BE `provision_users` + FE stream deploy on PO | **In progress** — mobile MINA :8022, bastion provisioner, linux-sim, docs/tests in repo |
| **cluster0** | Agent | cast0103 reachable | **Ongoing** — lab VMs; DNS `c1c3.acl0.f0xx.org` noted in `cluster.env` |
---
## Postponed / off graph (DNS + mail)
| ID | Owner | Note |
|----|-------|------|
| **2.12.4** | Developer | DNS / forwards — **postponed** (acl0 CNAMEs added for cluster lab only) |
| **2.5** | Agent | SMTP — frozen |
| **5.4** (mail) | Agent | Auth mail E2E — frozen |
---
## Priority tiers (open only)
| Tier | IDs | Alpha blocker |
|------|-----|---------------|
| **P4** | **5.1 OTA**, **5.2 URLs**, **5.5** RBAC UI soak | **Yes** |
| **P4b** | **4.rssh** deploy soak | **Yes** (alpha essential) |
| **P5** | **6.16.6** app LAN | LAN alpha |
| **P9** | **9.x** landing (excl. globe FR) | No |
| **Post-alpha** | **SFU** relay, **ULBW Codec2** voice | No |
---
## Dependency graph
```mermaid
flowchart TB
subgraph P4["P4 — backend alpha"]
OTA["5.1 OTA channel<br/>SVC-OTA · HTTP 400"]
URL["5.2 device URLs<br/>SVC-URL · legacy host"]
RBACV["5.5 RBAC UI soak<br/>API ✓"]
RSSH["4.rssh full stack<br/>feature/rssh-alpha<br/>Agent"]
end
subgraph P5["P5 — app LAN · dev"]
A61["6.1 Opus/Speex"]
A62["6.2 LAN soak"]
A63["6.3 lifecycle"]
A64["6.4 rotation"]
A65["6.5 audio lag"]
A66["6.6 stream analysis"]
end
subgraph LAB["Lab · cluster0"]
C0["cast0103<br/>c1c3.acl0.f0xx.org"]
end
subgraph P9["P9 — landing"]
L9["9.x hub cards"]
end
subgraph POST["Post-alpha"]
SFU["SFU / Janus relay"]
ULBW["Codec2 ULBW voice<br/>DR R0 · after 6.1"]
end
FULL["FULL_ALPHA<br/>no DNS/mail"]
OTA --> FULL
URL --> FULL
RBACV --> FULL
RSSH --> FULL
A62 --> FULL
C0 -.-> RSSH
A63 <--> A64
A65 --> A66
SFU -.-> FULL
A61 --> ULBW
ULBW -.-> SFU
style FULL fill:#1e3a5f,color:#fff
style OTA fill:#991b1b,color:#fff
style URL fill:#b45309,color:#fff
style RSSH fill:#7c3aed,color:#fff
style SFU fill:#e5e7eb,color:#374151
style ULBW fill:#e5e7eb,color:#374151
```
---
## Task index (open)
| ID | Owner | Depends on | Status |
|----|-------|------------|--------|
| **4.rssh** | Agent | FE stream + BE sshd Match User | **In progress** — repo complete pending deploy soak |
| **5.1** | 50/50 | nginx OTA path | **Open** |
| **5.2** | 50/50 | — | **Open** |
| **5.5** | Agent | RBAC API ✓ | Light UI soak |
| **6.16.6** | Developer (~99%) | 0.1 ✓ | Open |
| **9.x** | Agent (low) | — | Open |
| **cluster0** | Agent | NFS/DNS | cast0103 lab |
| **SFU** | Agent | owner spec | Hidden |
| **ULBW** | Agent | 6.1 JNI | DR R0 — [20260617_codec2_ulbw_voice.md](DRs/20260617_codec2_ulbw_voice.md) |
---
## Critical path
```text
BLOCKER: 5.1 OTA nginx (/v0/ota/ on apps.f0xx.org) ──┐
SHARED: 5.2 production URLs (apps not f0xx.org) ─────┼──► FULL_ALPHA
ALPHA: 4.rssh deploy (FE stream + BE sshd + app) ───┤
OPTIONAL: 5.5 RBAC UI click-through ────────────────────┤
DEV: 6.2 LAN soak ────────────────────────────────┘
Parallel: cluster0 lab (c1c3.acl0.f0xx.org), 6.1/6.36.6, 9.x, SFU, ULBW Codec2 (post-6.1)
Off graph: 2.x DNS/mail (postponed)
```
---
## Numbered priority legend (PO overrides AI)
1. **5.1 OTA** — nginx path (PO/dev)
2. **5.2 URLs** — production base URLs
3. **4.rssh** — alpha remote access (Agent, `feature/rssh-alpha`)
4. **5.5 RBAC UI soak** — optional click-through
5. **6.2 LAN soak** — developer device validation
6. **cluster0** — cast0103 integration tests
7. **6.1 / 6.36.6** — app LAN quality
8. **9.x** — landing polish

306
OPEN_TASKS_GRAPH.pdf Normal file

File diff suppressed because one or more lines are too long

191
OPUS_SPEEX_VALIDATION.md Normal file
View File

@@ -0,0 +1,191 @@
# Opus / Speex / stream protection — validation checklist (task 3)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-05 |
| Last modification date | 2026-06-05 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
E2E validation guide for **audio codecs** and **UDP stream protection** (FEC, NACK, negotiation). Code is on **`next`**; this doc is the operator checklist.
**See also:** [ndk/README.md](../ndk/README.md), [ROADMAP.md](ROADMAP.md), [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md).
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Prerequisites](#prerequisites)
- [Automated baseline (CI / dev machine)](#automated-baseline-ci-dev-machine)
- [User settings (both peers)](#user-settings-both-peers)
- [Developer settings](#developer-settings)
- [E2E matrix — run on two devices](#e2e-matrix-run-on-two-devices)
- [A. Handshake and negotiation](#a-handshake-and-negotiation)
- [B. Audio codec](#b-audio-codec)
- [C. Controls (user vs developer)](#c-controls-user-vs-developer)
- [D. Stress (optional)](#d-stress-optional)
- [What to capture per run](#what-to-capture-per-run)
- [Pass criteria](#pass-criteria)
- [Known limits (next)](#known-limits-next)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Prerequisites
- Two Android devices (or sender + receiver) on **same LAN** for first pass; repeat subset on lossy/WAN if possible.
- Debug builds with NDK codecs built if testing native Opus/Speex probes:
```bash
./scripts/build-native-codecs.sh arm64-v8a # optional; see ndk/README.md
./gradlew :app:assembleDebug
```
---
## Automated baseline (CI / dev machine)
Run before field soak:
```bash
./gradlew :app:testDebugUnitTest --tests 'com.foxx.androidcast.network.transport.StreamProtectionNegotiatorTest'
./gradlew :app:testDebugUnitTest --tests 'com.foxx.androidcast.media.AudioNegotiatorTest'
./gradlew :app:testDebugUnitTest --tests 'com.foxx.androidcast.media.codec.*'
```
Or:
```bash
bash scripts/validate_opus_speex.sh
```
Expect **BUILD SUCCESSFUL** and all tests green.
---
## User settings (both peers)
**Global settings** (main UI):
| Control | Location | Notes |
|---------|----------|--------|
| Transport | Sender / receiver | Use **UDP** for FEC/NACK tests |
| Stream protection (UDP) | Global settings | Default **None**; try FEC 3/4, NACK, FEC+NACK |
| Audio codec | Global settings | **AUTO**, **Opus**, **Speex**, AAC baseline |
After changing protection, **start a new session** — negotiation runs at handshake (`CastSession` / `StreamProtectionNegotiator`).
---
## Developer settings
Enable **Developer options** on both devices if testing passthrough / debug audio:
| Control | Purpose |
|---------|---------|
| Passthrough / debug audio | `PASSTHROUGH_DEBUG` path |
| Codec overrides | Align with `CodecCatalog` / `PassthroughCodecPolicy` |
| Diagnostics export | Session stats + negotiated mime + protection string |
Check **session diagnostics** after connect: protection line should match `PassthroughCodecPolicy.protectionDescription()`.
---
## E2E matrix — run on two devices
Mark each cell **PASS / FAIL / SKIP** in a ticket tagged `20260604`.
### A. Handshake and negotiation
| # | Sender protection | Receiver capability | Audio | Expected |
|---|-------------------|---------------------|-------|----------|
| A1 | None | Any | AUTO | No FEC/NACK; stable video+audio |
| A2 | FEC 3/4 | FEC 3/4 | AUTO | Agreed FEC 3/4; loss recovery under artificial drop |
| A3 | NACK | NACK | AUTO | Retransmit under single-packet loss |
| A4 | FEC+NACK | FEC+NACK | AUTO | Combined behavior; no deadlock |
| A5 | FEC 3/4 | NONE only | AUTO | Prompt or downgrade per `StreamProtectionNegotiator` |
### B. Audio codec
| # | Sender audio | Receiver audio | Expected |
|---|--------------|----------------|----------|
| B1 | AUTO | AUTO | AAC or negotiated fallback |
| B2 | Opus | Opus | Opus native if `.so` present; else stub/fallback per `AudioNegotiator` |
| B3 | Speex | Speex | Speex native if built; else fallback |
| B4 | Opus | AUTO (no Opus) | Fallback — no silent failure |
### C. Controls (user vs developer)
| # | Check |
|---|--------|
| C1 | User can change transport UDP ↔ TCP without crash |
| C2 | User protection change applies on **next** session |
| C3 | Developer diagnostics show negotiated video mime + audio + protection |
| C4 | Receiver UI shows aligned protection label after handshake |
### D. Stress (optional)
| # | Check |
|---|--------|
| D1 | Background/rotate sender — session recovers or fails cleanly |
| D2 | ~5 min soak with FEC on — no memory runaway (logcat) |
| D3 | Switch WiFi ↔ mobile data mid-cast — defined behavior (fail or reconnect) |
---
## What to capture per run
1. **Diagnostics** export from both devices (or logcat filter `androidcast|CastSession|StreamProtection|AudioNegotiator`).
2. Negotiated lines: `negotiatedVideoMime`, `audioCodec`, `streamProtection`, `wantsNack`.
3. Subjective: audio dropouts, video freezes, recovery after packet loss (FEC/NACK test: weak WiFi or `tc netem` on Linux AP if available).
---
## Pass criteria
- All **automated** tests green (`validate_opus_speex.sh`).
- Matrix **A1A4** and **B1B2** pass on reference hardware.
- No unexplained **silent** fallback (user sees toast/prompt when negotiation downgrades).
- Diagnostics string matches actual wire behavior for UDP protection.
---
## Known limits (next)
| Item | Status |
|------|--------|
| Opus / Speex native encode | Probe + bridge; full native path per [ndk/README.md](../ndk/README.md) |
| FEC 3/4 + RS | Per-shard wire (`FecShardWire`); v1 monolithic still accepted |
| TCP transport | Protection spinners disabled / N/A for TCP |
File issues under Tickets → tag **`20260604`**, component Opus/Speex.

345
OPUS_SPEEX_VALIDATION.pdf Normal file
View File

@@ -0,0 +1,345 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 16 0 R /F4 17 0 R /F5 18 0 R /F6 19 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 762.0394 92.36504 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 751.0394 183.965 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 740.0394 137.717 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 729.0394 111.933 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 718.0394 163.949 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 707.6019 160.1625 717.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 697.6019 110.13 707.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 687.6019 163.4625 697.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 59.68504 677.6019 123.87 687.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 667.0394 129.725 678.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 656.0394 89.69304 667.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 20 0 R /Fit ] /Rect [ 45.68504 645.0394 113.253 656.0394 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
17 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
18 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
19 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R ] /Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
22 0 obj
<<
/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
23 0 obj
<<
/Contents 45 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
24 0 obj
<<
/Contents 46 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
25 0 obj
<<
/Contents 47 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 41 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
26 0 obj
<<
/Outlines 28 0 R /PageMode /UseNone /Pages 41 0 R /Type /Catalog
>>
endobj
27 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Opus / Speex / stream protection \204 validation checklist \(task 3\)) /Trapped /False
>>
endobj
28 0 obj
<<
/Count 13 /First 29 0 R /Last 40 0 R /Type /Outlines
>>
endobj
29 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 30 0 R /Parent 28 0 R /Title (Prerequisites)
>>
endobj
30 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 31 0 R /Parent 28 0 R /Prev 29 0 R /Title (Automated baseline \(CI / dev machine\))
>>
endobj
31 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 32 0 R /Parent 28 0 R /Prev 30 0 R /Title (User settings \(both peers\))
>>
endobj
32 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 33 0 R /Parent 28 0 R /Prev 31 0 R /Title (Developer settings)
>>
endobj
33 0 obj
<<
/Count 4 /Dest [ 20 0 R /Fit ] /First 34 0 R /Last 37 0 R /Next 38 0 R /Parent 28 0 R
/Prev 32 0 R /Title (E2E matrix \204 run on two devices)
>>
endobj
34 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 35 0 R /Parent 33 0 R /Title (A. Handshake and negotiation)
>>
endobj
35 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 36 0 R /Parent 33 0 R /Prev 34 0 R /Title (B. Audio codec)
>>
endobj
36 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 37 0 R /Parent 33 0 R /Prev 35 0 R /Title (C. Controls \(user vs developer\))
>>
endobj
37 0 obj
<<
/Dest [ 20 0 R /Fit ] /Parent 33 0 R /Prev 36 0 R /Title (D. Stress \(optional\))
>>
endobj
38 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 39 0 R /Parent 28 0 R /Prev 33 0 R /Title (What to capture per run)
>>
endobj
39 0 obj
<<
/Dest [ 20 0 R /Fit ] /Next 40 0 R /Parent 28 0 R /Prev 38 0 R /Title (Pass criteria)
>>
endobj
40 0 obj
<<
/Dest [ 20 0 R /Fit ] /Parent 28 0 R /Prev 39 0 R /Title (Known limits \(next\))
>>
endobj
41 0 obj
<<
/Count 6 /Kids [ 20 0 R 21 0 R 22 0 R 23 0 R 24 0 R 25 0 R ] /Type /Pages
>>
endobj
42 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 896
>>
stream
Gat=*gMY_1&:N^lk)i&@Za8MHrCEER8qi]\\5==l72@G2'5T'6denO6-H;C(_8tQ"GP7Q;m-O']E-a3bO!9*-5$V>;TE'HC.%(3f!*=X4:Bs*c9#\0<\--:Mj,PJn7q0JDi@"quj(WC9?q(Gu+tT<I`/.\U#5j9V#XFZT&0b*sLf5^q8a_Y5HTPRdi<"aTC]Dhm9GTOB@(r!SG*IZd(`nAbYR[Ph368g(06ZtG%@7A`G*?>0#0u8^)>LDnT0oeS:;5N2ZcaGM9GROTNh/_=O*,plVD]K(l5IX#V\90r?8#n&h#@4/HlE!s]))Btcl_%JpALK8+"S>n`\gkn$koecX=5gL/d7eTVhNdG)l"LQ0Ll_]`ro9WHA#U,R%+h&qW)DphmJ2MKG*Q[NoGh^]i0a3@:"?hY<:%pS.pckhX"JRE3g5jruW_ZF>d@FZ`/8>M>Z":8`jkF/@<n)R=kp8jJYj_IL)f$>@T:\dqirAKtBi'`3R8Z.X@F6Npu-5l8YIEd@UP]Yl(`+r?i,gA)^JH^:3EI8Hn=^i?rSSK;k_j(qrlWc"\PibkfoYYd_:I_1h;sqPa\2FmC^i!ikP5C`H3e;&]boCHBT=ffno3?BAa-4lWOm;m5p.SCX8?mFj20jQ!JYp=tcNLT@?s6J+=c_rRa:D,3:'nMlK"W>;[)qqb_%lR6#`7]XVM#p^Va,><%#:Q<Xm9F3r>_>Tr(Y8ua4,8Kkt[IO58[4^s_^%S5\>"V#W0[)qt/Due-?<UH7ZA0iF)p0@%OJTWV=i:]4<rmRM``+(X5j/PA(C4/0a9(="&5L?MW`_=`V:k39MVEU@^>A6me21p;dDc%A1*[BjLAG#a3Ba6(YKg$$.*"#`WV069WY"!f[Ho+1^q,[7d.mpaBSZ9~>endstream
endobj
43 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 735
>>
stream
Gatn$?VfE1'ZJu,.F'1)_8OY!]bO^-1c.6NCjSE6+Z7cE[>XFZI/I9Z"Yi8GL*B8^A32fdHggJ67mquJYK[\>Y&JL*_$-iCn:W*dc7uI+%g0Q'RA;AadO7DQk:DM6K\iJ8()8#gA$EJeU:^KL;c+k]p23.QjNfSD6FA'JZ.BO2-lc4ZbsK;&U+t@pmG/qchl&^BmI)mge'Fa!kf5c1Na`qNhl9JR[.9nj:3kM]3@Xi'*k'oVCG0>1WL[9B/TGoPEeYg_j;Q4QRFB[9@0`[>k_n]R8IHoZ=8L2ScAh@8XJE5+'8ZF>/`$XKagGB@eI.fUniZ52IOrK<!H6L5Y(Xl)>7I@d2^T5/24^f]rBCcf+B\J.P_[<6mC<OP@JEadR@Dh&'(c/Umu1>U=%<4Z-7eti3G',:1M,e"c=GBi^Z\c!K].U\Kf+eHdl-uEm6Yf9[cq8Q/FOBh`%^jb\c?8ATe,l2'1\A^jkHF!g;?idlBQ:i6o[Hmh$sj-dT]5D.0GR+IVMm=5rjf!;Z<]@7Ur-Xb(gmKq<0p%5L*E%7m%?q^HlD3fP_nN1u`)dnbW4hn]<.5nsrFj"CY&"CE*'e(R*&,olMr1s181a4?jboX0[_&CcU7X*%.4`+UU`s4)BkhLXMQK:9di?6QI%.'\g$rQ*Hq3.,SPpl3tqm=NB::c16PZ4N$L^Nr(i'--Q!HrYNtXPTjF14[dFjcAQOM#D2qTfj?cWquPZ.mLT~>endstream
endobj
44 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
45 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
46 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3345
>>
stream
Gb"/)m8Mb%'*$6aYqs_]'\]9`IQ)XH")YU>!%$TADMB#uYsmXWa%gIVk^S(4^Vr0]c*d<-DUK0@>-4UVea@?WTci<L-@bCt^o&'/5#\+0bT;>87"s;?SBh[o*70XBeVJ*XOpZr?'3S3B*-^ncQGCiSJfb6=R21u`"_K9X'%$i30r$0EP`ehPJAZI(*1iS]3\5FV*u5gIiU@\;E!3M^E.FGg3+NuK%DWGnDF[i2`p42'mo7Etc>Fb^6dA`:%YL/'JpVXqrScr2a[`iZ:VM5[T.bfK9ic7`6stU&JK"Wm.3"$=9SWB@26LmZKm8H]:1/u'nWH8DjroY80Hlt"LI6F1m3g5>@ZnJm@=N3WVlgZ-?CnG6A6GRG<M3:9JU(+3;VeJD4H47)a(KmnPEk_ODQiSMI$6[Rs7,UWDL?^aqq&a[n\b!H3k`&;_Ob(LV8P29g2%at73k<#>6on-8S/?]653^DRf^R&:p$-N22H/7<XXIN9O3rg#\5.$L>GUS.`i!sB7L>S+nJB9U0Z@&9uo^$A)g$rbbK'4l]t#I\s`%=n!M/@H+TsYRdj#NA)@Rpf)^Xk]/G`>0]8X_\F8nu5![2+mQU73rhso?o"u3I'o1!&<Rnt)Z$g`3.#'U;U/PU.jt)7#1QQc3pio(=r>bcWHj/p_XKga04l4)tn8Cn.q.Apd?E>Lj6Fu8752Ro!^2Z:`Jep@P_U[>Kbb$/aA$I@[5j\=TnBhb/F1>OpU=")-q5a!/EupnE=K?<SC],3P(59&'EQjsm(:)Z%946jSa5Q%J;="9a:.C?d]'D<o,muq4QZ\h$RRBeViC[14T1oLiJ"Y7&hMZZWW(6j\]GH$QN#!RX4T5C0CqW"u4n07:+VE%ic.-]+-[;/N:TCQJ`0.OjA#(W^+;f>q`g\qQ6#m4m/E38eF#;Rl"cS*<_i!,?I!'qg6UB3EC9`M>Kf%N3mDaplcm]/JQ[931!IO*?'q1s5PP2Bk>\B7'][<E,g-gF7^)PD&!QW7?>m0W_?pu,hf2oi;dr$rM5X(69K(+W8+nMIO&B87L7*:uC:gR_<0F.ioG>6(5FQ+$Z>Xl^WP5j)NP4Lur^[&1<c(]l+7W[A39SqsN_-g3P%f=n4+Vus,[Yk?r^iQc6ggO`\ME_7-_]C#\_pDh69#QgI@^_eG%AUJ%&Tg=&?**e37f02TP>?B=QY/"cRnOB;I(m4o6[-cG+P$?5jP9291,R5uP,:*h##3YUJq:)g0&e'S`ZeB<^c;oZS=kPQW/pu+KS8V<mRDD-]O9$t@e*em]4.oRG8L!Dc,QZN#fmQ/WL?sn#a%e`d1d^\V+ZOLkilYOV8E(V*/:Uh7GZ-df&7HC]l8MjOeqh`,`5Sl65"Gk<55b*8DD*"hmSXmR>dYfF"6XS0fYQ/T&9/kAIP[$,Y\>)%Kin3JeTB/A4Y8mX=j[F'tHjUDagr;<N35.n#Kh45aFnJ6YR="D"a6WQKOb!!Mipr_jO;!-oriNBXbq@>.5c%>Id6ZG#%VZ*>s.b=I^jNMeb%G*/R'<F[ODh!SSSHl?6@U;t1^b.U%3k+kGV9eN&^lU#o;G([;lfA/d2)$MGt=Gt#$+2hsVOBL*N8Gp89n&)sQW`pME\B%Q'[oq7)BrcX^\iSUq3jf1klGI-UmlonJ3/!?/MRO"6Q+W?Ch"fSS,r87&E?O+N6%2FLap&"I+!Cj,V4@lQ^^)C2s9Toscr*F^@>2%Di.-fJnKIM%O/+:8SW<<1^*>gD$*S<tGW2?B%(,H\b@O=Ol;alQ2U60SJD[Pj2-tJ]7F/;7C-R0iQ%%bJ"0tK`!ao$k6:q,<@j%anAZS2Rf5,.7sA!1+rq3#<a>k*Dbj%"X13jNt(-$p9H<Mf+@6%D,#B/cjAaI3d"b0^$\*`u0;AR]@o1FcC%DE5Nu/9f++\L4D"h8d!l%M9=Nl_j%LJpiCLG#`20AY_oq%^JGB,qq,.f9D.-U=^)3_dKfQ-J>UOF<otrD@!2>C2#'qLOf=HTWDP\-L(E3^=e2*+KBbAh[.eMROk7]H_$NrXi@B1nT6hhH@()7jm0h;q5:hX<B.Buna`"J-rI%S+[Cu'\kYm)[FiuJ9JcCtKqHp*X):d.5%-2;Jjjg[4+=H1Lh,:8i48R-?\rmM>dj,(F:<<qr3[90Dfn/^RjVVE0[a?$rh5,ngjta&iqTARAtq=R0D3)2.qI=*74Mn!)I:-E_>$LEjt`0q;":no\=&/"V]CGPl-J$$b>-S6Kc>U3UDcqNqFjMbd5'@!W,3=)4"$cZaN9ae),I;4ii;B"967O?pAb>RCm*kQ8RaGEkaZ$'+:o?P\?4,+9i2NI,8#=`QQV@&h<U8!R4.aWGAHP@(-Vb3-s)mb?<)-b]:r[.4MJVI0`?07;q7E(,PFKi54KJ![TILHWZYH-d4Q_f\DUrCCtb8ED9U3-CSeG\Oq/l.[=FU<W`K7pSOeH4[5;q0;F.hL%4O!:D/%?<H=7O<Q,/>ElSoj;*fMtQDU;U,79c\X"@6"LL-h.i)YIR=`(Vd?p>DC)Ft+="[5kds-_>7;8T(l?&_LpEk-!mec#HcBbk+MMO*SaI3(abdF`BRY#NrI!cCMY",,N<;64*%Qn_n;Bg/"c4pK4TU8#^k)c0HtK3gEm2GT(:>,Z0d=:Bc3a>d1a9H:INFHtWcI$<VrNnQ>3[l>Wn/fre8U^RfM?ff_aN9<gsu_+8^-`Pn(*f.p-cT/m[D%Cn'rPFo'N*$5PupX-BS<hu(;:)L':FKVZs2l^ucHoTa/\nnP=?d,t*p)ZQaV/<?bMJ>rW/Pe"F=)OP.[*W<9WK,#[f.;J[AL"/$W]IeQh0^UXXUaOJ=`MbFq\rWRg>,+5]sOggK=*cI<c^rT*16=DHZk&2'`;?c<&o$4^j^%AgAAhb[sQN6'g%q$?Pbkqo@<Na3Mo5OO79c:0R>1Vp(Bf//)o?;_.juX&:tS.pu7&U*(7oV>.dVaM6?0efS(KYjWAa8<S%e-9"Bn2c!HZp/Ra'AUWc9i3TVl0(1#@XnqLi??'rNoHE`ZYBQ"5"R^0&h]\/1*0>O<MoningNU6eHW?`gX>f:1!gKrRSId<BN;t@r:?dXk_(m9fG]TMqI(0i8Q70_HCMe:EPrH_c:ZTHM37%hkSA"MA2hoBWtlH!fA$aoP3p",d<,!p3Wi\'t5RO*eRbLFH*(\qJEhGt)[8s['/%0(>H2E2n0C<kZC:&N^!]71:#h9"hX+3o$l&IuNSOa^i42eTPg]"b.\/-Mj\\Th!?O"9i[I_G!;R]<@<').bP$?l+(k+XrJh%N[[]OTg>\o[%*$l*VHL[+Wb5a1IUo$/enU]O7F#W:"&alo.l-t/l;%Q,)Sr^PU\n8D&oVlT~>endstream
endobj
47 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2589
>>
stream
GauHM?#uLV&q0MXkc0&.Fq_e&?W-HjaRQD2Lt<RrRho.CNF4#o+@]d)Ec.`ADr2U$J1q85SQiBO_[J[ah)"5aD@#J$+mE?X$h/<?j!t1a_I<\ee4a!B!Ugd??S=t,e"16a5qHtKMZ6@EHn9,7T4iWb@OK>"e!b$9:`E^YGalV9k6l-Ald!G%qKMoMp+7@17fLbk?+eq=78G:c?1PNfl."sMM;uKs2VI@/Ui6;tYq6DYeaoQFoE?d0_r><?l7`("jp5t6$$tSC<r^r$ZILQbW+PAh@qL)=Y$jA[WFNU),dVi4:07-e1PO6:^mSU:=mYT;pa\nU(gi\6[ui"+IOBVa"M9I_*l`,K#Eg1,1m2ja*3WHU/=HA=G!%goPdY1\kDO,i(9u1p&[eeHE!m-bVU6F.iDsl@SDSN\o=5;HiI%H[8iW/o+69,I@s$!USdtJ+g$SOM7f/aM_.I=%(Os<X-kr<Y\9lW`3"6nfl?E<s.!5M[P(%po"^UR%8]2Q;r!e-??YC,<Qkk8LpP8cRBGMRcK[aG"iGS_*Gb[MsFhbF7Re>/,H>smN2##%k8d#&4)\@uZ%="-q&>YCE^1s>#U+f.&1lIQ4/hW#n__a.kX6/CcDuQdQ&C](oM5;GY(fS\!LnitXgjrQrLDgRrd$#;$1<G'._Go?i6K)1%\Hq4d";:H#,'a<<#p/!q5TlelfeH"VZuP8n_e]+%)O:DmOjYCdN^]jX6$]ohabgjNlI*MW`jF341EgtpD5QO6DG&=K$q4B4O5iX/a6BL>'Gt_0^>TSVE.H4V6SW"EHK]pr<^3V?l*:lU%1Cc-ieW;/-__GA?JZZ)go<PX7kNbcbHq)MR,>]qO#J8>_2Xi;S0Nj3i8qGkX:iYpA]m*aOtM5#M3to[0+'A:h(J3-"MBpO2%TX6jS*Y"a[e$155_KKADSq,StHlPNu/"kcdneOZ0t;H)f^B^(KB1B^$fqA>CXiT(LTjPiHOO^kN)Se47>D*TKdC^UMbqHj0._$,d>'kLUaW^U]\'d!O,R_r@mH,gorG-,?DbA$GYP"#D6VV>kDeYieeTVnLESIU&fhVdXYD[V;c*ic)8o\VDeEd@@Ge?VD]_hLV[MIWPNG1WIgjBYu@?+5n=1-/>k78(P@lGVDBT\@F/n(VH%6JMnrr&rs1A"D"MFPBK1WqX9TUQ\,$jdpGrpYk`$0&&.9*(6Os2AQ/OaI@u+B@k^r.4V1Ms()fQPshp)74+*hk;_EN?+f]rJJU\k\!?scg7L9#N>`H'!]A';>*q?Be0Z30uY7kP?gQc8-N*7X>-f@GT0cD!K4foWQ"K%pf_noK8gX,UPOA@_8d.@dMB@NG^pM]'+RHf["#B<Lk$b#\_Pj;YQ%EF[d`eCK/U4>Bag-4H#b)P&m=B#k9COIe9rcI5I$#*H(:J6)p!SFE"F&edDO-;)Uj#s$<YQ=Xd[Y\>VW=;Jk$O4`h?W*XT@g9bcH4gL'YY*.&-%>uAF.#<(%k1Qd6Us;<mDCroY/T'3(-I!u7=5TDOSXb^4Zr1J49r#m0SJ<(uCB8mVWq049[]gHjJhaJ$?(TJS"PIdKPohD[;_,_Yb)(f?WEoCLNdkq&5l7_ZQ#1FHD=kN@_H'FjmY06Z&9DTo"ejZo<.jL7NWsa6jQlYO9"k)1P,n%KXk)Y1o"k1QZKB$aDHBV:o12s_!t/UaIB89j*W%g2@Qo8((atE-j_$Hm4b'#<FU[gNF"mDF(\4V_oD`jDb_bXiY'P:O?QS6n8[T9G_>&=7ruGQ$j4A8.a:K(79a<l-A5mV@E?>3]4P4h^MoeE3\+Q;N[.B%p-MK4+;'e]?8&/ZYJG7f*'<:<*h(C`_9.r=77"H;3*cWDHS`I*u;__,K2N<AnV`TQU`,T0#"EauX8M%;bHe7)NiZhlk5t0KKeU5'N9F'g=s!)]Dr(!b:Y*LHp6OtlYRf1if<UX'1q=jQ9bm6)%4YuEi6?K5OI@GE)?a*M@mSn^7-+;eF%pb25>iZo-`;;*EQqn1C[=BMj+:_LGqR'I=[G(JIMg$(b78S$ClrYA=O"i0@mUA.l0VV'>;aSqHLN"7gO3;tae'1M'RY4VM_M:W??.`_r.Y,>Cf784ph;D8#)^d1%d>5/3W14V%W]F"F=8/.26&):,ld7TVC!gAIdJZ3rT-7]\rE+W>Wn-f5((F_P#4+krRqnuN8R6E!oLHUMm%5j&.M&-3Fh\u;qrq\4'>(`@>s3Emdm%7\?9V%i:QFDp]bVdsMLkb<`VuX+E`f@$feW-321G_/"9C\.FiR=c.:F*??cF2Ebq@nge$e,>((XDCB8fQT^k^\DA]4RJdi0@AOpZ4c3-`5*=5:Jhc(Fp4'P1%b4`=_h)2nhiJGH,3,0ouI*9Oh[G@\<u.P@`QFA^B`!HlC[++i;DR+k%B4C9QIj5F=V*m[`7`+_=_iG5RmSPAm:IP!5Jrbpo9I&,>HcThi+g1G[qBBY`/g#h</ns)i"I45i*YU$@4/:8ajAXWAGG`Z*PMn4V0Xm=u4l4d=!f@A)<E08U<W*\1-kS^;6o8jqLa!AU.U&J>L?tp3XRA4/$q3KuQA:ar<9$JPjP.G+5j3Fu,`ds%t!L:2b#6~>endstream
endobj
xref
0 48
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000513 00000 n
0000000660 00000 n
0000000807 00000 n
0000000954 00000 n
0000001101 00000 n
0000001249 00000 n
0000001396 00000 n
0000001545 00000 n
0000001692 00000 n
0000001840 00000 n
0000001989 00000 n
0000002137 00000 n
0000002253 00000 n
0000002359 00000 n
0000002437 00000 n
0000002521 00000 n
0000002727 00000 n
0000003026 00000 n
0000003232 00000 n
0000003438 00000 n
0000003644 00000 n
0000003850 00000 n
0000003937 00000 n
0000004281 00000 n
0000004356 00000 n
0000004451 00000 n
0000004585 00000 n
0000004708 00000 n
0000004821 00000 n
0000004989 00000 n
0000005099 00000 n
0000005208 00000 n
0000005336 00000 n
0000005440 00000 n
0000005558 00000 n
0000005666 00000 n
0000005769 00000 n
0000005865 00000 n
0000006852 00000 n
0000007678 00000 n
0000007906 00000 n
0000008134 00000 n
0000011571 00000 n
trailer
<<
/ID
[<1cd619210883038fac0702297f0173e6><1cd619210883038fac0702297f0173e6>]
% ReportLab generated PDF document -- digest (opensource)
/Info 27 0 R
/Root 26 0 R
/Size 48
>>
startxref
14252
%%EOF

208
OTA.md Normal file
View File

@@ -0,0 +1,208 @@
# Over-the-air (OTA) updates — deployment schema **v0**
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-20 |
| Last modification date | 2026-05-20 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
The app checks a **stable channel URL**, follows it to a per-build manifest, then downloads the update. Version comparison uses **`major.minor.build`** (packed into Android `versionCode` at build time). Deploy never computes `versionCode` by hand.
The client runs checks and downloads in **`OtaUpdateService`** (a standalone `Service`, not embedded in cast/receiver services). Launch checks enqueue work there; download uses a **foreground service** (`dataSync`) with a progress notification.
MQTT is fully supported for OTA sources using retained topic payloads (`mqtt://...` / `mqtts://...`). The URI path is treated as the exact topic to subscribe.
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [URL layout](#url-layout)
- [Versioning](#versioning)
- [JSON files](#json-files)
- [Optional HTTP bundle (.otabundle.zip)](#optional-http-bundle-otabundlezip)
- [MQTT transport](#mqtt-transport)
- [Publish from a release APK](#publish-from-a-release-apk)
- [Docker build + staging channel](#docker-build-staging-channel)
- [App configuration](#app-configuration)
- [Checklist](#checklist)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## URL layout
```text
https://host/v0/ota/channel/stable.json
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}.otapkg
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}_sign.json
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}_manifest.json
https://host/v0/ota/00/00.{major}/00.{major}.{minor}/android_cast_00.{major}.{minor}.{build}.otabundle.zip
```
- **`v0`** — deployment schema version (default channel).
- **`00`** after `ota/` — OTA payload format revision within v0.
- Components are **zero-padded to 2 digits** in paths and filenames (`00.01.05`).
## Versioning
In `app/build.gradle` (overridable via `local.properties`):
```properties
ota.major=0
ota.minor=1
ota.build=0
```
Packed rule (each part 099):
```text
versionCode = major * 10000 + minor * 100 + build
```
Example: `0.1.5``versionCode` **105**.
## JSON files
**`v0/ota/channel/stable.json`**
```json
{
"schema": "v0",
"manifestUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05_manifest.json"
}
```
**`*_manifest.json`**
```json
{
"schema": "v0",
"major": 0,
"minor": 1,
"build": 5,
"versionName": "0.1.5",
"apkUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05.otapkg",
"signUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05_sign.json",
"sizeBytes": 42100000,
"bundleUrl": "https://host/v0/ota/00/00.01/00.01.05/android_cast_00.01.05.00.otabundle.zip",
"bundleSha256": "…",
"bundleSizeBytes": 42150000,
"mandatory": false,
"releaseNotes": ""
}
```
**`*_sign.json`**
```json
{
"schema": "v0",
"sha256": "…"
}
```
The app loads `sha256` from `sign.json` when the manifest omits it (APK hash inside the bundle or standalone `.otapkg`).
## Optional HTTP bundle (`.otabundle.zip`)
`generate-ota-v0.sh` emits a **STORE** (uncompressed) zip so the APK is not re-deflated:
| Entry inside zip | Content |
|------------------|---------|
| `manifest.json` | Same as `*_manifest.json` |
| `sign.json` | Same as `*_sign.json` |
| `package.apk` | Release APK bytes |
**Check path (lightweight):** channel → small `*_manifest.json` (+ optional `*_sign.json`) — no bundle download until the user installs.
**Download path (HTTP):** if `bundleUrl` is `http`/`https`, the app downloads one zip, verifies `bundleSha256`, unpacks, verifies `package.apk` against `sha256`, then runs the installer. If `bundleUrl` is absent, it falls back to separate `apkUrl` / `signUrl` fetches (required for MQTT).
**MQTT:** keep **per-artifact** retained topics (`manifest.json`, `sign.json`, `.otapkg`). Do not publish full bundles on MQTT (message size limits). The app ignores `bundleUrl` when the scheme is not HTTP(S).
## MQTT transport
Use the same JSON payload shapes as HTTP, but publish them as **retained** messages on topics that match your URI paths:
- `mqtt://host:1883/v0/ota/channel/stable.json` -> retained payload is `stable.json` content
- `mqtt://host:1883/v0/ota/00/..._manifest.json` -> retained payload is manifest JSON
- `mqtt://host:1883/v0/ota/00/..._sign.json` -> retained payload is sign JSON
- `mqtt://host:1883/v0/ota/00/...otapkg` -> retained payload is raw APK/otapkg bytes
When `ota.trusted=true` and build is debug, strict checksum/sign enforcement is relaxed for that source. Release builds always remain strict.
## Publish from a release APK
```bash
chmod +x scripts/generate-ota-v0.sh
./scripts/generate-ota-v0.sh app/build/outputs/apk/release/app-release.apk https://your-host:port ./ota-publish staging
```
Fourth argument is the **channel** file name (`stable`, `staging`, `next`, …). Upload `ota-publish/v0/` to your web root so `https://your-host/v0/ota/...` resolves.
### Docker build + staging channel
See [BUILD_DEPLOY.md](BUILD_DEPLOY.md):
```bash
export OTA_BASE_URL=https://apps.f0xx.org
./scripts/docker-build-ota.sh
./scripts/deploy-ota-staging.sh # after setting OTA_DEPLOY_TARGET
```
## App configuration
**`local.properties`** (not committed):
```properties
ota.channel.url=https://your-host/v0/ota/channel/stable.json
ota.major=0
ota.minor=1
ota.build=0
```
Or set the channel URL under **Developer settings → App updates (OTA)**.
Legacy `ota.manifest.url` still works as a direct manifest URL fallback.
## Checklist
1. Bump `ota.major` / `ota.minor` / `ota.build` (or Gradle defaults).
2. Build signed release APK (`versionCode` is derived automatically).
3. Run `generate-ota-v0.sh` and upload `v0/ota/**` (including `.otabundle.zip`).
4. Confirm `stable.json` points at the new `*_manifest.json`.
5. **Production:** BE `location ^~ /v0/ota/` + artifact mount; **FE** `location /v0/ota/``http://artc0:80` (`install-fe-ota-nginx.sh`). Public URL must return **200**, not 400.

302
OTA.pdf Normal file
View File

@@ -0,0 +1,302 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 13 0 R /F4 14 0 R /F5 15 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 762.0394 85.25304 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 751.0394 83.47704 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 740.0394 83.46904 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 729.0394 182.189 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 718.0394 102.141 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 707.0394 143.501 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 59.68504 696.6019 162.4425 706.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 686.0394 107.941 697.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 16 0 R /Fit ] /Rect [ 45.68504 675.0394 78.13304 686.0394 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R ] /Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
18 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 34 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/Outlines 24 0 R /PageMode /UseNone /Pages 34 0 R /Type /Catalog
>>
endobj
23 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False
>>
endobj
24 0 obj
<<
/Count 10 /First 25 0 R /Last 33 0 R /Type /Outlines
>>
endobj
25 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 26 0 R /Parent 24 0 R /Title (URL layout)
>>
endobj
26 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 27 0 R /Parent 24 0 R /Prev 25 0 R /Title (Versioning)
>>
endobj
27 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 28 0 R /Parent 24 0 R /Prev 26 0 R /Title (JSON files)
>>
endobj
28 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 29 0 R /Parent 24 0 R /Prev 27 0 R /Title (Optional HTTP bundle \(.otabundle.zip\))
>>
endobj
29 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 30 0 R /Parent 24 0 R /Prev 28 0 R /Title (MQTT transport)
>>
endobj
30 0 obj
<<
/Count 1 /Dest [ 16 0 R /Fit ] /First 31 0 R /Last 31 0 R /Next 32 0 R /Parent 24 0 R
/Prev 29 0 R /Title (Publish from a release APK)
>>
endobj
31 0 obj
<<
/Dest [ 16 0 R /Fit ] /Parent 30 0 R /Title (Docker build + staging channel)
>>
endobj
32 0 obj
<<
/Dest [ 16 0 R /Fit ] /Next 33 0 R /Parent 24 0 R /Prev 30 0 R /Title (App configuration)
>>
endobj
33 0 obj
<<
/Dest [ 16 0 R /Fit ] /Parent 24 0 R /Prev 32 0 R /Title (Checklist)
>>
endobj
34 0 obj
<<
/Count 6 /Kids [ 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R 21 0 R ] /Type /Pages
>>
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 857
>>
stream
Gat=*gMWc?&:Ml+b[Xf_f402Za6k6(XX[=f;4m]:&gs@[O9O'4rdG+#KZc7V,*Z3^F3^9n`qCNoHEd7<`rOuu\=4Y7J7&[3R=sK!IKl!#'E#5%MJ5k+$-!h%?!e5)Q3i?p/d"BJ)O^6.d.AA15_tTfDC,?`aj%=C>ic$.IEd`VbUDFCKPUh@`t+Y"`S*@]Pel,H0t;_FJ_bJ](,gq,+UfpgG*D?(RhUt1f/=,tKASr`%`'_VD9!q$M4UmP+@C8YJ16Mm7d,lcWZ@SP<#Ju0,uZ+HQP0l`SRlLDUm8oWUQchWatVeBb'2<"H^%IOO;?i_:>3u?CQao9<tVdb"B*SVTed)">N.3+UQ+2=fs$?16?bPdpgn(;)IdcU='riq0CubV-).$@g,9#FWZ]7[H\Nm'_l)SG<4AgETqoW#V#3SC7<WadP#,#@F4Aeq^P85Km4^KHASq0p_S#s[Z0Cb,=00ke5V,u5-UjK>rA%'`HuF5lG96NHo$lZJ[NA?fV9*<-=)_Z1<&6QEh_Q2e,SeHho!7-B%KmfN%Te\K4hC=M1d$G:!HG],QI1(R!963O$:'VII-QAbFkZqD<LN9#0>SYLac!"m0e2m`rJg!#m3:9'4W2,a)1!_9IF7T';$,K'?"Gq?HN]C)_2i]PX#c/J^E&to1k/rsl\)9do*+l,qRo[l-]D$SJoo#q?dtpfhp\,<f6:-g\q-2_M)Q5qCRR0j)^]3Rb+><;,\jWj0k`30)WeFnIZ6'6LoCutL!V\D[Lf`6\1NBgd++G)7%cAlY>sRW0?71ec?3h>2:LdJERurj&WMP5fXWngVEaOo7PQ=C0_FSe'oFG.YNd=0#^;qGK`I<db9qYU~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 612
>>
stream
Gatn$;/ao;'SYHA/'d/ZOr$3bSa]Ys'7rM3ApEU!RlYD"C'Q+pJmXSfFq\\W#ZG7_:1?<B\bB>j'N&OcJ'E2QCP@Zs$Hf"Kd"EZ@f6F>Ch^A%U1Z:`t4A$`m,Z1'X@9j]s;9E>hK>a16A-sM@b7Y/sSqM%1#7QTbG(IJi%*!V1RF9:$^_Oq]D2iD%F/X"R07*=dBW(VLH3QSti$m*&KQL#io(a2hroq";!&hs=_L=AV.@i'XH2oiua25R*SUEh%<b=mcflKhNM4N16#Zb#OVpl^0cE;cZM_j?rI<ZmLb'J%k5SFlBC&3(L'O%\W;`d4^r.pDfSp/AXCc3]SgHi`Hb36iQ3re\f)'\s-))M@=r*G6HDi=R3$sG8h^,8*!;H><\7+:FD-q`P>D69cAkXguCdP@rjrX:3tFm!mS0E^Mo*fL#0A+FK@AO%[M+\=40g/CdCi.@cF#sf>fBI5,KpJ5>UHPAgg*9pl6Bt(8:\TrmsRZcJ6$cXG1PP6Lm<SD;B46O($/`rC$pedKlE4V^ukS?lopZ3F8>3#K#W,97g^*-:H,Vl&m$9[_e.Nbs]&TR_%r1T\%P0c$rS8lCp:%rXt0jI!A`PLUo2[OaD~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
39 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2975
>>
stream
GauHN>EdOG&q6H[d5=q%.a60dZ@%Qj`<-L<?$84:0]>-jMGb'5&`_L]>FF/qikM+OHqA]Y*2"EA&mK5G<S_"TZ%1iW1@MP,_!bbMrl5(u_0?!H6Q?rXJQLm<]/(t8FBmgb>j?h]G_Ar7a61b)[hAIR8T8m.1Ku.]bD,O*/4s/1^ZIN(%&nLu9,NUh!r-@0Z^$p6VSM-sN'AO1b6S?PimU`^0TRCi_T*1*Ee8\9_'LYl0T7)OO:"sG1MfI"2;QDo%Vm--Kh0`?W0N0H*^<&$bibt>K^EBsPp#Q\Oeaam?l#q)4HV(Ah52ARCrR:4#o,:ef)0TZOL:&:h995i"=Aa1TA>Aba9Vrp*mKmNc3o>`=me(h0B0O@5L1'TE\"[9P:$K>MG4pP66u%D,MuOYoRmOKO+;DCP<p)Xn,!S2]$:hsE#)%X.7gmE>.QJXmo`mbFh+[js(GYV,EdC6CC!-BE'Wk^Eur>`W'B,;cm;)b!jS>Sq:t`GAis)27*L>62GCHT;Jk2JHlL=2\[7'E"sUY5MQMDa34[RJP)+US>*ug5L?KgV`%m1YW,LCd8kjV2S)6=tZPqf`#s:bddP791b=&/*ejh@RN%9!i+$\KQ@6\'q6Ckp?(Cu:kqVV&-0ut%EgG"#.FM$5EM*dE#Z>_sU%V`RMd#_oR)(d!DMTZ9\<P4GMf^Qr*lOChbG=k"=%LF;$)\)*Setd"1XnJ=r\rRq>,:g,;b,H#i[<t)@gu9D>;jfIrdAYW@J."0h:=E>iVkTM@.C";9FltsN'A%Du!sB,(.30EOYfOoW%^</=6>an/5i<5)n:C`F2R,Eb%M3"n0]21CTK_lIG(9a=aN,CTSHPP18qMA"=unE94;3Ur>jo]W-@H0qA%Yg^*TjV(alZt!&qTSajpE?n*DcH50kZb9Xr1;6TOZ<s?PbcMhS<Nqe4,5O.`N`S:PZ47_n6TkZbf*oA%a:S7iOUfpd`&eipgmS.t6bUnnlIS3q5dSW_^lZo$u6CDZhHBgb+'_Y>;-&7__^+:IUiR[FUjW`^UJfOdesF[0@A#76nJOLPICAjogt-7XW1\i&Zs8-^O3[W]Fu2&]T3c)app][5Q"H[H<R;In[7kZ/FA5'j(dKA<5h=h,:SO,<0NGc5-g\DiIFPZW0@u>k7^lg#C!9E\EE4\i=.n^Sa4OEs)/,?1J%/(IF0R@F7k=^$L4\8URr)#&%CE@5+$@WRA@U.'KdN)'2u&:df[_=t1c:.^]P)S$fDSY+FIPlg`c!UL,S34kM`hp0aRW/GcpY!"no8c"\NIK]>[@U8Ac[4-=U5!Sg=ST-0)*4<t2t&Fh8"lDC19H.kV5/C0#FIHuUKGLR!/C1?bY1KnBFF&1tjB3+,cd<nI9Q$Q]TLs9$\HuLZj%lqI3LVU!Q2`FIiVSTmG?V.\>i5n6'`5C40.A40b]ccNO6#)_foX8._!V^mXGQu0PE.R$.5PP\H)7++kg9k=Db_KuVFkpqSKif0.>t/dR9Y^FZY;`/"g;ngJ\^nYY^NOBFQn[-mP*SH_FF(@MA2WpKO"B;#6'j$\AWa`FY2>]/QM+_H32K9Z!d5*9q%])5#@RiS>HDAj2c,ps&ZK-\`=-^WQ/_D5E.1U/0D@sDAFhM^ol)jcU"6HH!"B?HmT)X%H-,.%YpJ>$n^tqOmSN6m+F[()ne/IBW!2ES$_#h(@8;9u:X%_<dlX7*>o:;bUG1<X!3P,.)$fV6Q+JC7i*Eg42d`+oZ+OiV-3mLq+Fa@\36:uIlHC0Ms+Cq6@=N_!0B2^r,uU8j@3urBRR&cnrX$0pQO5E2Ddd^nig;JYV[9,9.n*HE[it3Dmjb:rGsm5f%oZ=C9ggCS.c/1u%-K">2<H,?Y_);F;WdeRC='CRo3H.6g8LRPc>9tSj]CQ][-G:;/8I'$]A5Oc$ulp'`_X$YYm6ie=k.>Y8R/I.M^2Uf-p)eW%smhMmn+=_&`PZk$@9U913ifdDclOOKoJDK&b]nL/;sVG:A4tVrdMY>m=H)"%U0sSo@Fqp&'736F$+nQLtQHQ>+4d5A11mt$%9;Pp+^LJ9Qr6E73iMK`"bmaKrngqWr#.#lSnNWDoUBN?8\46D5;C4ok(:m:S%Q$"DM]-Ug5@m(+ltn=4htWH4."t14,d4+4"Ei:-1d4\.EL"G2Q5I"F;>4/A:TuHj[tP/n5[gi6&?f&M,PR7fCJ$m%B>kR->ThJ*%7bmBLs,mNE3aK#D\W`Yulfq!^S*a0IOb:Crm=ng[PZA$HU1q_J&7eChX4hY6Ma&P!14qV'HrcYMunCNZH%Wkd2EA*`P`[n6gVk.F!B^M4@1K9/H=I.om8D4GIap6JM%WNebAX\U\I:XJ8a=#Ag6Wou6#^(^^VBb0D!R,-JgrBgc99Wjat9[^f<0DtkG%%Cqf_=",uq;IAjb]OFb2H]D`-!l:E@g;/rI9O0UGBA0GmF,Gmfc.`-@K<V*#4Sf5^q?m1J61D(@mO]\c)J@s3PFlTqog80aQYlVYV>MN3Phu;_!5M1Kg+bE%ZD`e:bGi760S&PQX_]7(WXeXdBf`d*eo*/@\q0D#'!LDHXf/h7V9[tMMk*>4/e+&%'rn#<$XN".[2FMJIP)"2DAoNZh8t/L4N\IluO+]OO.HA\Z"grp(%V\7M=)(7?)'76;l\K1qs#E+E)HLX[*[;R7k5I/f1g=ZG,472!3O^3cUtO:Y521q60n4I*&ctHD/U]k6pEk?)s+";^;KPT4CSY9sO=rC>cYLnGlLP;/]d=@`('8C2VGU[`m5\Wm0;E#"TL:)ZdY(SVXn[9_.4RIC.euE+8J<Z)F'I,4AQg9c`ok?]4DAaT\<93dp[']BqTT^oG-8gE/6X4/"gMN"+LrCo<uFQ9cV<kA+F[Z`u%g\ONZXOU&I9LJV`m]4^T<h*H&?Uat_7gf_'?VS*WHoJN[sE7<rHi]EIfHVjati@HcB<ehZ.mJ5DBI:A0I&F5Y2Z:61Q%pTN,YJLOirX\Xs<[n~>endstream
endobj
40 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1936
>>
stream
GauHLgMZ%0&:Ml+$r@XWG%9fHag+j@5dEtigU^*;l_#Y61nZ/Kejd+pWEf;HDsrdRS:m1@,q'N5CfqrB4>UWq%-<#!+%Lg9.DX=FXA]?D(T%RZ4Q=2!![lHfD`^]OddtS"jP(^Y5sXeQfY`"0(ahA6[2]FK.CcOLCn806s$ArXJM3_kqrd2X,c@%YE\;QEqA#mrPaTkmBehPfM;N+V+"1?RXaVXDo:r5'/d2?f@N]@,i7BVpIm?<gPFP)b<jZ69W.86Pe+5_iQP,0rT#>*7Au+^KhOMHU=7'uSBq2H,\m63>6-=kR93a$o5itKT$=&-S8?!#!E1`I:/h]D^A[rHN*<>tSZYqQP0-d&K5u;J>f?u`S^IbtS=fYfKI:kAT=?![R959/2RUn%TKV\3mOfotT9Pf-*U\?Roi&;.)Kk]\;A\pQn/F*pccY8F5rsmno/OINY9!`cYKR>rZHD]h)#@mr"4,((/-K8V1H;nW[=%'4i?"8Ri`V&e%BK3\Z_t2ceoi.=99-u\"_ell]gWR,+>d;MkL$"6QV3!5h^gC\_TNQpL'IS4Q&Y#-14V3dsTnIC(_J<#GQ4=>H(-)n.MRfPAZ%UsW@_B7;*._Q41'/7C@2-+F08_6GgsUO,4@Kc\/!H=u0FUEh<Vc5Y8f'T[.9.>;qnZBV0^CrjrUHC67#+Zt^,r6_maO.dmUs1mMfSVi>qV@W-dW6&Bcb+f3tNQr$Rr/'b8@.?oe@fl)k2X`]qHLPg'7X1$;5`pb"Gm%3!^C5<OhP0kZ?@=n&%c?66MJWq"HY:<NuY#LtJoS%-lq0cB_6Gdc_15"G%47G;KgIGae_GQj,4Job9>5&KP5E25sQg1b)j4-qr>Iie0Up9'`S1=::&oHOU^"j+1itAaE00NL@>c5MIK'=m0DRcFDTS8jpmobC!T^DqQ+TSu]f/_sjnM<Y0%,67jr/j@nXV%23$Q$H(7(Y#2#JUH5NsCG%iilSoEY)6_!<I8(l0*2$r(qr<4:)ro2RjOUk@Tr[G,qna0i?%/4^?QGc/:X8q`C"/iKnUnZtACkpHJsLdrG<1l3VMA5CLL3fZ/h\OVFdt,l"^0K%G9l@pL9-EBn+ta^.^PUc5@R0^H_'ttG?+JAquCg^dIi)_WEcU7Y]dq7o^Ll#an2Dlp&f+2:tJnB(i/AJ^anO?YUEJ3[>:.ql;h0?r9(uWHGja1[T2CgN-MRnj<75g_=N7u)-8]g;*)pI)(&@NVeN4V4koA>'6!5T2Un!>Y2@)0(#CDh8BI'Jem*+YJJBW`+t<JZo)nmS]f<oVcBEUD<=e-D8DoJEiXU]\.g`jr9S.#pX*]m`9i%nL'eVUo9VBTR,d*j*a6\O,.Xj7M2uUC-V#AO1LJK:9*e>MA=[Z@B"U5ug@^^&kTn=P8_st_qYJr\59,cYGF^1Ee*XQ3e<6.$ji&9cMq@#n<ME)(+6W:2d[\-2IVr;b7#_ScZ_FqH:6$7ESW&gH/4P,!C3?/)jAQgp;$;#-HaD)]oW8D<b:5Eam^eXPIV\(fs&R3U;OTLGc#NUpBG)Ed:\LNuHLW4N5;)c)B\_Jkk:d@8+d8la>Rr]Y.i"ED]3N?#:176j@6XVuoO50e?qJtn(dF-2V#E,mm6)s#7h7N%c`EARmOfEE@*-D=PUR7]LrY$*f`(j&EN1IG5d3J-HeMs/7ilsiQa-jjK?`RYUDU#`\Sp^dh"]Jg<VS6r_qfnS%Ci=iRDP+i0'2t[PGoi6EWos7+Y+lXDLoSR3./a#seCDg6oc?2C))%pd]"H8J:Su+['6?d8Pr9:/(D4!U1ejoPXkGcf6[O;nD\LV;+(lGj,j?u5M3)/lG.4"MlE$XSV-tBB>K*A8KKdAM\U&a%o#u33CRZ9q"4c.N[_9op&pTEM%)MjI[k4'q`ai2b>(+=)+"N.'$:_V)EL`0:L>EK\Fnilbm/.)AiK,\~>endstream
endobj
xref
0 41
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000502 00000 n
0000000650 00000 n
0000000798 00000 n
0000000945 00000 n
0000001092 00000 n
0000001239 00000 n
0000001388 00000 n
0000001536 00000 n
0000001685 00000 n
0000001801 00000 n
0000001907 00000 n
0000001985 00000 n
0000002191 00000 n
0000002466 00000 n
0000002672 00000 n
0000002878 00000 n
0000003084 00000 n
0000003290 00000 n
0000003377 00000 n
0000003710 00000 n
0000003785 00000 n
0000003877 00000 n
0000003982 00000 n
0000004087 00000 n
0000004221 00000 n
0000004330 00000 n
0000004490 00000 n
0000004589 00000 n
0000004701 00000 n
0000004792 00000 n
0000004888 00000 n
0000005836 00000 n
0000006539 00000 n
0000006767 00000 n
0000006995 00000 n
0000010062 00000 n
trailer
<<
/ID
[<bfaafcc416205de30349560cb8ea8f8f><bfaafcc416205de30349560cb8ea8f8f>]
% ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R
/Root 22 0 R
/Size 41
>>
startxref
12090
%%EOF

133
PRO_AAR.md Normal file
View File

@@ -0,0 +1,133 @@
# Pro / OSS split (cast-core + cast-pro AAR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-23 |
| Last modification date | 2026-05-23 |
| Co-authored | |
| Severity | medium |
| State | postponed |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
Design notes for a commercial build without forking the cast protocol. **Not implemented yet** — complements [COMMERCIAL.md](COMMERCIAL.md) (dev-gated Play Billing placeholders).
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Goals](#goals)
- [Entitlements API (in core)](#entitlements-api-in-core)
- [Module layout (Gradle)](#module-layout-gradle)
- [Play Billing flow](#play-billing-flow)
- [Build & release](#build-release)
- [Migration steps (suggested order)](#migration-steps-suggested-order)
- [Relation to sender resolution test](#relation-to-sender-resolution-test)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Goals
- **OSS (`cast-core`)** — protocol, UDP/TCP transport, receiver, sender engine, crash reporter client, settings model. Publishable repo for trust and forks.
- **Closed (`cast-pro` AAR)** — entitlements, license check, optional premium codecs/UI, Play Billing glue. Distributed via private Maven or Play App Bundle module (not on public Maven).
- **Single APK** on Play: `app` depends on `cast-core` + `cast-pro` at compile time; F-Droid / OSS build uses `cast-core` only with stub entitlements.
## Entitlements API (in core)
Keep a thin interface in core so free code never imports Play Billing:
```java
public interface CastEntitlements {
boolean isPro(Context ctx);
int maxReceivers();
boolean allowsResolution(CastSettings.Resolution r);
boolean allowsStreamProtection();
}
```
- **Default (OSS):** `FreeCastEntitlements` — e.g. max 1 receiver, cap outgoing long edge at 480 unless dev override, FEC/NACK off or dev-only.
- **Pro:** `ProCastEntitlements` in AAR — reads signed license / Play purchase cache, returns higher limits.
Call sites (minimal first pass):
| Area | Gate |
|------|------|
| `CastTuningEngine` / sender encode | `allowsResolution()` |
| Multi-receiver connect | `maxReceivers()` |
| Stream protection UI | `allowsStreamProtection()` |
| Developer resolution UI modes | Always allowed when `BuildConfig.DEBUG` or `AppPreferences` dev flags |
`CommercialFeatures` stays **developer toggles** for ads/IAP/Play review; **pro entitlement** is runtime product state from Billing + AAR.
## Module layout (Gradle)
```
cast-core/ # android library, published OSS
cast-pro/ # android library, proprietary, implementation project OR maven coords
app/ # application
```
`app/build.gradle`:
- `implementation project(':cast-core')`
- `implementation 'com.foxx.androidcast:cast-pro:…'` (release) or `implementation project(':cast-pro')` (internal)
- Optional `ossRelease` flavor: `implementation project(':cast-core')` only + `FreeCastEntitlements`
**ServiceLoader or manifest meta-data** to bind `CastEntitlements` implementation avoids `compileOnly` reflection hacks.
## Play Billing flow
1. User buys SKU (see `InAppPurchaseCoordinator.SKU_PREMIUM`).
2. Pro AAR verifies purchase token, caches entitlement locally (encrypted prefs or Play Library `BillingClient` + acknowledge).
3. `CastEntitlements` refreshes; UI shows unlocked resolutions / receivers.
4. Offline grace period (e.g. 7 days) documented in product terms.
## Build & release
- Version **cast-core** and **cast-pro** independently; app pins both in `gradle/libs.versions.toml`.
- CI: OSS job builds `cast-core` + F-Droid APK; private job signs `cast-pro` and release APK.
- Do not embed license secrets in OSS; pro AAR may contain obfuscated verification only.
## Migration steps (suggested order)
1. Extract `CastEntitlements` + `FreeCastEntitlements` in app (no module split yet).
2. Wire gates at encode + multi-receiver (behind dev “simulate free tier” toggle).
3. Create `:cast-core` library — move `network/`, `cast/`, shared settings (large refactor).
4. Add `:cast-pro` with `ProCastEntitlements` + Billing bridge.
5. Play Console product + privacy policy ([COMMERCIAL.md](COMMERCIAL.md)).
## Relation to sender resolution test
Developer **Sender resolution UI mode** ([`SenderResolutionUi`](../app/src/main/java/com/foxx/androidcast/sender/SenderResolutionUi.java)) is for UX experiments only. Production free tier should still enforce caps in `CastTuningEngine` via entitlements, not by hiding spinner entries alone.

271
PRO_AAR.pdf Normal file
View File

@@ -0,0 +1,271 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 11 0 R /F4 12 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 762.0394 66.58104 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 751.0394 136.381 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 740.0394 127.045 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 729.0394 101.253 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 717.4394 99.48504 727.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 707.0394 165.285 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 13 0 R /Fit ] /Rect [ 45.68504 696.0394 162.629 707.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
12 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R ] /Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
15 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 29 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Outlines 21 0 R /PageMode /UseNone /Pages 29 0 R /Type /Catalog
>>
endobj
20 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False
>>
endobj
21 0 obj
<<
/Count 7 /First 22 0 R /Last 28 0 R /Type /Outlines
>>
endobj
22 0 obj
<<
/Dest [ 13 0 R /Fit ] /Next 23 0 R /Parent 21 0 R /Title (Goals)
>>
endobj
23 0 obj
<<
/Dest [ 13 0 R /Fit ] /Next 24 0 R /Parent 21 0 R /Prev 22 0 R /Title (Entitlements API \(in core\))
>>
endobj
24 0 obj
<<
/Dest [ 13 0 R /Fit ] /Next 25 0 R /Parent 21 0 R /Prev 23 0 R /Title (Module layout \(Gradle\))
>>
endobj
25 0 obj
<<
/Dest [ 13 0 R /Fit ] /Next 26 0 R /Parent 21 0 R /Prev 24 0 R /Title (Play Billing flow)
>>
endobj
26 0 obj
<<
/Dest [ 13 0 R /Fit ] /Next 27 0 R /Parent 21 0 R /Prev 25 0 R /Title (Build & release)
>>
endobj
27 0 obj
<<
/Dest [ 13 0 R /Fit ] /Next 28 0 R /Parent 21 0 R /Prev 26 0 R /Title (Migration steps \(suggested order\))
>>
endobj
28 0 obj
<<
/Dest [ 13 0 R /Fit ] /Parent 21 0 R /Prev 27 0 R /Title (Relation to sender resolution test)
>>
endobj
29 0 obj
<<
/Count 6 /Kids [ 13 0 R 14 0 R 15 0 R 16 0 R 17 0 R 18 0 R ] /Type /Pages
>>
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 846
>>
stream
Gat=*gMY_1&:N^lk)lHKZa3a1O^:qb6:q;[T-@8NR)T`HWA1CCpDo_??OFeB(+eh?3HOo\XEP484]m(b,Cn)]gta3J!,i=FQi_11hn7@/&Uqr'RMeV:"kHksV5jkl_i-ZDC'dSFL0FnMe\Y;ll0f4(i\?Aq0;9!Z;!0!Ta9*P$bghC+i2pDbQmQbW%uUVX[[St'kOI!XAWp\g#4MqVhclO)S^*jGDSUT&mhR6E%[-`XcRINORBC(9NikZ][r'f]l%<s!=$Y@+]OM0+q_X/fEW5Hce?EUdb3=_c/JF-rin#OpLYO.RN"F&":0Z`u-8dNi9V=(MJHg..I7p++;pH!Qe/VL#.VQ:;MI0t=Y9C?@H-J4Zl8#QUb(srlikPh=MPLg`T,-^VLTTW+=$*ZTGohjMEXt/F#+P4PKGp9<BWbeCl#68]Mn+kno=JK]h!#C^lN*p\lm55MXQ9fS3(m_JY5R#1]a+H+-P+ur4uKk5_-=DWPPDHQ/%DiOWZ>onQSe1fOpW^(CN&SH]u'pX0$K"7M3&ahc!VK&oGWK5=bZ0\3Fi0.,P>5j)K'pXnVc`DHCRuPg[BImEpu[:_-<"Ug&'lU_muOM(%.h,LN'IbMlbNj%O^*[]53tT*QF;T6V5$>bC()C).`3j*i]J:B>O-bVHi#Ki;Q$7f+E(W<LSf,<]g=VFUJ%0hY9rj,NXD6YUG6NI=O8fH#7#-ar-qd@]0foMC^Cgl9i3N\IFX)@;H$T.thUZ9SH:PKW5ThJWM`p9j&bDPs+J7N][4hfD9Tl7Pe_F5mPRhbH+%;#23q[dWCoQ\2A'DTVZ5`ctc]rV@fAnNZ98Y\>GAo0_u#mOE_]~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 576
>>
stream
Gatn#?#/1K'Sc)J/)I=hmR!3"&)"Qf=BbtKb>M[OL1`$9=2i<-H"$`%-[#c$PCY/K?suXXo:0kQ$#jXRIo]c]?JtN`5Y)T)LmZ.053&f5r0iH%auEgDQLG_`.E8,FVXJ2enOuDD";^)J<uIAtA0),gc%Pd)-]Ht3[`"7tL6Mq[Zm^D@%D,)@gS!<ZDc$@"nc$?'7dUgQj_s'I"(!,9,8<<>_3S[A1!Zsk]PBkJN8>UB`GG&NX(g$\n4p0)7YjIc[7%a]\tdK=nrN/>=Zj4"2WH(D>O$]8@R57_)e5=[K=.u]&Tj/LTV!H`SPPpYlX;jC)H5Z:S2RtNH3B]b=%e%LEFig"$Qo#d;ZV9Tfk>]2@9p"kQqF0"HnCte2M4c/n@BnH8rL0U)[8lMKMIi<89&Xr\\*H1NpO9+HkS.'<8_nKk0&q(3.gFTq-efD+)U0jr;hi<fC-'!P&5!9Uf?D^p"&b:4aZOtHNp%G`Q*?'DY.6sO76*QphmRI0$mkMXt:+*[X,7)Za32jMM4kU?MB*:.?undO146Zs!1K9A5j&,YXF;KO(Be\MtA)$-@Z'A3D.s~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3360
>>
stream
Gatm>=``?/'n5n\^eCYcEb!SR-.u(Slt"nif8c3k75GsAKHqfg_$a(GVUPS%&)9QMUdQMc-ZLOU!,)8U!Z=DHO-0D,7tc8;,c@7C@[Q4r:(1@5al?HCom$,\O\ns/Nol?Ah'U%EX-pO^`4D^^XTV"h,5CI03uc^YL.*..@d'8Up/`W%e:Dr>,8Og/8L7TZ,j$P@ABkAKLsMiP$D2SeRB9R.O;F38q'oCJJ$=/`dNp3R-W?06SoUHY)L5+"^LMUHhgY&B:C2rBO@D'%BgCNg>#5nA%*6pTL[(gi8<V*^c-=f;jSIl:('S50#jCm%_SbYTjl7FnE#3h=j<G%"`C(>b5$d_5TL)E2CF'QlpoR1^VIg9[UK``G=E?%6hEg(76X:f$+L&58lg?gNcudHM\d=mhArPR9%K]/,>gqIo?tt]^FLqkGZ0%(k*Cnbb2KD8f2Z`ia,\[%DU&5AdA_q$2cZOI/be6j@\!9#n\"A1qg$@'b@lS9cjR'he>cRQ1&nE(6+$T5*TFKoFqc7>]^&=<\?/D,9:@:Y&]N?LcUrPl"T<`)ZNesPdI$2j$_.%^aI,W0n?7RdYm"Z#+Y,Kb_DCeAt",[)i>KoJ]Qej9h)6jOWb4/E86F;#XOG0@oiU!^/qRm6+KNuru6\j#^K:H1Ogio\/ij5utqI:-nT>hLgZD@J\A&=FonJK?2#TBsJ"G@&Z02J4CO3'ld0>\ISa<d%L@'c-8QtPPi'mg-qO;mYV`h]KJD!aZSOH6G%J;U&n'n/j^!'\80Osp##q79mGinD<-X5F4R"cb5'^,35%p([C:UnN)f8XcFU:#LFt/_T1MeWLHFP\Z1XKO1+`2$9$kMs*1sFE/fi$cjbod?UVePscPTHSgf$[k,'$RBC.eXm-DE`tF-uZY4G\3sh@jL-Mo0-rp`5h0lMFBNB7p\a(RIVXf:O/Tg4kK.iH9aL17F#;Y@20h48L`T]c7rWT-dpQLsG=&_3aK#ip"38;cTj&d%[J%lX2!"sQ+ioSH!?3erf;0!*heUQb>Gq`MladV)n9Y7":s&N/GpbUqmU`KZ'GCiTq'97dME23&JqYkkp&mLO?HbD(/<)*ZmMK1@'%/GgoVilfcKb`5U9KX#.Wi]V>Z$VCa<)>4L@k%*BNU$09gJDaf@ZegRH?Ys'/A38&/I6>fkM@[^iM;:=l2.f!3&SS@'-*rbT6<KN(7J,b=BfDmhb+5i^'.SG)fkjDK*>6t@#oE8MF/j>i9$GHO:)gHds\frSGmdgq*Ok(?H6APaJi#)&^-2c'!+H57491T1t[#EU-Q"[:P_XhZs6o+)Bc,]RZV^7oQ?VCA[2sJdSoDS6UF^O6+R]6,YPRJ,)/H#Z!Ng[ki8-#*!e,0pc%tH"oWNSZ'[u$VfO1a_<sltH;5C]W#Du4e@(n$q3jK&/]T'(20SZOLhN_Aa0q[$MQpu2KCQ3Y)Q$/&i7YoLq&e6er=gN[d70jAgY&Cu]3&U)p?l]V5Hum4_0Q\7)Z+-;R@3r,@;`Ja(3+OCIVtBZK5kLc$i!Dnc:uL3>i#@HI6XLXC$]&2XB_N)$qG6XR+WqK4MH^J=N?bcP/gQ27(CBsgF>hd3'n0U[BE2-q^!jtfI=(2QZsJJG@YHH=Ml0b)"duKJ?\C9`'G[,3(A5H01rF32FM8V],b@Jp7YsVaXq:2o'Ai;Y4i=Ol^S`;[&)1kiY]\/i`r2\$@)]*Lin/r]$?bBmWNV&=(cA0/lP?ZeM"cUmnMr0!au"=8[g27O/h,,8MauemIKQ&e)t=&E7%pB#"4_a<$4H)CsjP(<]P4BHAa\tre\DIO0k$0ZlOcUJ)>`>64S5h;)qh?*1fLfOM0$,TBdF<lYF'M7Jeuno\'[8PqN-l[24';?N@0C0CVrT!!_%]i$j.aRcE1NMM:>h@,Y<klEk!lO_JtLBl)8JXFd+%Ia&L8)Zm(nfI,r-\'qHW6)MeE)HB=)q/=ClB_/Z5AWsSgSmGdZSS".?T[/S2S2WAn5h?t=XZAl`Z&2P.VF!LLq5N?Y%Im]mW9Tp<W+)Vo'k30q/kta!XD[V.)Nq&8iH4QlG\F_dFB+X=2G`4b`>EbT/62"]8ub+*;hL=9E=LSpGiJ*Mn,DD]JW@maMSZS1Y,uPH/9i?U9s]UikilXiKm2DTM<+@O"h#=@pmd88$>e80f68m/8.sF;fadnj&BdM+JQASqgFX?ZF[9uN[k%0X^0Y#Sp1q0BO7q&.`YF!<<m[0Mg90E!i8%OIm!$/@bT+#@k'(h=cKC<JkHD'miA-;*ZdPhuVP`dogZlf`Jtb]!Lm44tYt&mu^MB7J%C1.?p;1->0h/omYM4)NfAN5ne8:Um>Mp<DcWk;2[goWaF/ZH'eEUk)GABPfp'r.MWJh'3T$\R-jdmrY@EllOfpncEMZ=M?JX:[WhG[0]/.S,mn*Nk/'a80S@!`6IcgG\ET&t?LCfjYn9@*&a?T*[7QY,rfL5dqU*;*<me'tcs:HNAQV4j#!#n+sXaNG*$Se)Ksd\_>n@o]$6mJB;F1lK&3$OnWcF*E+VF8^40cs*\6Z0CJc?"f.=;^3]jZdAld"?'n+l)PC`b(%e531i%KH;Qpom\=`LK:o/!`ET(6isCYqPo"&ts-b==XcbTOas=TofH(aE2e)b!W^0HGfkp]Nc":[FL&iPf4U,JfX4BNgd?pju(&*2CrcR*@dAN</>q]8<lP]?1=cBJFg\Dg]V$&6s7p-$XEcmG+:#1OL5d^m%&%KA(mRgVeeS`1iW"4g+\.be67:XofOFIg=KBD!&NN?L3&H@@Ag%pT#XDL&f$+r,@DU>gs;?/kd.hJX7HokDp'M0-853(m*BsutuH!,3G>#&hY3EK*OGn`11m:1dBq5a2,jSCF>9)6bFq/o=0X-=.FD9TuTQh0J`9^SW&Q0fNu?1Pp"kmD1'5'!E)f6;ti=omCJOi`$neb9R[NFoSTWhdA%WDl?>:6\:0&4V1'T-MtH"eX%@deC'%^C24UU7(YD2B,[E54.`U(gJFM9;XOCK3#Nd/<JZ'$??@dI>TVokY._adb<5e$;=#%`QA+udH1R:"d?MO/MBmG\s-0B[A-eY0Q^rgTBq)*,f5$e!=\YN<81'(#pVip2Wo53_2L!H$)OdOVP\'!#)^%qVX!.RQ8)+GBZ#0O5.36l[EXeZA[.)blMQC^cT4G(BIrbuL58'4/WFK@_SqSb`kpcE(uH['92E>pX7%-(#GEL\l:VA'p_"CVF>C-X>Q1-fO:s:!XWmM@$^.e'ikqs95#aE%9JBEsID-k8eLc3BM`P$J>"t)FGds?[I)s.VIj57)[]-9Cj-re\qUUD0@16Gir4O0;B4m8_\!ZL5=ELX<ArMHh<1]Og(o*&GJV0];h)L00r?'lic[#~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 439
>>
stream
Garo>b>,r/&4Q?lMRuOoEoT![DLkhT@K@d?=EG4>Bkue^-$Cd7pGck8I=Nm%o$#lL<uuGXDXhTt.c;qsBZ@jjA1R/2Kufi>bm'dBhH,FDMpo`XOX(8)a-4m7>hTM[YnuTE.Ki$f*mK<)^?W!fl8?47akR6*?o8@lP%rKWZtp4<13SK?"5-D,IW=[,HkcL:+1HL_ZfkeI,<b3Sd#OpY3c)iN/Tqlcl]mmW,:Zts?e9MmYk7dJ1NK4[+,()B+^#9u$s'sZiXPPAi1_E,%CD:FL?Da,:!k+c2%cp/50QJTZ@oJL8s")QJl(N4"/10Mkh!\"DJ[lPU"3/M(@''6:?8R7'in4&^4juNmsAk=ksR40Tp2=`>V4#>QWb!5RKQ8McBB*3B>#AY^Sg*d=PWqIPM&@aK^glj&O\204H8&\GqhdEcSubh"1#2:E<~>endstream
endobj
xref
0 36
0000000000 65535 f
0000000061 00000 n
0000000124 00000 n
0000000231 00000 n
0000000343 00000 n
0000000491 00000 n
0000000638 00000 n
0000000785 00000 n
0000000932 00000 n
0000001080 00000 n
0000001227 00000 n
0000001375 00000 n
0000001491 00000 n
0000001597 00000 n
0000001803 00000 n
0000002064 00000 n
0000002270 00000 n
0000002476 00000 n
0000002682 00000 n
0000002888 00000 n
0000002975 00000 n
0000003294 00000 n
0000003368 00000 n
0000003455 00000 n
0000003578 00000 n
0000003697 00000 n
0000003809 00000 n
0000003919 00000 n
0000004049 00000 n
0000004165 00000 n
0000004261 00000 n
0000005198 00000 n
0000005865 00000 n
0000006093 00000 n
0000006321 00000 n
0000009773 00000 n
trailer
<<
/ID
[<a4eb65ee35e6b29ba7d315a4876585ed><a4eb65ee35e6b29ba7d315a4876585ed>]
% ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R
/Root 19 0 R
/Size 36
>>
startxref
10303
%%EOF

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# ac-docs
AndroidCast documentation (extracted from monolith `docs/`).
- **Remote:** `git://f0xx.org/ac/ac-docs`
- **Build PDFs:** `./build-pdf.sh` (requires pandoc/xelatex per `_pdf_build/`)
- **SPEC:** [specs/20260618_repos_reorganizing.md](specs/20260618_repos_reorganizing.md) Step 2

327
README.pdf Normal file
View File

@@ -0,0 +1,327 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 15 0 R /F4 16 0 R /F5 17 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 762.0394 95.92504 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 751.0394 95.03704 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 740.0394 99.91704 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 729.0394 74.14104 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 718.6019 142.6425 728.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 708.6019 132.21 718.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 698.6019 106.38 708.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 688.6019 107.2125 698.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 59.68504 678.6019 125.97 688.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 668.0394 102.165 679.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 18 0 R /Fit ] /Rect [ 45.68504 657.0394 109.701 668.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
17 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
18 0 obj
<<
/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R ] /Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
20 0 obj
<<
/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
22 0 obj
<<
/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
23 0 obj
<<
/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 38 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
24 0 obj
<<
/Outlines 26 0 R /PageMode /UseNone /Pages 38 0 R /Type /Catalog
>>
endobj
25 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False
>>
endobj
26 0 obj
<<
/Count 12 /First 27 0 R /Last 37 0 R /Type /Outlines
>>
endobj
27 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 28 0 R /Parent 26 0 R /Title (All documents)
>>
endobj
28 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 29 0 R /Parent 26 0 R /Prev 27 0 R /Title (Specifications)
>>
endobj
29 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 30 0 R /Parent 26 0 R /Prev 28 0 R /Title (Design reviews)
>>
endobj
30 0 obj
<<
/Count 5 /Dest [ 18 0 R /Fit ] /First 31 0 R /Last 35 0 R /Next 36 0 R /Parent 26 0 R
/Prev 29 0 R /Title (By topic)
>>
endobj
31 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 32 0 R /Parent 30 0 R /Title (Infrastructure and deploy)
>>
endobj
32 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 33 0 R /Parent 30 0 R /Prev 31 0 R /Title (Development process)
>>
endobj
33 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 34 0 R /Parent 30 0 R /Prev 32 0 R /Title (Alpha and QA)
>>
endobj
34 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 35 0 R /Parent 30 0 R /Prev 33 0 R /Title (Backend apps)
>>
endobj
35 0 obj
<<
/Dest [ 18 0 R /Fit ] /Parent 30 0 R /Prev 34 0 R /Title (Product and codecs)
>>
endobj
36 0 obj
<<
/Dest [ 18 0 R /Fit ] /Next 37 0 R /Parent 26 0 R /Prev 30 0 R /Title (Agent bootstrap)
>>
endobj
37 0 obj
<<
/Dest [ 18 0 R /Fit ] /Parent 26 0 R /Prev 36 0 R /Title (Maintaining TOCs)
>>
endobj
38 0 obj
<<
/Count 6 /Kids [ 18 0 R 19 0 R 20 0 R 21 0 R 22 0 R 23 0 R ] /Type /Pages
>>
endobj
39 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 836
>>
stream
Gat=*9on!^&A@Zck"&Rjlg\V.Uq:H>MB3P-SpH6Lfk*[9D$XtRpN6N\+,,e!$`r!0Dk>#0nK:Z_YkVQo!+.m6HpL"%hAT3%!T"f.hWW8k:Fh,Eb)oYRes]G'$B)7>KK>&b%YVHOC'TQ\bS:EJgW4Epfo-+gZ6gE]bY9dW)aMn@CWc%W(Vk86#AFeTnDnLDpr/VT+Be(U'5)b0-8C0n@/bTUmiB%GQo)W[l`4lhoI;,ldOQcX$DH:bol;7;MD:H8I7!OV3K.=n,_F!OqYXhmU7bK7@:!]c**nr802R]%U323"loa1ic8^T!Bpi8DO=BJr$)rLKKB:<]\VAL)CIg`>T\Yq_HCNU$LKNHr<VQ!?gV!G:7s+><(PRE=`!#u;QInKP4A&cc[H!.0pRm<lpD?f%fj'R`PXkpGFV"(>J$Up_juPO6muFMHVh[R>)k??%hauO306^75REYc][3,;#K@sPFH/SDd;hKF*p>OT"R+oLHi5Q8+HE\?[;KL':JY0?2p4D/["rLod6Ic3=LMf!%%J)]]7nF8p_pshX%(F`%"(Je#PDNX7\*7m6V/GIC!IXB3Q=b'`0K\'u2k2rb6+Dh64G8>M[*3iek]#Ml'*htDAomK+1nPeW#CGG*<+%t7puilL0RjpP-B7uK"mYRMpHBc2dmDbLUt%-4fhqu;c`_ds]h8+`#+!4BX\[<`0Wcq./\+34<t4gO<fuL?6%+Vk.(94t.u\@2FG3O#KW5TXJIiQJ2\<X!X!t);T[:`sb&QU*Ej)[ul843P.!2]+[mUU.@4GsbXbY@OlL],J@R5$/d'9R:8Knb;U6HWB4@1hM_<;tifm$6~>endstream
endobj
40 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 614
>>
stream
Gatn$;/_pX'SYH9/'_Y6#8\><@;j8N8s<;X?$Y4`CRg'3fZZ=WqX8'1>&A+5GnI?t#[dAPot<D`8A_%[?1!23L!(tRb##3+1QnI.MH)P225A45N0Cp48Hp=@8^]@%hh>X)'J/>p86Y/4-pRoJAObW:Zg^ceMP1?VI:09(K<]uH;9A]9_lf]7n_3d;"0.ul4q?r60M_00j:$M6N2J)D%-^<T[k2?q%f8e;<_c)tla1EXNKV#\-3g"V;k]PhD`.ep5;`3k#0kGmpIY#:$/TibMLa.TLXHTin/&5E*nOoZ"nEJ9G-^l1T_uW?85l;7?#-&VUa4D]=m?7Jg2?+7FNuAl7hd+"V\Lq)]<*'^iH?8.0;bR!UtY(KQ<b;jq\b.[0D^[7Fm+H4HY$up@Pm0AA>Z[N.XuB=&,dog#WCjEZ*._KI(1*$61sVd9]En[;N]<,IQAT2;fN3Ol?GY=SDrr6:>JQ_;MaMa$nhfpZj3N$-[]:GQe*anYOR6"JR5Cn.FC%cfAk8i*?'R"jV7t9e#nJ`8rrtuFk6Amk-["N%5h0jl[&+S;'Ibf7?4kKZd@<P.nP1P$3-+5;aG(/p7?9@.4rP=/f64's2Dh@2fRZUg&~>endstream
endobj
41 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
42 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 322
>>
stream
GarnQ4`>s,'\rL!2j,Y2Gf7VIg8H)\3`Pd>,m?5I#W;ibrdKEq;-TkXak$(>48Fnu%%uk//)XH<EDN5Aq@1@\LYgra8+.%\Q!COeM1e5G`Pcf9LV876-G%l#K*,CQ;^S0<a\kinanW)+]KY#]`""0\8d#Z3)`N`Qc5)/n`T4j]2mFrp>udZ/iF!\C_4^:C<uDgYN?;t]lsAjWG>7Cm3HhPjjkG<`>uMNQGGlJP@85WsB5@d#KYA>a4:eq?8MHQO3C`j8O>8*qqA(d,M.D@UpK%llfIN8YHi29O=@u%T.\qY&+uM4mF"EFOqC/.7[9I6g~>endstream
endobj
43 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3687
>>
stream
Gb!;&CNJ3#np>gSJ%BZD!Gb.kJp\4S4'Wn9^r^GYX&QAA5T:lOH=/hLrd#h$&dH%T>BO#MfGT#!k=e+;Rt-#R,l2@q!I>-`]d#cj/65$3Q3<*XBT!CNI3=<9;S+6!g4BHbU9k'./)g46N*dh:/j`sN"\STWDsqAH?:@Zk]J\KC[LGm]%(FFer/\)W',J?&.M);qU*Opo5A>`q#Ur(lTgQf-#VE;3$VCY-8t0uMJp9L>pbbn\Oi;3r&h684Lu:!irJ!<-cZ,!s>h[Ya&5$p@p\6=3.E3o2FYH`lN*5Ao^eDB-6Hlf(;gZl)jY1&m@2AHGN>5m9XMZUOb8MnJHs)4BbiH(L$8@[&%^09+]6,g:j^QH^`A:kS=Y.!(+a#>,KOP=4lPNGI[\$gFI-kD/M04bI9@tlS>t+a[JeGV(qT]t:kf[Z+(Q[E1l\R'7`!M/N7rU7DGi#K]Q\r-r$Z[i[o5M<!]XK]Pl0n6=Os?%;/O>Pq8Q#S[OXs=WVL?p*F%r:Q5O1DWh7:7.qmmkjeClNJ#[Eqs`+^jWeo6pg\uco*0MI]+5!7YK^V<34DHE.K5b/CW5!m+clRFF+K.,@$JC:6;C(m#?m,llI7_2;=$oZ@qmCneu^H-(g;hLA$]boVSEn9_Al="Q*JS,+OleE[SW!^H!D%,'OIaHjeHhR(d<rl:%WIU#3Ni&FKI6&<BB#DSaS!Ofc8FSd5mP0DmZ5Lm=n,qgH5qU]pJiD?8L(b.k(qW)=Kbes.<:72$;(58MI:cdfg_Y`kIKO&Mm#5=NTFN0q<_!ZRK/WTblt^Y_O/QqV/'fWAPNl+o)YhP2>:,!)Z"q/3YcVJY$G[12+@MX\GE8[Y[^XlsWb:]:Du$]fY#8Ktq-p5#@t!i+T'?E[pNU%;mMZf.Aq5V*)*MSJ@2Ws,i:!&$.p#6@-Pm@-K9d1K18-$AgI)T]nUd,)(#9U/*mNu&B`s1XXoV>+!\`mX*et3mgX*<>kh-iC5=I+^"h@3XPa)E4q3">a;GST<d#lLZm4hrYX=a$<_e/c9=iWTATOZ]A;q*PO>ps4n:4SirJS@f$r\/r;%qEM`N'XWJM^&mf*iuH9!=-Xf*?9]?2NOpsQd=i$_cRg=F=g'eF;*om=RIcu`@.$rW2^*+aBb+\:CZBIK3L(CqE1hK)#MDnn=YAOXdu?$=uB^0<^pa-\abb^%n^gUQqu3bj^bDe@D6`p!(Q6Tn6P&ZQlBZ:YVGG58;L/\h:,0?'s;ka;nqQ-O6UDgpd9Nr+gq,n3;H`QcGDR8mYIq$J2nRd0cA:Si?9Cco<7qMq?]<;Zc"hZa5:ARZh?L.>!$>#%a-eGNUZKu*"C8Y:IgU5b)'>n#,]C):kV#Y2%3W'T$L(GC'1%pbN<i6Y2d-]c7R_pG&o;]W?+S+POVbEonVh2P^#&6%+q0)mZ)S;[1T'=2jQY__GHdii3([T^<&q=R+7h.R#94sl"%6LB'#hg:.9;1kABO>(nNk-ha!"+0pXrfh!Hd9I5]HLc8%W3+P!75rG\IfN@*Q+M06aDrp'/=]QP76.uCS4ZAk$no>#?m*bQJd)OtYjqUiTAC,<at:V:\"Q1^#03&KthnWKrIG,MX'B'Pr`<RpiXmKOU=C-&5>]+'KMI:qs'TUb;Ie*UY=iGKe_k&]^-mhmXn1%=%gQHtbY/W-#JXOOCFFSPNH<\B]h5X+krAS=PY<IcqPk$W,"i#Z10lb]39[!Xt6Ppb:OZf[<5#E6jiStW&>@#%SfF#O=e*?G@@hlQBSJ6:qs+Bom_[BDW'Luf=L*Ig;bo'!"i#%+0Y-KaP]!s6]dedTo`UOX:!H5N2)1LE7B^&$pO/_'T_\W2GnqRN,`2<aBD>2JaupcS9>T^.t`nGu/[0P2AliC,@oW1;sCVN5eH51R%?khe)S(oW@uWC@n;]?n<`mhgC?Vu8TB*Xf!=LQ3fIqARmZWE,Ulh9&k8LA=`!e\[8=)ihgPY`1S#$t(_FYXEZ9'cpO@S`>LE!q0UnN+-WHHEG;OX]%6\REd.jWg8RXeZkS\NLH`&.K1nes$q\BP/AKr[K>A?@oJU2HBsP)iAsak\T1.qe&IT*VQ5+rM!+VR4=um]<@@Fpo%8A(GY?[&YdQV4%r$#!fWLagA-JuXl`--f=\e`!rQu2ScjCTs'gE\TcJ\.pH*U#,1T&hn*K8N_.8d*T+/2W,.OBOmn1Y*+StY.f2QS_ea5W2Zl=?KAq<s7-rQ'JcWQN.gf`.Ztk'@b>CJ_8iV%*?43RWG)[%2cB#(^3#U95suf">Jh.qE'S>$%!cj1M:>*fPl#?_&iBVJqf]8o-X-ghNI3'*I:6n/,2k-lFee$lDbg1!1Gqca0!D1KL@OD>FtTc1+3:=/U-<nF@olN:!-;%/Mp^qt!1U6PZRpi;E'1r^3^q01jcAHYN]RV%*#%,btH-k$oRB0t$*M,2VufKB*\$hV(_WE1)5E.0IFu!hpqB_IWc]3Z/J]O!*;B:4`0q!Hi&[KZaL_AY,cdVci.rMKtY%Su.mQ=lsFUH(&<9VO/`6k"1GUP%C5\R-1$t1^KESW%DXI\4f.ck"P8U!YR<Ci6)6k1>:[0"=.Nb_3[Yg2ku?"8Jaug(!9)]E^gZ38It3I)VQPjL`%Lph3;c_VG2o8Q1=pJZl@m[Y(U>*4R*Gi2`$k'&\CY.;2B8(TC4[/Xfe#nQg#)gES^l*>16YXAe^Yj#@9gH4V904GIc@mc43KPlp;L`#Q.b4\l;M,fp8e$qcCDXeH6X'Lk%dDkb9,E[dNb]@LQUMV0H/BZ060cJqQVAL5T4?=MOOd^ENETQ14Y(EkV/!'+o^.^a)+dRL5L)20D6.U@\?i0*_ElOF#1-j9O1PIQ[S`]iMVXBb@0fbAkpTAV8LtLm!.."%L[K6UeIALn7Kemai5d/^dKoPR@?>jnYO-hmtr;;rG,;ci7PF?Cpj>RaM!Sos:L[opU?<3ppAJZdnC4?h1PRa^tEejBn8<>Jdu^@BaHrDhi*Z4kFQrHFAa(lLOmfp$05f6^?Q>P</C1Te)@LchBB0kCs"AlAW42rI:c_T?ZPk+XR=H+]LC]<]eoA"g=QKMsQ(DMJg!uAl:e!T3q6(MfJD3Wp9Hn*I&Rs^-b;dkrSNBoVMi-8#%jM>J#G$^MdpnpIG3uG)`"<P.796%(1u-0fq'/lY#HI>rca>]8gZ2_5:B<@r2C"XM(7,GW12mn)rZIK:f_XH1"Jj^gM?"Gt(uun'-$)9q.L3;0$<tU-roh/h2GD&3n5oU62c/DZW/KL(2d=lZcA^RUO)Ajga._5N#Id(RQbGJ6f9+EtbcghrP"-s0=IE0RYh#,Fs"YNEP,g*0+D:V];QJ05kM3abG[E?LNkt'J8qAJNOO<nhhU3ZoV).SKjCPrub$Ja<qMs^^JD#)MiS@iofnGFG\YE\O3'X9@0%.IdT9pH6Mbg\/-<G!ls/6Hl;@ZgRa`';3U]de-s;(n3eQ+Q#m"5]ERI<$8eTI=ra,.\&=6&`_ha0-o$&TTF?$Mb12gpNs`FU]43Qo]UuSJNifCM+.a7e+-^1>$\f?_'bA`$L*[a.g3G<C[4\?/QFM>q:cq<u'KFLUM.EE6e@8!qY$;jek`ct-kSsh]\IL&.-U,]::m-:\W2C\*/C;tRR3,fdFh@kB=m+iI?S+\j7$6I+mjZqN#brV,AG[e[gS3-5R]ur/h&ep1aEbI5UX%p/*Vf,2WMc%,~>endstream
endobj
44 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1496
>>
stream
GatUt@;hW+&;Iso=,^,7amCCA`>ER/&Xt%HSX9CPY>V/2J[NbAB'J_EfA*REJ_25B*H`=E9!FY9H2Pt3b9R3aI$*Q8[K\]_TR0:]=HLaXfS0jW"G5^j[s)Ape*\08f/IPc4^selJW8:?BtTP7p[X8,,\nN/I<G;6JjI((DpFgp(=;SQeM;icbi-_E!6IHeI&t4jiG2dKa#3^1W81aNa$a>$0kcR82=tLo0S.LYEF&&]iONm.F1AJ+(Zp=Ss&]qA(5qaNcf]8r\ZouF&PQfL@Q,AcaSP<\&`-8Pj/N'h`LHXh+F/:BANEVfl2dna/[^[d]sm1d,gG`oWe(hZ7OU#O1bZbfO3_MSK[V=UU/lh=UiHd2C;nY,K>SZKRY>Gm>Z*d+[ff.1?r@"XD6kp3@-Rod/3!b#:hc*_n8j]01n+99$U8IL<.R&-')mKh6EsLB`UaS)k:H1"rC`d>9?PeJ#tYpWXd8^8UfDE\<=@D095#(uaP>s<]bbA&d=OLrCI;^%Q=DtQKf*rm6ubJ%XJ\\[MT^)fWSFT,WH`+P"C#r$SY'Ieb&(`8G&KVVXr?0BT8s4a/'H5Sm4TuUaf]1fdrYkh4h;4U.[OBg<\i'Cc_:`QWt:'&E!IK'M0;biSO8<U'`BrV-u*mjI!*bU5]\O9gBRPUi2+pImofMq7E<nE_=LBu)S==?Nd>ID,R7PrE-cL97)Q8oBn]-"$!eDFTa$$mY^qJTPktBCWM0lQT<[HXKgp.W#7'#'/3]cjUPgiIqJjr(e12pLSig&TFqY<#mpMp[YNaT]T8\u;oEq5"5obr`QS"DN%BNg3A]E<&<%kpM"403`#p*<OFu/.+VG8_S=l''Mms1&#Q?:cLJ_B,F$T)Mc64H-Ai7=NTZ^@KY"Fnm9e8aOscNTh>)qg3$`kFfJ"ct<=G./rs:7XC.__l;?7bb\O8(nD*R\,Z[fWo\6?[r&`c<SlP]]EBiKhj$)(ucH7)f)'@k3FeuF+o(0U</XWI963JO\?eS!U#33">4^A5%u^<c>2>3f<n?^(gik(:U@4@8)]=Y*mfR5%ScT`.F(W[AXuVgkmb(:gKSf.c:.,cJer]u8u:Rop:tscCmG*7j+O"!VI*Nl>CjJt"L2)a%C`dZr\MNB1ORN!,aESooolnQ(YL/qR>iR=3XWaGoICOpm2<m'oEUF%?n*rh^4b6j]7pou3!cU,nBZ^0,d)j&GZ!IjL^)&0_HTLU29Dn9B\R.2iA]N\,"3.B_4_d1ktib&<o<E?#Q&WS`%7r:$Wn/*SV2u5oqZuL\a9kc6?7&se\d9</nnVoeV2"s1go(PH*`Ak+BY&7lh<'$k5G1D*Rjt4b>3Jni?\99VUTPc)kCZ?^`T7XfTkc%.@,ag87*eVZ7?HU+!.>QAh*(F%r050*r]FqMFu"YU)qNV`K##2a^`:GUO)]dBJ0n11G*rT+,4PEi]?T"prLs6nU]Y8s+g*&[,5p6ijg+<QMZ>[%<]]W*M"9+i8+F`0DQg~>endstream
endobj
xref
0 45
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000502 00000 n
0000000650 00000 n
0000000798 00000 n
0000000946 00000 n
0000001094 00000 n
0000001240 00000 n
0000001387 00000 n
0000001536 00000 n
0000001683 00000 n
0000001831 00000 n
0000001979 00000 n
0000002095 00000 n
0000002201 00000 n
0000002279 00000 n
0000002485 00000 n
0000002777 00000 n
0000002983 00000 n
0000003189 00000 n
0000003395 00000 n
0000003601 00000 n
0000003688 00000 n
0000004000 00000 n
0000004075 00000 n
0000004170 00000 n
0000004279 00000 n
0000004388 00000 n
0000004530 00000 n
0000004637 00000 n
0000004751 00000 n
0000004858 00000 n
0000004965 00000 n
0000005065 00000 n
0000005175 00000 n
0000005273 00000 n
0000005369 00000 n
0000006296 00000 n
0000007001 00000 n
0000007229 00000 n
0000007642 00000 n
0000011421 00000 n
trailer
<<
/ID
[<21bd418253115595219f2239810885ec><21bd418253115595219f2239810885ec>]
% ReportLab generated PDF document -- digest (opensource)
/Info 25 0 R
/Root 24 0 R
/Size 45
>>
startxref
13009
%%EOF

248
REMOTE_ACCESS_IMPL.md Normal file
View File

@@ -0,0 +1,248 @@
# Remote access — implementation notes (v1 WireGuard)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-03 |
| Last modification date | 2026-06-03 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
See the full proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md).
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [App (developer settings)](#app-developer-settings)
- [Android VPN consent (WireGuard)](#android-vpn-consent-wireguard)
- [Production deploy (no new nginx HTTP paths)](#production-deploy-no-new-nginx-http-paths)
- [1. Database](#1-database)
- [2. config.php — required keys](#2-configphp-required-keys)
- [3. BE packages and permissions](#3-be-packages-and-permissions)
- [4. Cron (session cleanup)](#4-cron-session-cleanup)
- [5. Verify](#5-verify)
- [6. Browser](#6-browser)
- [WireGuard data plane (UDP, not nginx)](#wireguard-data-plane-udp-not-nginx)
- [VPN process (AIDL)](#vpn-process-aidl)
- [WireGuard third-party (Android)](#wireguard-third-party-android)
- [RSSH alpha (reverse SSH)](#rssh-alpha-reverse-ssh)
- [RA control HTTP (:ra_control)](#ra-control-http-racontrol)
- [Tests](#tests)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## App (developer settings)
- Dropdown: **Disabled** | **WireGuard** (lab) | **RSSH** (alpha — outbound reverse SSH, no VPN)
- **VPN routing (WireGuard only):** route scope **Hub only** (`172.200.2.1/32`, split tunnel) or **All traffic** (`0.0.0.0/0`); app scope **All apps** or **This app only** (`IncludedApplications` / `VpnService.addAllowedApplication`). RSSH ignores these. Prefs sent on heartbeat as `vpn_route_scope` / `vpn_app_scope`; changing them reconnects an active WG session.
- Pref key: `dev_remote_access_mode` (`AppPreferences`)
- **Disabled:** notifies BE (`status:disable`), tears down VPN, stops `RemoteAccessService`
- **WireGuard:** foreground service; jittered poll **17 min**`heartbeat.php` with `type: ra`
- Heartbeat URL derived from crash upload URL (`…/api/heartbeat.php`)
- Session credentials persisted until `expires_at`; expiry tears down tunnel locally
- Userspace WG via `third-party/wireguard-android` (`:tunnel` / `GoBackend`) in isolated process **`com.foxx.androidcast:vpn`**; main app controls via AIDL (`IVpnService` / `VpnServiceClient`); TUN fallback only if GoBackend fails
### Android VPN consent (WireGuard)
WireGuard on Android **must** use `VpnService` (see `DeveloperSettingsActivity``VpnService.prepare()`). This is **not avoidable** on stock devices:
| UX | WireGuard (v1, lab) | RSSH (**alpha essential**) |
|----|----------------|----------------|
| One-time system dialog | Yes — “Allow VPN?” | No VPN permission |
| Status-bar key icon | Yes while tunnel up | No |
| Full-device routing | TUN interface (split routes in config, still a VPN) | No L3 tunnel |
| Outbound from phone | UDP to BE `:45340` | HTTPS poll + outbound SSH to bastion |
There is no supported “hidden VPN” on non-root Android: any TUN creator is classified as a VPN app. **RSSH** (foreground service + existing HTTPS poll + outbound SSH reverse tunnel) is the **alpha deliverable** agreed by project owners — device initiates, operator reaches a forwarded port on the bastion, without VPN consent or status-bar key. See [Rev. 3 Q&A](20260602_REVERSE_SSH_proposals_summary.md#rev-3--wg-vs-ssh-qa--bastion-deep-dive) and [ROADMAP.md § Remote access](ROADMAP.md#remote-access-alpha--rssh).
Uses the **existing crashes vhost** — no new nginx `location` blocks for HTTP.
| Piece | Path |
|-------|------|
| Migration | `sql/migrations/007_remote_access.sql` |
| Repository | `src/RemoteAccessRepository.php` |
| WG peers | `src/WireGuardPeerProvisioner.php` (`wg set wg0 peer …`) |
| Device API | `public/api/heartbeat.php` (`type: ra`) |
| Admin API | `public/api/remote_access.php` |
| UI | `?view=remote_access` (nav + hub card) |
| RBAC | `remote_access_view`, `remote_access_operate`, `remote_access_admin` |
| RBAC admin UI | `?view=rbac` — global roles (root), company roles, privilege sets |
| Cron | `scripts/purge_remote_access.php` (hourly recommended) |
**RBAC rules (enforced in API)**
| Permission | Scope |
|------------|--------|
| `remote_access_view` | List devices/sessions/events in allowed companies |
| `remote_access_operate` | Open session; close **own** sessions |
| `remote_access_admin` | Whitelist devices; close **any** session in company |
| Global admin / root | All companies; root edits global roles via `?view=rbac` |
Privilege sets (`remote_access_user`, `remote_access_admin`) on `company_memberships.permissions_json` grant extra actions to `member` rows — see crash console README § RBAC.
**Flow**
1. Device polls with `{type:ra, status:enable, capabilities:[wireguard], tunnel_mode:wireguard}`.
2. BE upserts `remote_access_devices`; returns `wait` until device is **whitelisted** and operator **opens session**.
3. Next eligible poll returns `action:connect` + ephemeral WG credentials; BE runs `wg set` when `provision_peers=true`.
4. Disable from app → `status:disable` → closes sessions, removes peers.
**URLs (production)**
- Dashboard: `https://apps.f0xx.org/app/androidcast_project/crashes/?view=remote_access`
- Heartbeat: `…/crashes/api/heartbeat.php`
- Admin API: `…/crashes/api/remote_access.php`
## Production deploy (no new nginx HTTP paths)
After syncing PHP/JS to the BE tree under
`…/androidcast_project/android_cast/examples/crash_reporter/backend/`:
### 1. Database
Migration **007** (already applied on prod per deploy notes). Tables auto-create on SQLite dev; MariaDB should use the migration file once as root.
### 2. `config.php` — required keys
```php
'remote_access' => [
'wg_endpoint' => 'ra.apps.f0xx.org:45340', // public UDP hostname:port (FE DNAT → BE)
'wg_server_public_key' => '…', // wg show wg0 public-key on BE — REQUIRED
'wg_interface' => 'wg0',
'provision_peers' => true, // wg set on connect/close
'require_wg_tools' => true, // reject connect if wg genkey missing
'session_ttl_s' => 3600,
'min_poll_interval_s' => 45, // poll rate limit per device
],
```
**Critical:** `wg_server_public_key` must match the live `wg0` interface. Empty value makes connect credentials unusable on real devices.
### 3. BE packages and permissions
```sh
apk add wireguard-tools wireguard-tools-wg
# wg0 configured separately (Address 172.200.2.1/16, ListenPort 45340, etc.)
# PHP-FPM user must run wg when provision_peers=true (same as CLI test):
sudo -u www-data wg show wg0
```
### 4. Cron (session cleanup)
```cron
0 * * * * cd /var/www/.../backend && php81 scripts/purge_remote_access.php --hours=24
# Orphan wg peers are pruned each run; skip with --no-prune-wg. Manual: php81 scripts/sync_wg_peers.php --dry-run
```
Use `php81` when Alpine default `php` is 8.5+ without the session module; FPM is `php-fpm81`.
### 5. Verify
```bash
cd examples/crash_reporter/backend
chmod +x scripts/ra_*.sh scripts/verify_remote_access_prod.sh scripts/test_remote_access_api.sh
./scripts/verify_remote_access_prod.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/test_remote_access_api.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes ./scripts/ra_e2e_cli.sh
```
**Linux CLI device simulator** (same heartbeat payloads as the app): [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md).
### 6. Browser
Hard-refresh after deploying `public/assets/js/remote_access.js`.
## WireGuard data plane (UDP, not nginx)
HTTP control plane stays on FE→BE `:80`. **WireGuard traffic is UDP** — configure at the **FE firewall/DNAT**, not in nginx:
```text
Internet UDP :45340 → FE DNAT → BE wg0 :45340
```
Hostname `ra.apps.f0xx.org` (or similar) should resolve to the FE public IP. Devices use `wg_endpoint` from config/connect payload.
## VPN process (AIDL)
| Component | Process | Role |
|-----------|---------|------|
| `VpnServiceClient` | main (`com.foxx.androidcast`) | Binds to `IVpnService`; async apply/tearDown + state callbacks |
| `AndroidCastVpnService` | `:vpn` | AIDL stub; worker thread for tunnel ops |
| `WireGuardVpnEngine` | `:vpn` | `GoBackend` + TUN fallback |
| `RemoteAccessVpnService` | `:vpn` | `VpnService` TUN when GoBackend fails |
| `GoBackend$VpnService` | `:vpn` | WireGuard tunnel library VPN entry (manifest merge) |
Developer settings still uses `VpnService.prepare(Activity)` for the one-time consent dialog (same app UID).
**Crash capture:** `CrashReporter.install()` registers uncaught handlers in `:vpn` (scenario `vpn_service`, process `com.foxx.androidcast:vpn`). Reports persist to shared app storage and upload via `:crashwatcher`. Lab: `scripts/simulate-vpn-crashes.sh` (debug APK + `MODE=device`, or `MODE=upload` for BE ingest validation).
## WireGuard third-party (Android)
```bash
bash scripts/init-third-party-submodules.sh # or ./rebuild.sh (includes init + :tunnel build)
./gradlew :tunnel:assembleDebug :app:assembleDebug
```
Gradle fails fast if the submodule is missing. VPN runs in `:vpn` (`AndroidCastVpnService` + AIDL); `WireGuardVpnEngine` uses `GoBackend` with TUN fallback in the same process.
## RSSH alpha (reverse SSH)
| Layer | Component | Notes |
|-------|-----------|--------|
| **App** | `ReverseSshTunnelBridge` | JSch outbound `-R 127.0.0.1:<port>:127.0.0.1:8022` |
| **App** | `RsshLocalSshServer` | Apache MINA SSHD on `127.0.0.1:8022` |
| **BE** | `RsshSessionProvisioner` / `RsshBastionProvisioner` | DB creds; optional Linux user via `rssh_bastion_user.sh` |
| **FE** | nginx `stream` | `nginx/rssh-bastion-stream.conf.example` |
| **Sim** | `examples/rssh/linux-sim/` | Laptop heartbeat + `ssh -R` |
Developer settings → **RSSH** (no VPN dialog). Operator command shown in admin active sessions.
## RA control HTTP (`:ra_control`)
Isolated process: template web UI + sandbox shell when RA session is up. Port/token in `adb.json` `ra_control` block.
## Tests
```bash
./gradlew :app:testDebugUnitTest --tests 'com.foxx.androidcast.remoteaccess.*'
bash examples/crash_reporter/backend/scripts/test_rssh_unit.sh
bash examples/crash_reporter/backend/scripts/test_rssh_api.sh
bash examples/crash_reporter/backend/scripts/test_remote_access_api.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes bash examples/crash_reporter/backend/scripts/test_remote_access_api.sh
BASE=https://apps.f0xx.org/app/androidcast_project/crashes bash examples/crash_reporter/backend/scripts/ra_e2e_cli.sh
```
CLI validation guide: [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md).

395
REMOTE_ACCESS_IMPL.pdf Normal file
View File

@@ -0,0 +1,395 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 19 0 R /F4 20 0 R /F5 21 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 762.0394 132.389 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 751.6019 174.72 761.6019 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 741.0394 209.309 752.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 730.6019 100.125 740.6019 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 720.6019 159.315 730.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 710.6019 168.9 720.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 700.6019 145.1325 710.6019 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 690.6019 87.19504 700.6019 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 59.68504 680.6019 95.52754 690.6019 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 670.0394 185.293 681.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 659.0394 117.701 670.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 648.0394 157.709 659.0394 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 637.0394 142.597 648.0394 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 626.0394 151.485 637.0394 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 22 0 R /Fit ] /Rect [ 45.68504 615.0394 65.24504 626.0394 ] /Subtype /Link /Type /Annot
>>
endobj
19 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
21 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
22 0 obj
<<
/Contents 48 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
23 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R 18 0 R ] /Contents 49 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
24 0 obj
<<
/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
25 0 obj
<<
/Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
26 0 obj
<<
/Contents 52 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
27 0 obj
<<
/Contents 53 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
28 0 obj
<<
/Contents 54 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 47 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
29 0 obj
<<
/Outlines 31 0 R /PageMode /UseNone /Pages 47 0 R /Type /Catalog
>>
endobj
30 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False
>>
endobj
31 0 obj
<<
/Count 17 /First 32 0 R /Last 46 0 R /Type /Outlines
>>
endobj
32 0 obj
<<
/Count 1 /Dest [ 22 0 R /Fit ] /First 33 0 R /Last 33 0 R /Next 34 0 R /Parent 31 0 R
/Title (App \(developer settings\))
>>
endobj
33 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 32 0 R /Title (Android VPN consent \(WireGuard\))
>>
endobj
34 0 obj
<<
/Count 6 /Dest [ 22 0 R /Fit ] /First 35 0 R /Last 40 0 R /Next 41 0 R /Parent 31 0 R
/Prev 32 0 R /Title (Production deploy \(no new nginx HTTP paths\))
>>
endobj
35 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 36 0 R /Parent 34 0 R /Title (1. Database)
>>
endobj
36 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 37 0 R /Parent 34 0 R /Prev 35 0 R /Title (2. config.php \204 required keys)
>>
endobj
37 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 38 0 R /Parent 34 0 R /Prev 36 0 R /Title (3. BE packages and permissions)
>>
endobj
38 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 39 0 R /Parent 34 0 R /Prev 37 0 R /Title (4. Cron \(session cleanup\))
>>
endobj
39 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 40 0 R /Parent 34 0 R /Prev 38 0 R /Title (5. Verify)
>>
endobj
40 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 34 0 R /Prev 39 0 R /Title (6. Browser)
>>
endobj
41 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 42 0 R /Parent 31 0 R /Prev 34 0 R /Title (WireGuard data plane \(UDP, not nginx\))
>>
endobj
42 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 43 0 R /Parent 31 0 R /Prev 41 0 R /Title (VPN process \(AIDL\))
>>
endobj
43 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 44 0 R /Parent 31 0 R /Prev 42 0 R /Title (WireGuard third-party \(Android\))
>>
endobj
44 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 45 0 R /Parent 31 0 R /Prev 43 0 R /Title (RSSH alpha \(reverse SSH\))
>>
endobj
45 0 obj
<<
/Dest [ 22 0 R /Fit ] /Next 46 0 R /Parent 31 0 R /Prev 44 0 R /Title (RA control HTTP \(:ra_control\))
>>
endobj
46 0 obj
<<
/Dest [ 22 0 R /Fit ] /Parent 31 0 R /Prev 45 0 R /Title (Tests)
>>
endobj
47 0 obj
<<
/Count 7 /Kids [ 22 0 R 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R ] /Type /Pages
>>
endobj
48 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 861
>>
stream
Gat=*95gRZ&AI=/bV?46fAe4u]8ourp!OWC?@"oRBrM/E$0eJiIL!03a!Lp5P$%8LqrXC<$oS0b#HQ`n!q7LdRl^`e&?GjH;\2(,pL*]ALAt:(c&AR2!c'CJdT;)H1ZP'g=oso%iRS*C[I=ntr]29!4LhX-,ih8llI(@tV#r<:6u*7bK*^U80$+G*iH"1k>Bgas6C_lC/obS;(3,X,8p7Ma57i:_\\$aQH1Z-""i38'f<FK0Oo.^qNeLl&kp?PXd5M@0YoeR.H639Ud869V3Um+@K+oG6a(0s>"HhH?Zd2h?J2V84n8+r']telWF\85;ia<j%Ip0^b->&kV'F#>+c5j!hM6hTp21coS_^[tVrNSi^o?@1[`:c<s\V+%TkEQi0@[OW>/-0'W0]N,8U.MoQ8mY6bMb_cM;jEk[e_=Brf$VT36B+ZY1m]or%+dSm@rA'bg)gTSHQsM>gYqP&Gg#XiZ!9Pb(=[Z>X"Y!lT5Jkao=uH3V[iHbqhV`2g#n&r^l8FomGp*Oc/&@F'SSu*CQI\MmQb`"(7[R/5e!/OL50_P5a"S+.UGjV@(N75!N>7>cPBqhh/5WaC>46q/!,r>H$18^O,MJbbe`1Ym_hQa/fD_A:<q4-0ks4V'?;P<o'A63CW(b]aXR"N/U-<M%`RUCK^Qt[Kj29f<:/urgct$"5%C1g9%+NO2a6'H/I)1ocMXIuPKLXUGcj`QD;^t!hHJQ8Ee:7&1UKlN+Y,2MD%ghaIL"pm(u'Vo#%9Oudi_TpTc/UN+Cn,H=&t;GAbWf:iXUJ'=<C>pdL7%+d)3G1R8o&bna)=MA(,)QhmqDi"t_DoO']Ic1QCTu\'-kL%G4E!_!E_6*/P"~>endstream
endobj
49 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 819
>>
stream
Gatn%?#Q2d'Sc)J/'_]eD(Z$OTm(47Y*I]nCKPcoWH,gH=GT[gd]EIt#=tntXX:MD8:ZZXIJfZI6c$Zm[c_H/E1Ff(.0]!(\c<Y3&CLrRUkAOFh9QKIM.5V$^c<cKOG*BM4sjV&#:oif,a$,W+Ir7!8AQh^peCT96Tmt!W8[2&I(rMqVW+bCTGN](<D-?H"%[o-qPhmANlPB#OnSnTTTc8b[k"8VqP^E%50W,@/9N6J=BX70>2&e'!sX*:"qABrm"pBS[u8h*L.12n(HL8EY8T2JRXo9>$R<E(KuomIJtt&4VFKb'X=gl3>ZR\M6l)UbaW7<kTmjm^RSKOhoubN1s8?L.1Q)k_Md"mB.ilQB^Z%Fk!jjAAUfq4+pc\p-n$a.5*7j].-&'$]M#mb!'9#Dh(/&@-W#RL^2heXV5R3l/l'EbCE+1MDk!LY'#09Cce@'D>Ppl&:L(UqXVH>rA8Fq;,,E.Q.4"H;CFqj[N][':SE\%j%8tdNMil?<?)3h;9$?ca[qadc;PGr83Q-gK3"/:b"`tqJN@FW%@or9_AQV*XVl"^+0lU2Dj^1aZ^ZSN/([1\:'E!%jr!ff9LpgJ(XUNYZT]'L@3I!B2?m=*-5Ka)t1Uschjs7g"8E/uo7Dba\37,n.J6t.33+pp!,YE5>j2uqf8E&3#H1VGZ64G)n@jgT>P4$#Bib\"?IQdZ>Jah-tZA[bU(Kj%^5BsWO\_/t&3%u&eIBJJU:%<brUg=5o!Q:?]DqfK9p`faD]-9S)g8+?*'5sH<TOD+Z`p=E2E^^6"JI%M!?C"t=VkeeS5Iro)f#N[O43<~>endstream
endobj
50 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9$AT&MD_`PtL;ZBnUJQ&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:ps&&=X(PCH'Wj;^H^&o:u0B4;^:%sp:ZE&Wa6R#'r_sXjY5-C*Ut>641HTb(JK2s*r~>endstream
endobj
51 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`Sh(bM!;,fa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#A2MA-a(rHVR:D)FhU3a#MVn+@6p(6,SFQ!,JH2MH_7OtUjbJt$1U9TU8;~>endstream
endobj
52 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4262
>>
stream
Gb".?Bli%=ot,^mqp]V/D2R7#.C6A@XE3c>Z]=Ehg8R.PMLm*!'ZkliA5Ct[h?WN]Dc60";`d0",_--4qnNjG'n;&;mqr,3h8AFc!M[F/&r<4uE;orFhk,<],O%n><33@()IQ>l78jX7f:@>)S1li94.Ym>9U3MCFbX+^Hrj#siI3$G*6Tp6n_sY@hbaaNR_j+2GnBNh$mN`]Ju3:KC8oqRp@TN2Dsq#YmdArVZJfI4D)WDh5:N(UncJ6o_Yk#)n=SF[V^Z+[,om8iMQ3.8qZPYC&uaiN$&50Y<lYt6;,!OJm1ZS+@SNpq`ro<Y>FO+t6!%%1[b<+m,bOS>bT9(RG7Za>AFr?TcV)>]$0PAu#S6)+\t`RC;@X:uA*0thLDL'6:FsJ$g(oAq'oPa(Eq)-Ur'5';Y*&74@.-lWDC81-Vk%2$aI.BsOhORlT>SFD'>H4rhejH@S,%$k3V/"BobFqaEV7G6(Tnd'cnl\nD*/?;rW5'BKguhr_$0T7r,M=J#[QqL1HP_FoO3EPS/HMTNora[i)t,k+n31CLDSDh3asRma@2Cq+hcXUjf1?FdPo6Kq1dOj:^oila@Oo4dk;gN4VgDtomQ*KPlk@KkR,j(k0A"hgto_@9?#4JXiu0;nIch+cpg^I"umH"Ta*\id:)JARRIk;+Oo<GY.%]`"Y,;J>UO-7?e],pPsp1UrZ1)^k#00$/F[Vp^'(-aRK>egJZ\j#dn&-6LamA]kqYR_oOC!6XJs+t3H\eMU'"HLQFkM98QO._JU3A*js(HVB>4RnmJ&1T@!IhDb.\f;Dqu8pk(D^6Ce*I-><H%&+Z9G(Z\>:JRH?dH"2<0\Bb_:<=>N]2!M]"l/fn`8#o5B"e/4,1&0NF"4t`jTA,oAjdB;1NBm++PC-(\d0j?CIN*!4KLd\K]DklP@=^4>T!b)k6#_Zn,<X[G8%`5/:`EPpM.nRNUFpF,Ee=Xq82A9Dl_%JV8`MG/HP4QA?Y:P]rJI;_c1]S7e":_kP_hM1A/XpESU6Go-[)u>=TdmHj>u\[`=9ZHPn[c:SIL><R\2/Z4LAkG_HC%incaP!(5rn5r!gYt$JBOKs%GT5;S:qrbM:7*SRNj1jUFmMkl%cE)=#fU\",!MK$abKoUI)c;,[sW0#]9*k=!]\O$rM\PR1n22RN-W'W^GdO^^FDf979)b8qaRG7._W>I7DnL6H>AbF2sf>S?*T8>_YfFVpQOt=UABDFSl:Q=@61h=ZoIjl8AQZK3+*1NVZY2ke*;o/.+L0&/[cS@1K<<h-d!/K?$J.4dPD%nJ(pnNSk`\TqR[l#`C53Qj:]#JbdjnlNZm+J+0"!\Ck4OL@W_'1#.9X#YS^r*:]:(+4q3HoPo\4;*Ws_Z/*WJ'um3uO0bd2,$pMNl@_dHp&Yjl](88D2FNg(X2WAarF/h:&0X,.`Dg``;3cC1NG(Del]N+4g@X\^PIO7!M(9/uYdbHj4l7:s!j7V`<$1sufr/qY-%eDt<=+0c(7ZCCVSRrn\tR;h^ZAp`&I7_<9j8KjBq?gF5[uQ%n4cd#RVqD`kS$Usa.V(^2DL"YUO?BZiI#^@BLK2b'%tD^?iC'lMP#!JWARCI,WGn+TBRPJdL-j]Ne`Pa]&:`jK9l"^BXQ!8%S8/,>"_C9#q:C]jSJ&S[SiW_QD3*uj92Cd"aN)bOUI8M@d?Lp3`1]!m!PQhr(1K-m'7t\&3i$sBo\&$2=YJ(bM'[!/;-F";puikn0UT:J+r2=bVkh-5+t'o^]KQ/XSOAnr(_aZaAlj$peR70O>)DRj-.kdkMs&K.'mt,-u>90+5/XSD1!o,3uMpCc5eOC&R]J]r1%B>D^/r(?SdaT6-;+PVq(=lHji&lP>niJ!V^3lH'aA5b\@jj`&W.593"h:\qp^MmkLG)(XIKQ"-t.46;Jl-&Mb/_6o]f->-aDaH^L`EPUe\m!7=Qgj!fMqpHaR$*7c<X7r)m@Llu1o\.8H1r9PPL4XQ%c:"8@l)i+TYAl^-L_pf-<J-6pOf=UqWS%BqqqsdL"':'9W0uqA@=atq?^TV:m=L/I#5kXaBm26*8qol0[%ZUmg4LgRWC@Su@HEA7N'kT:T6(_C!^o(uKrGH$DT<\KF.Lb6R2LnQ00Rus@!5U3jZ;V_dJ&m\DX&_Q60q:J(_k5XL8ja<jbt38igTV(p:aY3L&aD#ShQJXe(P.ko5dV!<J;](If`*g5?nNh7&FI4<Y5H)Mj8h_XBllB>a+'8746Crd*,kr!oc1@#98695.+)Ld=t%9<](P%`8fN2s4L/@g3b0tGroQ(5-AuurbnI"hTH5H8]IB(e<h(?<o-]Z)IALfiR"cgG`''uPPPGVS!jl8M+JNL?!@3?17XLF;&-PUP$sI33K?F=i3)HV0(U=<t.$V"IKG0W`QNH/Yk*GEL1eF=&)!!?l`t^@<!OogN?2o(jUML)%$lb?Z&L;M-!HgWE1ib%G$;mu8@BSBVjc"+L0a$V*ej2HKOVC7KoB@;a/b?5^dR;[D:lL)g%Xa1&]a9?WZt#Lmq=n#VZ7!dB"2]%u>!1XTONTYaH3304h$_jn:4R,B?F\"b0LaHF_/1N+R1R\<?HAd9X-@8P0\TB&eT=Nj<9lB^::rbNE?N<B35;^5G?Pu-'q7M9N?\#Y]/SGS4%(;n[EIZ["XeBqXm4:ga=RjRLnU5R0**+*)?dd5&9gX`o(le,]X$7Mb'7qX-=>V=g4TC#+--hRCM[Z`@mbf:Sh&^V/iPoF0p,g??7HQA/_.uA8mEg_I\k1SJuCM-(3Lq[OO[3_PN+]DW)kqHq[(\Q?0`b,\=IujmAQ;Z&Ya@b0G&LliK;2g/N0!p%)RE(n<-H]E%`_S[mZ_NYulApE;U.0E7Y!#@s%`Ll/Zuiq9BK1_"Dj\DKhXP#JI`&VTIu5])&6F1T`*H9):O$:b,n,$9Ti+>uUp"nqn^RUt%p,)s?cIB9?K,rE6N25p;=t@>GJ*)]UF4F2tEQ?GqjS0:4gBnA#_!X)jDsLtblffRtG5OmaqWNb:H9d1mD)XkiE:OR`@fLH*r#'YeP*?-^;H)MMD0U9eIn/ZAcJ&_q_r9`UXloeGK?</'9K3@SU\UlZL=k2_;Sbt30m&1K![UmP^pdd0*B:;Z&NOErkf`k[9?I*)XIngt:.;`90RD6[P8nc=\`$Np0GC10BV-A`:i;s+Z/)-TsgSS6/VU:'a#l)LTtl%Io"5HSr`,Lr)u';_BdVGSu.W/2neYb>2TV<N/3n,Ha_4S-_VVS<CcE2,oj^WW+%rbGRNV5U0.[rl/&r;FG:(<'u>-5r^D:=4mcW#s37ZBOp%V4N4b:9-T)RHqYnk`(Xa'KU$C^PC=b>.@mjlNL.ZdZR`4@..1q5t6pu](uo,]0hG6%_mbgQSj]#=5[s(g?@Btcqa!BA,u+3])&m-W0%3E,7pQgo0%R4IF0=.bNqm4$d]&ETe;$[%:(EJ8EkW5']87sqeGN^_A#^#g0;l\@cS2_mI.P6"ebO!Z`IBcM"40SZR6hSBOe#;#^)it(*rN][\90NjjBNN(f9A_-^oIClF#1XGC1+Aa-\roY8$/VH_1Z+"CcL*&,*OK3gUGjkWqcYgdK+#Kc+c8?X<^*3iu[t?o"BcXf8m@kI`c0pj%L`TmmG*dG*=0?POpqUh(!T;%Y[/)u`c:(q=XLf))Ap(Uk!D]JuT@B-)uY!_iWc2jjR(,/)ZkXg?LSrP'j>+$`"FedCJ"&iWMs=-))IKf#s?SX%/L[G\Ha1JZs^pM5]]%]ae5ao9-Cd\YM.[*QBRrL4&OC*^Q_W@Ys]I['Yd'BgfIm8'RNY/!P,-GX,p(c"4abA&1lU2SMf[C_=UO$LT?h&q`LBN6U*@(m=;inFYUbPGN[d!e])VBX56LGN)8r$gd_l;cLK;H`(&;);%.P)eef7U:Bbi;%e7S\<[R^mFR_/l'dt#VF]V-n4`t+d,D(noUZfqgA9,GSF]?@On9d8@'Kpe7Q-/nPp^=iU]<VJ_(gDj#E!pUQP>WLU'l$+-hA[`7([3N&jTb=%DKYiM`m6f8YQg2J_:YNS$*NA=%F^]]KQApYj3S\O=a/_u5?0?K03?34@rEa9g`k]?/#$NH=:e:SJV/]Z:i>Z4K(eHIu<J&na;-RSd)#X(<ir$2<MmikkNRh9G083t\0BCM>VX0uoAclRs[T5DQ)C;P_=;>1G7ib&7j]o\E3R*778h'4MBB0Q4t1m(;2NH.)+CWYhOX"05lMK\.&>57d"k>`!H=Ra2es0QgSC@s\WQWL/?t,td>Ijth0p+#s-.oE/(n~>endstream
endobj
53 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3506
>>
stream
Gatm>lZ_&D'*"Q<@a<Z_)u;hpM'5%<F7Q!;fhL3XU"p7&*fa\4-oJH:/;NB&hi[iP(`Z6</Z0c#7\e@E*krT3"9UlcropTd0#l98@"N+/GRgT<*#RSKBRKBkM4%:'0'NmO_Z=[`'#PbO3"LdaU07%*&/W>:0F)?9mt1d2ojY)E%1ip2QisRjU^o:jY@.86j4A%2YP?e[P3Y!-AD2>Q6h.q=U(=[KqSX2%8fi\Zf29CuMN2+iq3ub,#::H0#Y&IQ5iXi^C^)jN(=<P"-=FjQ+F8brOt<$6+u#l)!D@s?1@"]q9eo.ds7M-,D_q+Z%R*b+l.#q_b:!g3O<tCsP/4.4C!Hf<a_*l&64FqI7?Y#SMJ`C)qCPUD!u^@Jqb]?9$PG,Q;G[*>e;Im`+P7bb(T<N>lo@'W?Jccfir.n<Y^%Kp<(]'\V1P9QP&/=tG;I(#<;gWQ]nOb2?n54RPkmR:P#HW*:\\k-0>N(8lc#*BZT-s<eAJ29&j4*cb`/5Z8L,'aNb^_rl!#G$ocR!O!q*t8X'8[qM%&X+S]-_QBEB8FE:s$S>j05aQ=OI8rDI]@,B,B+]\3rI@H*-#Bsuk4<L699Yma^WZMb:[>;)5OXRY&dV<m1mAQ]hEllb^c?b]n(NNRHU_S%`[V`r;T5Rs]1[u&ItT6PsR2iS[gR3JIufrcS8Bp_l#UO@g>QT[Qh2D1J=bq3F_,02/>%^8C)Jit<YJgKos>%bZ<[F#^b&fr[6=%Mi1Q./\9du:ur2iQJQ<ZJR`"[T#@-X)TmmQNXq&Kie6jgP7tELAoa'DFH_qqp-[9AaOJ0(D'Q;g,mI!.V=qJsr<8\Y#28bGn(99>MqKBlI)`<Aj7$B5.#fX#,rh53OYB'FOUKgLXmEB9bqT@'kgQ1SA2UN+)>``kLF81&T9u-oh_L8Ad8]O\lu*7`?KOC!n3$oe!LA<JNr19pCFOeGhfN.P-?9C27mJ_A30q*BN<AD$-*g2$>HKrr>*L_&E8]O]!q"C<rhCGLr[bCNE.s=TmOpf'@I&L?Yr]%1X8M41'fp&\$.!h>X&m3*Np(JP1PA3pG_>#_'R&R)YV_pO[-[rm"S_XkF!()XoffpbV+s;:.aBo`u_S#<[=V!V"i^J6?\r6K/@obG5rP@_npG.7E8Xk]S[6Mr_S5*7V+V&C9cKc+8&EG]JC':;-g-^*Pk$W&IOs`V#CJE#uq[<;9otdm3>h\)Y8\3o2f/H4m\;[I4oE\;Fm4^tun_gJ<]EQW\MRYBC%_iQ8662!O&)XgnSK1Vr8#LY$N.Y?\!-AKbsFDd$gNlmH)XM!B<#TZIg`7Y(Uf$JR'%K"j_(_4.'nN<qXo9p0)`r1l9UD0/\[-*'E>,\;sg`Z-GMF\&\F_ijrWr-n9e.:o$>om0j37m6N!-AmW*I#?,,e46(UeOD9"DH8eLeR,TjC5c;3l3C>?9b-B3%N490N=P1EYu\sQNm=ph4A:7Ajiqq>X!$B%1nZT:]+mYa:_B"P$Mf46+7m`u&CDBXRm_T=mXOfm$o-J60B:IPr:4lMIm<m\IWKp[5/.$eRB1!P]Z5#NVTg+SSj):&&ZlWTZrkE7QYJZED#LNb(qQ5FB[UuTb7Y-?gJ00EW2)os7$C42&Pmm,)F#RhLD[*ERUe?O-+[Y&3`=:(ao?urpnQ!KTTa13:lM?:%TW[M<I"i.JqlSF,W:"rGYg!a2]gj+"=6JNj>&!D4sp[sI2J72LRqMMWR`/'D$\Nb:$ZGE3h.Gf.X7]q.Hl/>2%esO<5agUX3)Zf&Q=6mT&l(Q1G#*ZaDXe*TVlP')O!k*4O!GEej@'A<N>f>ieD6&@md>/<6^aEK?'='J"ejN"^SS)TjVM[Qokgfcku+cG=]B:i-;TOleT.We-kEpckd!'(jSBcjAjUV5j+10/0nc%%0@9/Qe'ct"KjN@Y8\Rq"\V^rf,'m6BU,&jY:)*q0\[p*=C.2/%Y]W*\"?[07n"]YmJ6G*bmQ6kJSEk\O^3)m@LOgB[=QuJ^>TNOGcioG9P(%D:)in(cloYfWt/Lb7d\%i26p2RS7H#/85RlrKP5N=eA(THIIeRgB<[HX!+4JDa;7;#,B$/Jqh\?skbbc:9XLK!T)64Sb'Z3^0\"Bi)6MmHA@^[c3ucZ9'>lV\^VKb*49%e$[P9&)K2pq$XRDmVhWq7<<Z]Vh-I6G3S5W_]HQ:luTcEPWi2sBJm4DZZlX:Au]PW^4;6EO.SQ=a-bTi&j'd-U>Gt.@VA!Iu#gPnDP,gQW)&W1!JW_%<G;+=9AG6luq6XMd"T+UotE>>Un''[,-.9SFq0r:ifi!%!uf/WJ[c85E(U4-hY^3EDn06Hn%LPHW$MQaRu!K</>U%I:aX<[6;&YNC/#!-.h#+8re%NpX9;OZtn]9a/8TZT_28[QjsnZ0%GR(]u0=0YT"DP8/IMb5G/YsWa<j):PuT%hp+)RGrDpN!1:p:5q3a4T)8/%*ip[I;E#7BJGYY?%+[Mj0)f*V/,7PJ[rPjL.\;Im6DNa03riI4qkMia^5-.j.(JaacS].\8pL*_[%<gB;#3aQG*'o0d61`.I@ceGmDcO!eIGH&cZ,rVQ$C$%gKG!I.WaI"4unh!5!9qVKkND/ADc'`6Q+>,i"h;_`fYW%rm!`;nnjaoFU?",M(L"8Gh(+K27Ji);OA\or8B9"+;aPg),_fl="Mnf]+Sg4Wcab9`0cRuPeP19p,h$''c>LrWX0WG;tp_^1W5#mP;h":CgjFT47r:1D%K.MuW(?h,QsT6%'3Jp%muQ`B;r\rpQGJ]Ku?dt/I86*r$Y-^\N)g\'+X<=:'j%d[(0orZq"DGFlN`4R*`XB,p<T4m5r]\,f"Le0f:-Crt0$F\k!d\ppkT;IOCKI*@_5eZ.8:AnJno6(A\B-A";#G`KI^;R!?\Y=&'[pa"&+N=u[%4A:h!3\O@W6=LCZ\c7;8T70,1F5\oY'8D.,r-*T_=T$G)QV!gSiGQZSo3hqeVt3BGKL3Hd"et(N2$'5pX`L`L$Rm'_4h<>nk^8_e-8!ucAXo^$TPp"2s/4#cVIoN7dhkZG#brD;=:4)[^=A6Ytdfu^aQ.a\bJkUd^M#5f5*4np>%.-jC6\/=-G*VkNK-E+3LrLX1)r1$l^*2&%'cYjrp)IMHJ[[IE4#Me9M)1his-m:Z2'<5W?/"*?P&C3b-;W\ASJ2bHm3UWSEW"hEk'2STR5<VYYqGLO`[pj$#h/gjXCEF(/=?,Q"Xm(d:Z(:_FcFQR!Z\YSk*hT8C:lf!d)^:/p[5^V;*V[u*"Y*uAZ[c,Dt[ef2]a'^t<[1n`pPe9s'$(S)=C:L%eks&G_lG,KJUU3drXgALJ5?QFdt/'Zh\qto9%#4[kNc3.`9OYFRp/UB;BYe'+N?B&[6f_[Y\fP"3fDtO?Qkjo0Fq[G%Tlh`!qT]JAoT@G#Nr@*D5DBV!gY!Knp`I94;UYeA%qa"CGCHkA5LC9254NB:l51I>+D&_jf6dj;_TV=<qp4t^oj$I#6RcZkjgeJr;i,*0;:AtbD'(g#~>endstream
endobj
54 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1529
>>
stream
Gb!;cD0+\r%0"PUd$L4rH\2ue]E%[]")[du*O]Me]:V.%R)D8$+jSc7jl`8+5_&igm`g0iag?nA8WWL7OTG[o0:MnO('*/&QldbR)b*c!%WPn+LV$FNmi#J)H;.j+W!g?$j$V!U[JX!2E`Ph0W+CVA[YRXp&*gS4P/Hq'7TuDF2]\jRf1NcbUI6&:V*2GnrtBPpmP1P$]81V5g<'LeOYnM8:5OBa8-><f+CS?(QGfN;!Y*e9JX4kG3b`mDULtELCKk09g=i=)7kEP4?"V[@2>bI%\5[jSk+cLNn@1!DE5kZt267Re%e[ZI$`#oc8i8qQ\.!lI$5fCj_&UBKRttVg64QjRA(2SZQj0t'9^neDI*;;u\>;N(`RCHuXh>Hc>./k5B5h)T$/68ah.KrkH!^\*7_XX_,5(,ehOki,5=KZAA\jnSQem,qL;Qfr+He51(K9LAn"m16GFl@"J$+*fC/CA1+]g-+aZt`M83a3?.k*0fggG%feV[Y0Z6P6Fnp]H9"6+72&4dkBha!tUO2]J,P3!%:MG&2NfV*L%]c0`SpVpBqW;.CsYatNcj5U=-fN>i"`0+8fXK"^c1<J5/4L7!Sh?]jK\FSY?Gbugg^*]meTTMJb;o2U?>U,6ck[.`4Qb[$O3i"?[n*L83@rt]30q#;LP.HMdk'?Z\2;U95eRrSC;ODMPZO(7^r-FGa22?#NhLYZbJ'U"#[!fuk:q]*m<?j-/joa"_75;el(ue"F*;+ck.$(M/IB2-mM<"1+Sq=`-^^7bsJ?-oq:@XkW>\ssi>jiZjW]6YhUWB9$.Mnoh%Eg9sf>lu^IJgL9/B9&Ce$Y9Eb9=Sqo2]m2^hYsK?40_rMWA5MWMT&PAt,'7K>Tbd.J=OrZRe^\C6Mf0&DcOg:U]SS]lO-dULG&:`ZpM&I8L*<N'tJU_**G`MoG&IMSK>2Z:mdp`+$Q%TW>VJ9d!&YKa[iDaJ'K2No_isOctL4V8BQ7;,QrggFd`ToC^sZkb&kd-:J?87\^"\>H.,#AE1'c)/`m$Ge1K%29.B!q!K^+P_`J^6V+$ZOH)50*8c2"Jlb-eD`rIk'f%`\.jU7t0tbeW!Z9CT4GX\&;Z[(!!\uS%KBG'm,%TYc4=cOn;caPmi1UjT`$4tr8XcT)MMlOi)KD%Ao*DMN-kUnG83Rc/_(oP?^a)"h@i\b65tC5)a/P(,bF=kTahjF[6CXfc4$sg%q.)M_Q:./7#O6HFVr.Xm17=$MMI8'4Lr',s%?Z#]1<orl3-+tk*&9L-k"mu+5iQf8\c,([d[[NAP8'H8OAVGq;Zi!M8\[[CSI,>&AlouM9.P9ij!(Y+\Sc&)pW#D>;Um[",YaE#KsV+GdjoKpShZQ'1CpbmP:QF#H@g&=\+r">U[%$odIP4Z_r%^HHb/N@@]]f/IM6P?[G$XZqE9(3E>12,Pln@('_4.EhL0Ydh2OKT2g<0upj8pr]BtQ'&,0TH*knquG:<,IW8>!J9QS8jn^bHKG<Bpc5+t]c$+<K38t3AXL(>X+HT4k!#C6j^DZ~>endstream
endobj
xref
0 55
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000501 00000 n
0000000647 00000 n
0000000794 00000 n
0000000941 00000 n
0000001088 00000 n
0000001233 00000 n
0000001382 00000 n
0000001531 00000 n
0000001680 00000 n
0000001828 00000 n
0000001976 00000 n
0000002124 00000 n
0000002272 00000 n
0000002420 00000 n
0000002569 00000 n
0000002685 00000 n
0000002791 00000 n
0000002869 00000 n
0000003075 00000 n
0000003395 00000 n
0000003601 00000 n
0000003807 00000 n
0000004013 00000 n
0000004219 00000 n
0000004425 00000 n
0000004512 00000 n
0000004843 00000 n
0000004918 00000 n
0000005065 00000 n
0000005167 00000 n
0000005346 00000 n
0000005439 00000 n
0000005566 00000 n
0000005691 00000 n
0000005813 00000 n
0000005917 00000 n
0000006009 00000 n
0000006143 00000 n
0000006258 00000 n
0000006386 00000 n
0000006507 00000 n
0000006633 00000 n
0000006720 00000 n
0000006823 00000 n
0000007775 00000 n
0000008685 00000 n
0000008915 00000 n
0000009143 00000 n
0000013497 00000 n
0000017095 00000 n
trailer
<<
/ID
[<73cdfebcff903d1566b9858951833b52><73cdfebcff903d1566b9858951833b52>]
% ReportLab generated PDF document -- digest (opensource)
/Info 30 0 R
/Root 29 0 R
/Size 55
>>
startxref
18716
%%EOF

223
REMOTE_ACCESS_VALIDATION.md Normal file
View File

@@ -0,0 +1,223 @@
# Remote access — validation (Linux CLI + mobile parity)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-04 |
| Last modification date | 2026-06-04 |
| Co-authored | |
| Severity | medium |
| State | pending review |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
Companion to [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md). Use this when verifying WireGuard v1 **before** or **after** deploying the BE, and when debugging without an Android device.
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [What the CLI suite simulates](#what-the-cli-suite-simulates)
- [Scripts](#scripts)
- [Quick start (local orchestration)](#quick-start-local-orchestration)
- [Production / staging BE](#production-staging-be)
- [UDP / WireGuard data plane (ra_udp_debug.sh)](#udp-wireguard-data-plane-raudpdebugsh)
- [Optional: bring up WireGuard on Linux](#optional-bring-up-wireguard-on-linux)
- [Mobile vs CLI checklist](#mobile-vs-cli-checklist)
- [Troubleshooting](#troubleshooting)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## What the CLI suite simulates
The Android app (`RemoteAccessService`) does the following over HTTP only (no RSSH in v1):
| App behaviour | CLI equivalent |
|---------------|----------------|
| Persist stable `device_id` | `RA_DEVICE_ID` or `--device-id` |
| Persist stable `random` in prefs | file `RA_RANDOM_FILE` (default `/tmp/androidcast-ra-<id>.random`) |
| Mode **Disabled**`status:disable` | `ra_device_sim.sh disable` |
| Mode **WireGuard** → jittered `status:enable` poll | `ra_device_sim.sh poll --until connect` (fixed interval via `RA_POLL_INTERVAL_S`, default 3s for tests) |
| Capabilities + `tunnel_mode: wireguard` | same JSON in `ra_lib.sh` / `ra_ra_post` |
| Apply WG config on `connect` | `ra_wg_quick_from_connect` + optional `--apply-wg` (`wg-quick up`) |
| Operator whitelist + open session | `ra_e2e_cli.sh` or manual `remote_access.php` curls |
The **control plane** is validated by HTTP alone. **Data plane** (UDP to `wg_endpoint`) requires DNAT, `wg0` on the BE, and optionally `wg-quick` on the machine running the device sim.
---
## Scripts
All under `examples/crash_reporter/backend/scripts/`:
| Script | Role |
|--------|------|
| `ra_lib.sh` | Shared heartbeat + admin helpers (sourced, not run alone) |
| `ra_device_sim.sh` | Device-only: disable / enable / poll / optional WG |
| `ra_e2e_cli.sh` | Full flow: disable → wait → whitelist → open → connect → close → disable |
| `test_remote_access_api.sh` | Minimal API smoke (grep, no jq) |
| `verify_remote_access_prod.sh` | BE preflight: config, wg tools, DB tables, wg peer sync, optional smoke |
| `sync_wg_peers.php` | Reconcile wg0 peers with pending/active sessions (`--dry-run` / `--apply`) |
| `ra_udp_debug.sh` | DNS + UDP port + HTTP smoke + connect + `wg-quick` + ping BE (`172.200.2.1`) |
```bash
cd examples/crash_reporter/backend
chmod +x scripts/ra_*.sh scripts/test_remote_access_api.sh scripts/verify_remote_access_prod.sh
```
**Dependencies:** `curl`, `jq` (device sim and E2E), `openssl` (random). Optional: `wireguard-tools` for `--apply-wg`.
---
## Quick start (local orchestration)
With the crashes stack listening on the default orchestration URL:
```bash
export CRASHES_BASE=http://127.0.0.1:8080/app/androidcast_project/crashes
export ADMIN_USER=admin ADMIN_PASS=admin
./scripts/verify_remote_access_prod.sh
./scripts/test_remote_access_api.sh
./scripts/ra_e2e_cli.sh
```
Single device poll after you have opened a session in the UI:
```bash
export RA_DEVICE_ID=my-lab-phone
./scripts/ra_device_sim.sh poll --until connect
```
---
## Production / staging BE
**Verified 2026-06-04** on `https://apps.f0xx.org/app/androidcast_project/crashes`: `ra_e2e_cli.sh` passed (connect returned real `peer_public_key`, endpoint `ra.apps.f0xx.org:45340`). Poll step may take up to ~45s per `min_poll_interval_s`. UDP handshake still needs a device or `ra_device_sim.sh --apply-wg` from a reachable host.
```bash
ssh alpine-be # or your BE host
cd /var/www/localhost/htdocs/apps/app/androidcast_project/android_cast/examples/crash_reporter/backend
export CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes
export ADMIN_USER=admin ADMIN_PASS='…'
./scripts/verify_remote_access_prod.sh
BASE="$CRASHES_BASE" ./scripts/test_remote_access_api.sh
BASE="$CRASHES_BASE" ./scripts/ra_e2e_cli.sh
```
Or from your workstation (same cookies/session as browser login):
```bash
export CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes
./scripts/ra_e2e_cli.sh --device-id ra-cli-$(hostname -s)
```
**Full E2E from verify script:**
```bash
RA_E2E=1 BASE="$CRASHES_BASE" ./scripts/verify_remote_access_prod.sh
```
---
## UDP / WireGuard data plane (`ra_udp_debug.sh`)
After HTTP E2E is green, run from a Linux laptop with `curl`, `jq`, `dig`, and optional `wireguard-tools`:
```bash
cd examples/crash_reporter/backend
export CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes
export ADMIN_USER=admin ADMIN_PASS='…'
./scripts/ra_udp_debug.sh # DNS, UDP nc, HTTP smoke, connect, wg conf
./scripts/ra_udp_debug.sh --apply # also wg-quick up + ping 172.200.2.1 + teardown
```
Steps: CNAME check (`ra.apps.f0xx.org` → same A as `apps.f0xx.org`) → UDP `45340``test_remote_access_api.sh` → whitelist + open + poll connect → write `/tmp/ra-udp-<id>.conf`. With `--apply`, brings tunnel up and pings the BE WG address.
Skip HTTP re-check when control plane was just verified: `./scripts/ra_udp_debug.sh --skip-http --apply`.
See also [20260602_REVERSE_SSH_proposals_summary.md — Appendix 1](20260602_REVERSE_SSH_proposals_summary.md) (Linux laptop WG test).
---
## Optional: bring up WireGuard on Linux
After `poll --until connect` returns credentials:
```bash
./scripts/ra_device_sim.sh poll --until connect --apply-wg
# writes /tmp/ra-<device_id>.conf and runs: sudo wg-quick up …
```
Manual:
```bash
RESP="$(curl -sf …)" # connect response
source scripts/ra_lib.sh
ra_wg_quick_from_connect "$RESP" | sudo tee /tmp/ra-test.conf
sudo wg-quick up /tmp/ra-test.conf
ping -c1 172.200.2.1 # BE wg0 address if routed
sudo wg-quick down /tmp/ra-test.conf
```
This step fails if UDP `wg_endpoint` is not reachable (FE DNAT, firewall). HTTP E2E can still pass.
---
## Mobile vs CLI checklist
| Step | Android | Pass criteria |
|------|---------|---------------|
| 1 | Developer settings → Disabled | `action: disabled` |
| 2 | WireGuard mode on, not whitelisted | `action: wait` |
| 3 | Dashboard whitelist device | API `ok: true` |
| 4 | Operator open session | `action: wait` then `connect` on next poll |
| 5 | VPN comes up | `wg show` / ping hub via tunnel |
| 6 | Close session / disable app | tunnel down; peer removed if `provision_peers` |
CLI E2E covers steps 14 and 6 over HTTP; step 5 needs `--apply-wg` or a real device.
---
## Troubleshooting
| Symptom | Likely cause |
|---------|----------------|
| `action: deny` + `wg_tools_required` | `require_wg_tools` true but `wg genkey` fails for PHP-FPM user — fix permissions / install `wireguard-tools` |
| `connect` but no handshake | `wg_server_public_key` mismatch, UDP not DNATd, or wrong `wg_endpoint` |
| `wait` after open session | Poll rate limit (`min_poll_interval_s`); wait and retry |
| `jq required` | Install `jq` on the host running device sim |
| API smoke fails on verify | Set `BASE` / `CRASHES_BASE` to live crashes URL; ensure login user has `remote_access_admin` |
See also [REMOTE_ACCESS_IMPL.md § Production deploy](REMOTE_ACCESS_IMPL.md#production-deploy-no-new-nginx-http-paths).

View File

@@ -0,0 +1,289 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 13 0 R /F5 14 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 762.0394 147.501 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 751.0394 70.13304 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 740.0394 156.381 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 729.0394 129.285 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 718.0394 217.317 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 707.0394 182.189 718.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 696.0394 127.037 707.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 15 0 R /Fit ] /Rect [ 45.68504 685.0394 103.045 696.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
17 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 32 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
21 0 obj
<<
/Outlines 23 0 R /PageMode /UseNone /Pages 32 0 R /Type /Catalog
>>
endobj
22 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False
>>
endobj
23 0 obj
<<
/Count 8 /First 24 0 R /Last 31 0 R /Type /Outlines
>>
endobj
24 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 25 0 R /Parent 23 0 R /Title (What the CLI suite simulates)
>>
endobj
25 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 26 0 R /Parent 23 0 R /Prev 24 0 R /Title (Scripts)
>>
endobj
26 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 27 0 R /Parent 23 0 R /Prev 25 0 R /Title (Quick start \(local orchestration\))
>>
endobj
27 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 28 0 R /Parent 23 0 R /Prev 26 0 R /Title (Production / staging BE)
>>
endobj
28 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 29 0 R /Parent 23 0 R /Prev 27 0 R /Title (UDP / WireGuard data plane \(ra_udp_debug.sh\))
>>
endobj
29 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 30 0 R /Parent 23 0 R /Prev 28 0 R /Title (Optional: bring up WireGuard on Linux)
>>
endobj
30 0 obj
<<
/Dest [ 15 0 R /Fit ] /Next 31 0 R /Parent 23 0 R /Prev 29 0 R /Title (Mobile vs CLI checklist)
>>
endobj
31 0 obj
<<
/Dest [ 15 0 R /Fit ] /Parent 23 0 R /Prev 30 0 R /Title (Troubleshooting)
>>
endobj
32 0 obj
<<
/Count 6 /Kids [ 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R 20 0 R ] /Type /Pages
>>
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 874
>>
stream
Gat=*968f@&AI`dp6_Y.U6YDUO(k&)g>S4Qkp[G#Lr:0I;D=pBpKa1(575DB%+\O]n(WV@V+'>7@.B7UJ?L,$h]neEE"4:aKQ%!:iTSSn5?CIr['-tB@gqC(+:1PI`dQep&""/HDL>h]M*N8dSk$_UHSl#EPZ.oRj\uNE'I_)M=$i*5SAE1>9p`DE.$!\@%8T;-fLCd0(?NXnK;XW!P&:38mGnM"JTU]L=]fp,<_<;\FEn2O.tgtXDGlW&&+gk466fEo(Q)^q&_^aL$]h9X+6X]=89$RCJXU:qMk()/]1l`BlQddWn&/LGBnpEF0]AM<[/'jk;qrgL>)9l)Es"0&o+Wna9H_;b9^%aqq%[n\Vd(JATf&noPgCIE'L0h^S3q*foBLCAj_8&iX?^n!(8W=m0ChR0?WT)KmC&/o;sg(gA`XUD(DQ+WT@h-LBI9E>-9:@4R"%P_NGUE%d4Y)R>iC,BKihE$ZcQR?Wuf!F6(R+:KfATOMq/tnF1h&dkS'iupEPsrRYq$MiFd+>LT+m)(iEDuG95/:eDAm]p't.H\Bn_\5M9JS*ZTo+[*Ip+T-a(S*oatS9t_O._ZRXn7n%UU*jr"Aqn/9R%JF=I\(9AkVtCM^:@0UcU`C&a_%E-u]lnKWnXId88bPhjnM[Z@*B"Fg_RF*h8Ssb\rpeRWoYLI9NF+G5pr*:%TCdUR0jbcsW(C]_O5HptJbT,NT2sR,qcpqD&Fm>^2l:k;@;X*u,1ea&7A:uI]g5rB.oajq"Vo$6+JqKh#!uAsfg*UciAD0Qc*!pH,m'prm?YZW5>#9)N\tPi2:Lc!=h)[H7&-G=[VjB@l-HE:Q41nle@<r[.hkoQ/Z_U)$\W<BNC8<E5LeGDT`~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 646
>>
stream
Gatn#>Aoub'S,*4/'_Y9#M8*5hpR0QOjYO]]_*RKjFA2^&BoN9p[`#g<-<&RTaFr>&fA@O*6pPIYZmp8k!sr+$J0J)L^3PY8:Uc7$cqPqpP>>8"sJke&-ice0uAAX\G+cgKG2ZBI6`6"iA^iPLL(l1;#?CP-@*QA%.ra#hO3>J/R_?+Y2URr+IBLp[F&&@If]MrZ6.l5`/jC8q#1Rq+R('q*W<6R##*-I\sS,Y_Jml]BId\>+-@Zd8n=JCdI[e&:PN=B=HO[j)l=$[`g+bN"]FG.f1%J>AA,P=iT#?`(bYZdPUpY-K&O;kg5C7f[oj4H&e3slA%$FI8"o]Zb>\KG1CT_0e_HulZ)oUHD8CF>O*'&*4ZFERD:u:q@FYhR<1=c[]^^)(>DV#4q7fdk#"'9abFNUsqlIF'EJ9Su.\TQILVt:WJLYBi"OX'1`attOgJ'=c`h[0Q4qL.23?N3\+XUnki;NX@%YZ8n+6$$+[l<$dQ<*,F#/4aE9:B]FdqQ6sDgtLto@\fBQf$/pJ;ra2T_YS\.o@BF]L[/F;8[-l#LA":E2DB`EmmY8VNJ#kKc?B$Pln6O0;[+\6g,p.#`0dN^Rs"_':GTY4R:l-'`'O.M>J/sLdFFk0"A1LH6`9A"/EWuid'M~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
37 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3521
>>
stream
Gb!Sn>B?9)(4Q"]kaL>0OV=e7*Qb_B.`G9u:\bc!8R>@b[@h*8&J,j`p<3C-4UhH9D?=IJC2;"\Oaofk9:ss_=s=U;:Q5<lXqCLi7tXS]&g]AA+]6&_RU;cQr8d$t\>*#&X:8/q62F-iP+_%.hT=AA!aX%VI\+?t@`D*+d<ppA!NUTrc>goC$LqKuJ9i%23Zt$SHRSI4rUfr04uL=r(Xq']\@fPf_J9OSDiCF3Fr8ree3hP=FB5T*pOI]P+oT%sTnT(,FGf4A^d(L/oTjU\LNI`]a0g^#YXh8b&IjY/((FM5AeE]1I@QlPi`QS3M_6q!&+CurBDrD6[SCB1Rg;R3fX!p%Z%2&k0e#jh^oUo//14k/(W:XKC*Th&WmX/*@9jG!%0SF-C0D8Y&IYnCee=uL/<7)7"+c>@9p-D#,cjVL<_jhE)*uZ&7.*@FGjIXmI%>JS+#\u8D7[6g6u:mO'JX76ECiQ$M[^4S/6"Ek\Bd)0mMra6>>BbM/UBKKJotQn\u0=.U1;ZJN6Q%WSYG%WMc40^Pf4!2I+S&#h!9@WEV*g+^I,`u^;@H&`5ZWe"ZbN?io9UC<\\HYm%k%4\^q2s6V1<iK]S=CkX1#36Ulq=8t:-GOE-8<YTugZl9iXI+NuoLPQ.I9iin:E`sXAqpmJiBW4BBF'.[*)b&:Ba(QPZ!]L?6(\@c:I]GX;2=J`q:7&cDNA[j07]953Q_<nb]ALi`7!kg<Jf94kKg$q"0mG:LAKJc&B'*fR@o]O5>-8+tc!fQt3giXQX'<6pTb]&dNOa\-8G;Nh?M\(ffd\WlR5tNC\*34eE7sQURhY1Mi1AuZI0f:R2=^^O6;N],MmjHQU)l/VD8GR$Q&45XOf1"pH&Hj7_eRuD9/46g'WR_B_ElqjAWU@ecPM9m5e)f,e.*_lPC.1$ujuN;T#LPRLO<dJfKLPj$oOFoX^QX_L21,?p5&d@8MphlolV^fJ?i(VpG6/)/M8Qk%XeK^FCGd1d$VAkYl&tp4X21)k)g6*F9-<&m<0P>;<Uq+s@dIP,WZGb13lXf^lKGE*=4VctP._p-#l62heJO<mR4^K0!>N]:[T%rjNaKp6-"CsjW8HKWOlIY7pH31[A`$iT0;*mVJL6ICdBc3_@0#h=GC+ID>i*]&pYX"m`iQDi);9cUOr:V.+bK\>@Z>giG>>R_,Tob#FWq':NqYu;7iP:O2:f"dk@b8#qiL2`USYTG_8r(>5A%A`4HYmAT&D_'Y^Y!HK*k6G(\k(6Yjt8H3p1"O[a9-pprk[EjZkb*&i&h]br+a!nu_K!m=PG9fR,GTTt`L#h!R>rK*ZE03*`FX&["ELqbVId!pTEP+*5T?bU2P:GhZc0K6Y-?BLj=dm]?qX]$%,qf)ot,K,,?33H!8T\\?4Z<2,-33Yag^U9'3\.2t"#>Bb_<]fQ_jU0Wkk$/MWPqZm&nG_1^6Y,ENoIboQ<!B@@/g@"XJQfOJcR`1>@qubp9[ZD`XOjkr#g\1Oi\BH![Dk;F]Qg#`.a`C89.Xds30[f]Y.0j[R4W!pEM7i!pb(hb@*_=^-fk8SZ]1>ru>)2-;!7JG>We\Ej.V8fhB+u?K'ugFhPYpYV-ub#ERPHi25ojnqJ=mZ]^*ZC2\@tIQELdDm8G+0bhRCTI+,Z:LetATLpl6.*?3)SpnfBuJgci7dcd0b;$Di<0G;/#I;[VU#VG)!65aFM,6>L&aN^lqPV'MV(,UJ_NneMlN<>jeYUt3j)<)LgE3fXPZB'9@pMlD4V86DkREI95iL6c9.=gWi(kR1Mu/nG,@,g8/5)YbJ_%"fi\T3t:s/.les0_'.e0C*(Bh#@tj?bQ#$Wl!@n^jbsfWr<3R($-jrQR_+q>JYdJV1/lq^\:C:i!n>Q'0++AEp_5#qds;L'94&K9#>75$#_,G[>'FaVCqKY$rSMF2k`gF1Z\?DcUg\DDX;(8r;LjFG$N^$4:ZJ(,a/Hs%QmN1*]1eA5(YFB:`8V(_B5Bh<#gd6:pJA7r;qpPdW@dk:E*,<jnd`FHC]TIQ8ha^2mO4+dq`s4g?FT&"5iVpf4UXJ^ajANm,*9X3=IbT\m^3omG+?LZNIV=*R@l@qi:+t&R5-.'Oph<$7-3jo_>aIE##LUC<9%F6+J-1X]F/`ich'Oa([ZCF.EOt3o?XS1UMiganK(aL*e!>49B'`c%&+g_-T:rC`h659q!C1E`X3!"rqT&DL7o&N52G)Fn8#kSq=Bt+"KYg)Jd>(5<"4uScsQ%J=)Jj9N)YHb"/qt\&-`\V!uf3TKI#C!T5S$<d[P4gfT+>T*MgqQhl`Mc>&Q4):UJQ"*g^L?Xk4@`5VU)Xd$gbV,faK(^.Jts#A7ap6(\0J5MIP?jVa.XS;aFFeL&)4b9Z/=;Q;.g]P(/5d58/JsG\RFML8,ILYVcg'EV:?@Q>jh1%o`YIfmgKWdrM@.]J4UrLAD$*N6iT^f39N]t8'9a_uX/X\Pcp=d"&q\PIu)$l@KI#=A!f9VSYU3/r.e69Z*@D`@9<Wjra.n9Rc<$rDXh1Y>u73RLbG$Y<f`UVk5F:0??/H5FN%/BQ^3<nMBBt?8U]u^']`es*"gW@"JiY80B?+DpJAVP<(gQ*j[EK*uKR9+*gQk6`Wo!5?,<E8@7Gp!K07aMaiJ)pusT9qoN>kD:YK];3NV@=R^'!?XM[a4_A,7t;$s-hH0`l1^Ce4"[JGkp@ZNArC9j2-=;]V2.kk[QPT8Z2Cid\1UR=7s(P0;'M%!k&PK7BD>Vq@ZklMu3&2bB.?D*7(K(lmN4d#6RiOYlK:5q+qn8"5*2BM&d,B8#S7sSYfQ*moCE(XtO^PDmS^,LBN)Q2t"\l=`Pa8+as_.\cdA?r+I=DUTMQePu\[)51JnJ@jB)l&U+AKrF?,Hi+>?\(oP?f3C9qK#2'-J*Zi^Z@nGDQn1t[E3SuID\;7(oZMeO@hsG(U<;aZ1[I98i5HoQs<ZAX3M-8:3L_:KA;>h$L,QTCOYeK%P\t2+SQq-S[h)8jbOolcA%u20'^"l,51Tch:,TeTbhmqo3Yih/lBmg$].2@Kg67V-&arje03Z2610/(rg+'QWuDO+CEKN<(PZBHFSN.I(KoXkQc1tmW<=$oTc'A37m]WX8Gc-g`.j,(L98mj]h/dm4@PWDlFW7@a/2F_oG\KJWkBMDe6.mY8i$2BJW:ISiF,td8,P)mFBdRXm]NM8j\ggAfD6>#i^)#14k4A)`(Sg.tAQKr&&s(_uhieMdjjBt$tNoq@ic_;1d'Y;!=98<f)e=$q]+<ke+#LNI5,)#s3Fb3:$K%%aAmCP@d%A$-NfP\0N\A%O3"$<f9PJo^fl[$0i#/<03ff\E_8Z>%ic0JS7W8ADST`4p@,q>5P[t!PZrXF"L>=TIm<6R0>/#"XTS=aJ>PS/'jB")4.`j*R#rU>[D:4jq?7<52BqYh-baFgj_&VcUF^L[tEXq/(Nl)0H-K_ts$Q*@C.GAVZ_lr#pW@pK[8>slTWr?+6]*/*`tP+Ik<Q$KdcLId"F'm\U:aa5KO+RO[e3r(E3k$i<~>endstream
endobj
38 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3259
>>
stream
Gatm>l#W=b(B<g;i`.=0,>Tl7NGpd[V:`qqALABhRJER,$:@J'jt[C3?+#1[^?-%p+HSaMROHODGP+%HY<`.0rs7lHQ3Zo)%d.1%&4Dm=6'+)Ek1;;TT0/\N3Mifrl:BlFcuaj!-!eOR:B!,1a>/Q61QI;T2'_^G:W]B^$$$_P+TeRWgd;Z/W9g-L(bl$nm[RIC'?9Z,1(>E?_f?cXY$j;sacg4sTjpH#%e8uVrF2u@B^D30JK\<hT#1CHk'o'2s(kYU1u^>dd$U9H"PSYFF+VFJ]u<<VOXZ6\iRnZ>r(H%\0G,\:MujN]8E;=/+LC0=8$`uYHn"oo0"`2DW5g7d5*'??:[d.IhMu7+-8U9[^4WP?1EDI2GX5"3e$97U7Ml:eqRb4ClA4J?ceElI.\*0s;EKFTPm!.*_ZXS<-o,S*UY5;Q$7EXIf@:S\iGU(f?P>6$_)>d:6J(i9/-`*9bU4<UBRisEfV)`;]4=po[H0EI78:+c_MD-[29hni#Qg;GUKq0GbEt]eD*FSh_;S3m7>1GK&1)c@.OJC^Ie!oM)9+rJ)^erZbm5hVh#(2*?uT/E\UWt?Yas;cM<Wb+KWu3cVH]tUTS$=i_8A5<>ZuK_7o66'e:lrZ#.<5%eWp[*g%7UtcuU]mko'X_$mP?3q424_&Do)S)N\U?!7eUZ[#X1.?C[XW@7[pTs8:(Ed>D5#UC3g3Yq;0Z/Gegl'k[RRX`GIhh0f62r0g>t5FJtCq"]p6GW,Hkqa0A/hhrl1VflfX2%!dT^Ydu7a6X4:<;Y6L(+[lB*lo4$Ja]?tB<YODIkOHKg0"rABU&BlYVoc*i7#Ta`3C@+$/ku>NkYW]'022qU;I:^.jhdN_8bq9HPP"9jor.MX&W:Q$0kYFFem/2-FcK`q."e.!GbD+hhH78G':)*PXPj.[Gnh1'm5kM>QW(E`u%KRO8hS!(8n>J`O<9'6rkS$rs,X(Z&K&Zo58+u6YBU'h]cgAG%0Gl1rd<;6K5n<1+^<CRp[;EY@8Lqlae\2Ynpl:%l):`l-eMIfoM>nr89fm%p*AaeijSu`-skoF(DHmhF3eM']K6)E*9KNnec!5ec\04i:!jWg3//KjEf<?8`fU,AE[nNZNhl%C-b7B=Z1*Ik\N(2NWM0.G8gtFqZOAPD<;>50C#teWY?.Y$s@.pArlSh;,f%Phgq3\q`Q/L.o7ECWk?K4`pMijamEnRb\#a70b+;ClNU'Gc\u0ic\@-%C+p,Pltki&\M".Kh%TO<L&U)=Qg,3]@"2oCAd[POK&B1Unh]a6(KjG:=PDc"^s?25[u-'?EI>(sIA'._*7>69%MM'Mb"SqhKS[Q<jAW9,@M:$*/a1UL'ico0rC<c9%/:dknQ=:!),Q"TE3nip@(;i!VX%^gVCO,Djoe0ZoSr)_\EQ/H`K*V?Dh^`jPM$XirZ]=<3!q6g@?JOn]+b8[5/O$/#!O>EZ)0f"=Tg4GG+H5Pp1N$'ZSGG>?neNo'Wg0)B-Q6Ls*uG>J/LkM.+0[D\/hN2P#N#a4)'RGL6f&r:/W=@oPR]W;1$,^651^/XqiGpbI#H,N=B453EAs[@mef^:<<g&:<3/K_*:AI"&%*_;]?YE!'2)m>8T@!?<Z@KI_b[)[+=B1bD)Q@'?#pkq&(`g\84jj`PT))2D!<IJI^g8B_H1CZ7^0tI\7n-.UNf.=V5S(3sTnWU2YDV4bIqe6aS>^##+/o$1i-eP2f',Ue&9m]#ne5i&!mPAV&s#cQ;+$9ba-3"]9ss,0l"[ndjg'ro>m\Xbh^(q8lJ5bobBp:;3at$GQ)0PHUdpJhQQ0p*%)InP'O<`%9sPEURF.JrtMdrY7pG.0Da!jVZ[QRjm8Bnf"^HK'tZ<cWXIE]ZphhTY5sM'XIk1*(%rl0l$ij>Z_KE]1P/DI>UKkOXkPNl$0(,jGCAPa!76&N`4/*!q6QM%f3?`LF\.cU=-s,]l]I=bY="hTaS(dAKYSKeo7nKS&[>@Ji,3c#pQga[.]f\:EMgoP1o,tNTmed7LAtAZ0"U55!#;"PtBSO!V=]^,0[8p[r2Ufqa0qLI;1Vr48iLSQ,1?A`)1P?*(YhqEuV3D]3TPAe"<"oAfO4l]M&pSi')@3W=[SE-+WH?]a-)&)6a_(h`FuB9`R1&:+/;ON`301^q6=gX<WsImiKgIYNZQc\K7H#0"[ZXnF,hqbmNYqf[QI6St2[ZP<4#0bl#uKJ@?(%*GLnpmB,E:?Fg.`71>n&Gu4d9TR&T$(&>9!D17kh&G\g%$2,`i/G2DI"Mf`(b'4g.:C^=I@&1.qU&X-PpF"`ee7;!uqX<h''*R6_&e'(j(rS%4R&*G;$bNnD$=SXaKb8pYapMGi$O@S"$[>Z&+AAjfi)R%4:p@6U=2b4B^rc^b'aoi`.LUKd/I75u`<Of&]\$sNiL#gH-<i,VMY03gL'(#03Eajcf&8lW,u2bmNh29`:lrsR(`H70-W(EjU8Z6!.@^`]+e<I6AlFqG-&U?%Vma".JBIeCjk"%lPFHcq*WqOX1H2M\m1UX9N.U!/JS:&W/ERsRQ@QfZc9[92_cp!/K2CUXYVTDFNAEoQ+N,?A/D=a*%_g1dc48a@c89j>`j.\T]Qdj)Gcb0p8iSKql>'L/3tK8X_%Ai(Fo9[;oGBIJ=s&-"'E=occTX1U7AOl$N/0SC#`.4iDs7P0]g3&]Ht<m_jF#FV?7s;I,k"#97!im>`;?"?MYI]mc_g'-AVmBNrAG[FRGIr?43C<\QVMf-b;5t!#gtg_D2T,*U>s_TFF1\9D1Ur2a0pbHVZi&7KDbf$=9n*EHt^R4jYes4l^^5q!-/M?%+\N*`3SH+7Y<YI4.[<olhk<?%Rf[U$(WO!B<8a`U0q3Hg<[,96hM[C=Tc`&dmNS10Y?sXVrU!;1M)?gM!,/TNp"@XmjUC)&I]9iBJXtbFH(St02=a/-p?s8&W*9L[fYrf;=a\uqIaXm,i3UXpZCP8I"\_p,hT.Rp7VunjM>Jb3GZApSbC5&)RR^8`:U`gT(OKmqo#=d"-/,D1U:)8fQRPs1I@)"qGG+9FUKmnSd=Ca+2G)7l]4;YRUE%7K9bJ0`G9>=r>_lOlXaEN]Fd7V]9l[a$_EUnn@W>.n).8Qgk+^[7l:4;a2k!RB04EbPo#-pXVb0e20>-FC;khB8E6M9i:X9@D^RJ.k7_?QGr_;;N:/^Slg30"XMH_#lm-H-Qq!bg]^2Cq:6PiI;ka3&722_'ZoNmU($u],j3WMmL^L$c_C3rVTOJo0cD#:KJ!ecs!<~>endstream
endobj
xref
0 39
0000000000 65535 f
0000000061 00000 n
0000000135 00000 n
0000000242 00000 n
0000000354 00000 n
0000000501 00000 n
0000000649 00000 n
0000000796 00000 n
0000000943 00000 n
0000001090 00000 n
0000001237 00000 n
0000001385 00000 n
0000001533 00000 n
0000001649 00000 n
0000001755 00000 n
0000001833 00000 n
0000002039 00000 n
0000002307 00000 n
0000002513 00000 n
0000002719 00000 n
0000002925 00000 n
0000003131 00000 n
0000003218 00000 n
0000003552 00000 n
0000003626 00000 n
0000003736 00000 n
0000003838 00000 n
0000003968 00000 n
0000004086 00000 n
0000004227 00000 n
0000004359 00000 n
0000004477 00000 n
0000004574 00000 n
0000004670 00000 n
0000005635 00000 n
0000006372 00000 n
0000006600 00000 n
0000006828 00000 n
0000010441 00000 n
trailer
<<
/ID
[<1627e3f70ce4b1d1228423dc24c605e0><1627e3f70ce4b1d1228423dc24c605e0>]
% ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R
/Root 21 0 R
/Size 39
>>
startxref
13792
%%EOF

283
ROADMAP.md Normal file
View File

@@ -0,0 +1,283 @@
# Android Cast — implementation roadmap
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-18 |
| Last modification date | 2026-05-18 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | pre-requisite to |
<!-- doc-meta:end -->
\newpage
\newpage
---
Last updated: 2026-06-04. Living document — refine as alpha and commercial tracks progress.
---
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Alpha definition](#alpha-definition)
- [Alpha app checklist](#alpha-app-checklist)
- [Alpha backend checklist](#alpha-backend-checklist)
- [Remote access (alpha — RSSH)](#remote-access-alpha-rssh)
- [Auth & email (alpha)](#auth-email-alpha)
- [Product positioning](#product-positioning)
- [USB-C / HDMI (roadmap D) and USB-tether cast (roadmap E)](#usb-c-hdmi-roadmap-d-and-usb-tether-cast-roadmap-e)
- [Commercial (Play Store prep)](#commercial-play-store-prep)
- [Crash console open API](#crash-console-open-api)
- [Stream protection (FEC / NACK)](#stream-protection-fec-nack)
- [libvpx / Opus / Speex](#libvpx-opus-speex)
- [Transports & protocols](#transports-protocols)
- [Receiver / sender UX](#receiver-sender-ux)
- [Deferred / post-alpha](#deferred-post-alpha)
- [Related docs](#related-docs)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Alpha definition
| Track | Goal | Status |
|-------|------|--------|
| **Android app** | Stable, feature-rich, **demo-ready** on LAN (discover → PIN → cast → stop; soak two devices) | Core path done; polish UX/docs and gate experimental options for show |
| **Backend** | Deployed **OTA channel** + **crash ingest** + **on-demand remote access (RSSH, prod/hidden)** | WIP — examples under `examples/`; WireGuard v1 lab path done; RSSH implementation in progress |
**Alpha app** can ship before backend is live if the demo is LAN-only. **Full alpha** includes OTA/crash URLs configured in developer settings or `local.properties`.
### Alpha app checklist
**Runbook:** [ALPHA.md](ALPHA.md) (feature freeze, defaults, sign-off tables, log script).
| Checkpoint | Engineering | Manual sign-off |
|------------|---------------|-----------------|
| Happy path: UDP + H.264/AAC + single receiver, protection **None** | Defaults + freeze in code | ☐ [ALPHA.md](ALPHA.md) § A, C1 |
| README matches `CastSettings` / `CastConfig` | Done | — |
| Fix misleading strings (camera permission vs unavailable) | Done | — |
| Two-device QA (rotate, background, notification stop) | Script in `scripts/alpha-qa-logcat.sh` | ☐ [ALPHA.md](ALPHA.md) § B |
| Cap multi-receiver at 1 for alpha | `MULTI_RECEIVER_ENABLED` off while freeze on | — |
| Hide QUIC/WebRTC/USB in settings for alpha | `ALPHA_FEATURE_FREEZE` | — |
| Receiver stuck on rotation / STREAM_CONFIG storm | Debounce + idle grace in code | ☐ B4B5 |
### Alpha backend checklist
- [ ] Host `examples/ota/v0/` channel JSON + APK artifacts
- [ ] Deploy `examples/crash_reporter/backend/` (see `nginx.conf`, `README.md`)
- [ ] Set `ota.channel.url` and crash upload URL in dev settings / `local.properties`
- [ ] **RSSH remote access (alpha essential):** device opt-in via `heartbeat.php` (`type: ra`, `opt_in_mode: rssh`); outbound SSH reverse tunnel to bastion; **no VPN permission / no status-bar key**; operator whitelist + session open in BE UI; E2E validation per [REMOTE_ACCESS_VALIDATION.md](REMOTE_ACCESS_VALIDATION.md) (RSSH path TBD)
- [ ] WireGuard v1 remains available for lab/E2E; not required for alpha sign-off if RSSH meets owner criteria
- [ ] **Email infra (alpha essential):** `info@` / `admin@` / `root@` on `apps.f0xx.org` → Gmail forward; SPF/DKIM/DMARC — [20260607-2FA-email-mobile-auth-flow.md §9](20260607-2FA-email-mobile-auth-flow.md#9-email-addresses-agent-config)
- [ ] **Registration + 2FA (alpha essential):** email verify + TOTP + mobile-friendly login/register — [§10](20260607-2FA-email-mobile-auth-flow.md#10-registration-2fa-mobile-auth)
---
## Remote access (alpha — RSSH)
**Owner agreement:** production **RSSH** (reverse SSH, hidden — no `VpnService`) is an **essential part of the alpha release**, not post-alpha. WireGuard v1 is implemented for infra/E2E lab work; alpha remote-debug UX targets RSSH.
| Item | Status |
|------|--------|
| BE control plane (`heartbeat.php` `type: ra`, admin UI, RBAC, purge cron) | Done |
| WireGuard v1 (device `VpnService`, UDP DNAT, `WireGuardPeerProvisioner`) | Done (lab) |
| BE `opt_in_mode: rssh` + bastion/session provisioning | TODO |
| Android RSSH mode selectable (no VPN prompt) | TODO |
| Outbound reverse SSH client + foreground service | TODO |
| Operator E2E (whitelist → open session → shell/file on bastion) | TODO |
Implementation notes: [REMOTE_ACCESS_IMPL.md](REMOTE_ACCESS_IMPL.md) · proposal: [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md).
---
## Auth & email (alpha)
**Owner agreement:** public email aliases + **registration with email verification and 2FA (TOTP)** are **alpha must-have** for the crashes/build consoles.
| Item | Status |
|------|--------|
| DNS forward `info@` / `admin@` / `root@` → Gmail | Ops TODO |
| BE SMTP outbound (`noreply@apps.f0xx.org`) | TODO |
| Migration `008_auth_email_2fa` | TODO |
| Register + verify + TOTP enroll/login | TODO |
| WebAuthn / passkeys (optional alpha) | TODO |
| Admin: recovery email, clear lockouts, audit | TODO |
Full spec: [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md).
---
## Product positioning
- **Primary:** WiFi LAN cast when **USB-C→HDMI** wired mirror fails or is unavailable (common on some tablets).
- **Not in scope for v0:** Internet relay (`gateway_url_hint`), TLS on cast protocol, WebRTC as default transport.
See [USB_HDMI_CAST.md](USB_HDMI_CAST.md) for wired display vs WiFi cast.
---
## USB-C / HDMI (roadmap D) and USB-tether cast (roadmap E)
| Item | Status |
|------|--------|
| `WiredDisplayMonitor` — detect external displays | Done |
| `ExternalDisplayCapturePolicy` + developer UI | Done (policy stored; capture still default display) |
| `ScreenCastService` display hot-plug hooks | Done |
| Secondary-display MediaProjection capture | TODO |
| `UsbTetherCastTransport` stub + dev-gated transport id | Done |
| USB tether / ADB reverse discovery + framing | TODO |
Details: [USB_HDMI_CAST.md](USB_HDMI_CAST.md).
---
## Commercial (Play Store prep)
All **off by default**. Enable only in **Developer settings** for internal testing.
| Item | Status |
|------|--------|
| `CommercialFeatures` gate | Done |
| `InAppPurchaseCoordinator` (Play Billing 7.x) | Done (connects when dev IAP on; SKU placeholder) |
| `InAppAdCoordinator` (placeholder, no AdMob SDK yet) | Done |
| `PlayStoreIntegration` (listing intent + in-app review API) | Done |
| AdMob / unit IDs / banner layouts in main UI | TODO |
| Play Console products + privacy policy | TODO |
See [COMMERCIAL.md](COMMERCIAL.md). Pro/OSS module split: [PRO_AAR.md](PRO_AAR.md) (design).
---
## Crash console open API
| Item | Status |
|------|--------|
| Workflow triage tags + multi-tag (duplicate+fixed, etc.) | Done — [OPEN_API.md](OPEN_API.md) |
| `GET /api/reports.php?tag=…&tag_mode=and\|or` | Done |
| `ingest.source` / run metadata in payload | Design — [OPEN_API.md](OPEN_API.md) |
| List filters + synthetic tagging | TODO |
| API key upload auth (per company) | TODO |
---
## Stream protection (FEC / NACK)
| Item | Status |
|------|--------|
| GF(256) ReedSolomon + `ProtectionEnvelope` v1 (monolithic) | Done |
| **v2 per-shard FEC** (`FecShardWire`, one ACUD message per RS shard) | Done |
| NACK + `RetransmitCache` + `MSG_NACK` | Done |
| Post-handshake negotiation | Done |
| Backward compat (defer FEC until negotiated; ignore foreign FEC/NACK) | Done |
| Default protection **NONE** (stable cast) | Done |
| Integration tests (`FecShard*`, `UdpCastTransportFecTest`) | Done |
| Lab soak under loss (HY300 / BL6000) | TODO (manual) |
| NACK rate limit / backoff | Done (`NackRateLimiter`) |
| `MSG_FEC_NACK` by groupId (optional) | Deferred |
Enable in **Settings → Stream protection (UDP)** on both sender and receiver, then reconnect.
---
## libvpx / Opus / Speex
| Item | Status |
|------|--------|
| `libandroidcast_codecs.so` + JNI probes | Done |
| Cross-compile `libvpx.a` (`scripts/build-native-codecs.sh`) | Done |
| JNI encode/decode in `libvpx_bridge.c` | Done (when `libvpx.a` linked per ABI) |
| `LibvpxVideoEncoderSink` (surface + buffer) | Done |
| libvpx decoder on receiver (`LibvpxCapableVideoDecoder`) | Done |
| Surface → libvpx without MediaCodec passthrough on cast | Done (ImageReader) |
| Opus / Speex cross-compile + CMake autolink (`build-native-codecs.sh`) | Done |
| Opus / Speex JNI encode/decode sinks | TODO |
See [ndk/README.md](../ndk/README.md).
---
## Transports & protocols
| Transport | Status |
|-----------|--------|
| UDP (default) | Done |
| TCP | Done |
| QUIC (Cronet, experimental) | Partial |
| WebRTC | Stub (`WebRtcCastTransport`) |
| USB tether (`usb_tether`) | Stub — dev settings only |
---
## Receiver / sender UX
| Item | Status |
|------|--------|
| `ReceiverPlaybackActivity` (primary receiver UI) | Done |
| Settings drawer + themes + RU locale | Done |
| Multi-receiver 1:N | Done (gated off while `ALPHA_FEATURE_FREEZE`) |
| Legacy `ReceiverActivity` in manifest | Orphan — remove or wire |
| Immersive A/V presets (dev) | Experimental |
| Sender resolution UI modes (dev: Separated / Legacy / Combined) | Done (test) |
| True dual-stream encode (480p + 720p simultaneous) | Deferred |
---
## Deferred / post-alpha
- **URL shortener** (`https://s.f0xx.org`) — PHP API in [backend/url-shortener/](../backend/url-shortener/); sync to BE + `seed_bearer.php` + `libqrencode-tools`
- Internet relay gateway
- TLS on cast stream
- Passthrough video codec
- Lab automation under simulated loss
- Full wired-HDMI capture (roadmap D phase 2)
- USB-tether cast production path (roadmap E)
---
## Related docs
- [README.md](README.md) — **documentation index** (all `docs/*.md` with section links)
- [GIT_FLOW.md](GIT_FLOW.md) — green `master` / `next` development cycle ([PDF](GIT_FLOW.pdf))
- [INFRA.md](INFRA.md) — production FE/BE topology
- [../README.md](../README.md) — repo quick start
- [ALPHA.md](ALPHA.md) — alpha freeze & QA sign-off
- [USB_HDMI_CAST.md](USB_HDMI_CAST.md) — D & E
- [COMMERCIAL.md](COMMERCIAL.md) — ads / IAP / Play
- [PRO_AAR.md](PRO_AAR.md) — cast-core / cast-pro entitlements
- [OPEN_API.md](OPEN_API.md) — crash ingest triage API
- [OTA.md](OTA.md) — backend layout v0
- [BUILD_DEPLOY.md](BUILD_DEPLOY.md) — Docker build, staging OTA, deploy flow
- [CRASH_REPORTER.md](CRASH_REPORTER.md) — crash upload
- [GRAFANA_vs_others_graphvis_pivot.md](GRAFANA_vs_others_graphvis_pivot.md) — graphs dashboard pivot
- [20260602_REVERSE_SSH_proposals_summary.md](20260602_REVERSE_SSH_proposals_summary.md) — on-demand remote access
- [AV_QUALITY_QA_SESSION.md](AV_QUALITY_QA_SESSION.md) — AV quality enhancement Q&A

419
ROADMAP.pdf Normal file
View File

@@ -0,0 +1,419 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 19 0 R /F4 20 0 R /F5 21 0 R /F6 22 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 762.0394 100.381 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 59.68504 751.6019 124.2975 761.6019 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 59.68504 741.6019 140.1375 751.6019 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 731.0394 162.605 742.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 719.4394 118.157 729.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 709.0394 113.717 720.0394 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 698.0394 265.741 709.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 687.0394 151.037 698.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 676.0394 131.949 687.0394 ] /Subtype /Link /Type /Annot
>>
endobj
13 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 665.0394 161.269 676.0394 ] /Subtype /Link /Type /Annot
>>
endobj
14 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 654.0394 121.277 665.0394 ] /Subtype /Link /Type /Annot
>>
endobj
15 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 642.4394 126.157 652.0394 ] /Subtype /Link /Type /Annot
>>
endobj
16 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 632.0394 121.709 643.0394 ] /Subtype /Link /Type /Annot
>>
endobj
17 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 621.0394 120.829 632.0394 ] /Subtype /Link /Type /Annot
>>
endobj
18 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 23 0 R /Fit ] /Rect [ 45.68504 610.0394 92.37304 621.0394 ] /Subtype /Link /Type /Annot
>>
endobj
19 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
20 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
21 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
22 0 obj
<<
/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
23 0 obj
<<
/Contents 50 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
24 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R
14 0 R 15 0 R 16 0 R 17 0 R 18 0 R ] /Contents 51 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
25 0 obj
<<
/Contents 52 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
26 0 obj
<<
/Contents 53 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
27 0 obj
<<
/Contents 54 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
28 0 obj
<<
/Contents 55 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
29 0 obj
<<
/Contents 56 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
30 0 obj
<<
/Contents 57 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 49 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
31 0 obj
<<
/Outlines 33 0 R /PageMode /UseNone /Pages 49 0 R /Type /Catalog
>>
endobj
32 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False
>>
endobj
33 0 obj
<<
/Count 16 /First 34 0 R /Last 48 0 R /Type /Outlines
>>
endobj
34 0 obj
<<
/Count 2 /Dest [ 23 0 R /Fit ] /First 35 0 R /Last 36 0 R /Next 37 0 R /Parent 33 0 R
/Title (Alpha definition)
>>
endobj
35 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 36 0 R /Parent 34 0 R /Title (Alpha app checklist)
>>
endobj
36 0 obj
<<
/Dest [ 23 0 R /Fit ] /Parent 34 0 R /Prev 35 0 R /Title (Alpha backend checklist)
>>
endobj
37 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 38 0 R /Parent 33 0 R /Prev 34 0 R /Title (Remote access \(alpha \204 RSSH\))
>>
endobj
38 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 39 0 R /Parent 33 0 R /Prev 37 0 R /Title (Auth & email \(alpha\))
>>
endobj
39 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 40 0 R /Parent 33 0 R /Prev 38 0 R /Title (Product positioning)
>>
endobj
40 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 41 0 R /Parent 33 0 R /Prev 39 0 R /Title (USB-C / HDMI \(roadmap D\) and USB-tether cast \(roadmap E\))
>>
endobj
41 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 42 0 R /Parent 33 0 R /Prev 40 0 R /Title (Commercial \(Play Store prep\))
>>
endobj
42 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 43 0 R /Parent 33 0 R /Prev 41 0 R /Title (Crash console open API)
>>
endobj
43 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 44 0 R /Parent 33 0 R /Prev 42 0 R /Title (Stream protection \(FEC / NACK\))
>>
endobj
44 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 45 0 R /Parent 33 0 R /Prev 43 0 R /Title (libvpx / Opus / Speex)
>>
endobj
45 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 46 0 R /Parent 33 0 R /Prev 44 0 R /Title (Transports & protocols)
>>
endobj
46 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 47 0 R /Parent 33 0 R /Prev 45 0 R /Title (Receiver / sender UX)
>>
endobj
47 0 obj
<<
/Dest [ 23 0 R /Fit ] /Next 48 0 R /Parent 33 0 R /Prev 46 0 R /Title (Deferred / post-alpha)
>>
endobj
48 0 obj
<<
/Dest [ 23 0 R /Fit ] /Parent 33 0 R /Prev 47 0 R /Title (Related docs)
>>
endobj
49 0 obj
<<
/Count 8 /Kids [ 23 0 R 24 0 R 25 0 R 26 0 R 27 0 R 28 0 R 29 0 R 30 0 R ] /Type /Pages
>>
endobj
50 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 849
>>
stream
Gat=*9on!^&A@Zck"&RjlZ)ib*9h@M;FT+g6p:2U&[T`>,]!`jnaC7K%J_E*,dP4CmJ#ag4<>"72%>KDFU[cmL\iG%ADhA+3T8mLi/9gFS)D=Z/r4TQje.h^eFC&bR;"unjp23Jj%67U+A+>K,@iG@_hi"^$N/PD@"?+_@*BNSapCLhVVm4`SL7j37Njksq](&@VBCb?>93#T&+V,C+A7>@%X+J[Ig,i^Akfq#qeXt4aV'H/,R]L$6tl=3aUSn1d:h&L[no/jJE^!u`j#@qJ,DM18`Ht=dpZT3_H^*nY:l!?nYC/\AF)G9l-dMe*+>3t_N7@L!We^Dc(3h#X^&d-V)>%kjO!<f'YVcXN?M?3H0#lLI[eq,jbRg2ij(7WMkFJXh\7m1S;raJ6g_Zuj]s:OaudRAUpnr##mCr:;3#)a'l4Dl`EYpfq!QcAFaFtD]#"1cs-%%Xp+h4\a\<\@Dh97e+8+Uhblb/1T,QKXr>t?#QFSSp"?1Kp<J0,sOQf\1%"l3@Zkr?E2o$i*1Fd##G3G6Eh6=,?4\in.'<R%+@(<%>)rSUST?`$4\oj(G.TALeF)V?PA>S1+(?NTkp4dY/3fFPgLPKiQLK8]('-0gf-*YS@QV?lB*J+O.i%\LZCk2W_ES!SP@/aH/B:431R+.:7aO:MD^7!mEKXa]q@B,Gj^\hgR(AN6*qW$jTZCb?''iVJ'5o[_K[]m4$$BZn\l;Oc.Zh6V4MH2./!V._!C-YaPO:?lE&A3SP'uFZ-S=6UXX:g(hSIWp(c6).ST<AuF&g`\(@X36UieU2n]5PBS*g%PjB.YH`UBnFXs"ZkCU0A=r;hTL2js?Dm".qZ%Oo~>endstream
endobj
51 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 814
>>
stream
Gau1->u03/'Sc)R/'c[5-XgKIHZt0q5T?J6K=$Qn(rmq*P;omS@/k\rBrMq'/YGe"@OlWm5(;m?:+1@3dI&W%JE%&uc6uKnfa;CX!m&*kBf>fJU:ZgZ8>7C%!f5m(G^^RhHT4^3K__;P`(qU2:4U8+9*5OtG(ebKHPu*7<hflRBe\Z`%PiK38PgoKWj/k7Adm?'a?DZPDg8sm+*m>%pUB]-P&>u\;kI$+_',/GZ70XiRNCJIgVmk,N.d8gZ0SPR5P>5mD*\k6XK0e%*Y?0fJ@OQI/XuZ4+=nOJ/*=-<<WX(()?\.bhD$T"Mda>%S1hoGBP?sik,%->BeTC.>]aLd$hs_-k%42kdgO!=:Md[Ya@rX^,:p4EFD%prDZ?g\V9r-9f8J\,'0(U8l/`(PTG>dghSO@p#*,V"%9mNSWBkK5T?K:^@hddT!^0D'/[[";gSn>pJ&/uO/YU_#m=c/XK:usr$CAnoE`oN0%8:@M4V/fe."m<)P1R3gN$dN/%@V<\l8J5!jQ(A85=.7B9NBs)[1B3aeSYdRMcRuU*WuKa.ZcI2)q-9R%>M2F70uue-o-oL=HT"E+j2s+P9VcK?a4\N]a"NX/@A778nor!"MePW&%cZ/K>:-7Z_6:n!HKL_GjdBH`u]B?Y>,@%8s8!5,RZc(7MMH'0?>3XT+CJ#ppgV^ACO'Vq$H1bPNh1>7d>A*GK_[iWj5bH\6!tcAHT:');LMJ?>9SNi"jP6FU&>qXOI\k+l8#ABgZ4.Pf*U!E<OVB/a;-!PXN-uY:'7A*tt*9nLPq/V+J.^",;/kr1O4fhaVHNHi~>endstream
endobj
52 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9$AT&MD_`PtL;Egp*cQ&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:ps&&=X(PCeL]7-s8G,Z6b]&CVFJ!om<]_Oaf@33/!:]XjY5-C*Ut>641HTb(JKc.+9~>endstream
endobj
53 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`Sh(gXlj:fa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B]'raq*pZH62`G1DsFgA+6hs6uHU'5'".RbXr2MH_7OtUjbJt$1U9V<CL~>endstream
endobj
54 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4064
>>
stream
Gau0G>?BQM&q9"FeC,IOJn:p(%kh^W8NamB`[#VKg#F<Q[]Mi]*(bmh^OE5+\#_*D4X>R5V66ud\pRN6#f,Je&:HLH!9X_,>m_\c=KFcL3!g@%0O:4H3"Zcq8`g^]js<iHKd47+id3%[.g\&#;\Oa)-[[c;Y>:76ODuS:%#\u@;i=cKm"6f*>T#g5qjh;OeG9<$-d%OkIhDufp5A=@_=GhKQF\5%&u'tR=11c<#f(D&)ZpN#!n5nAWGgLF"/0Y#cN3B[Dsc!ri'+s903J.l,l:cSGf"/E0/Q5?F1`noo`V:PqVegd!f7R*V1F=ME3^n5o`U/l.Cu64&BLB=&CNfdE]d3a"p1A0FOIrk<]O%.`!-:"PDRr-JrkrLa.[ba!j`AA5FTOSP!mVH850Bpj(PbN[eY)[HZ&5bU7un08OX7"]Js4BU+??nl3a_0^o9*:-%T3uMm'*8/lLkg5SP%B+rV?#I`!Vf=$iXC!f8<N,[<0s"Qc8t9;58U;DrISK*9]F+39QHMC:lf9>qC+9V=o4Zs`T7WuOE+3*t_>G>.qf1S!0E7:,-[8Pn;!msSl&qk,cenT>U%l89gpI2S&NbP,f\N8:XB:SLs"X+Y-7lER<8Mp![e"aY4Q.BFp)k0p#BS"!?4=Cj!#=ENj<I;2l&aZqW+!'D(#`]*/)#*/l0%SZ>FqWS\l;4'it.u,q!T>TLOVV_b.$cB[7#XWHR#(^DL6hoqX/%4Z"D:7A46bB`sUhTG[OAOptn^*&77_$:Ai6jF6H&`n]i8MuoVMQG<#&DF.fkT`s*VF984to!A3u0jY=F!(5)t11`Kkufsg=82E*d+T.Rj,O?l@SFDmu:L^/%T#F_;#m19:`@+I4Wj9^4CUIHqK%\LcA[^:@>Y[Zr*\q+&+W2KPTSE#4OU>hqroDc1HpNef&CAbG&m@*UbZYpXriBputRa]5qWcHe6ER5u-"#NfH`;SRj>MDYs(thXE+A&\p,mMH7Z+ap;iB;.ME*>#8cJR])>:GPqKm$)\A8_GF523WFJY.-EJ41#"f0i0+9;d/[POS3Qe9cfkcNe^IoX.Lb663e2LR0RushJ09cT4gGC#LMT;>=5kfA_q@C4r3[#..?_IY=fquA$K3dm1'A/V*Mg3,RA*`9!ReKK+\<H]>kDZU#>]H\ikn^]MI6\IfIms*'ndM5/EjCc0YS#[f6QCs+,MAVgcJ7>/'AoKf6$^%)BZQG.!DGBq,Msddp:XB%.b7@'+B<=nM&ld5f4.]/HF,CI4M^*a"n0[6_cujd&'*c=U3/#1IPQO74lO4JsIl`B-1*Q&Ru&'T.teL*B568<i\5-aM)*Ck'QjEo:A13[5u%5q_I<B(.OQ7>JeO\oY6bV&a:0[-O``nYh/<i>N$@']S[djhAQIE[s-fkLO*7&3)Ces@Grdt:lr(c+=KcLY_\+C'+u"8=S:8(be3G*R8pqC@Te-"]OErL-DZcH/%cU#9#c`>4(0JJo5(nmB!Xo&Np3J0)QQM!%.:\iRD!,0CW"sQ&4pa8h69s'EbuCgQV"rbbJ-JKmh7fgg`])\_@]3TNQ2_14[>.#*&m]CU_l/k;jR>JC.>2,"=\4BGBVUBYtIf1!:h`TkJq/QnULdEe$J>Mja5FFgZVNanocOgDB>b_DYj,B-:.fZ'_n]6Tm:kEe+(EM'XgJe;)p&BRgGh\q]r)(dG*kE__CXQPaXZ"O3i^"/,,rUj7BD:5;<7d%=6PCMnR8ZiR7]3><?f.e7-+e+S;G9r&?Bi/bKRg'TBp1ZgO.gh[T:^nRC,\9/mBZ2H-=+V-%tt#5ca$U;/Spp/'],eJlLS*IJm&hCojUI<?H:XL7L?(se*@f*gu2D8s)RQ%rY0VeB(tU9VRqP!=j2,qk*P5qn0=h!g(R7&YX:iIXdZc@tulNuR6k>9bSFpNg<8@#JIA*i?alN"DskFdn$9k4XP`(8p6!nuj.<(:3+M3gn:-:$9^ac1q3OH?%HR5eQg%Z(2TUAL>jP\]`_/]iEHhp7um:qI3ZBmglbHkW=3]0.*HLqf?@g)6WG%jA+LlK,,3.RcWXQ]Qff>Q^@=.oO$&W[<6C:gXOe9=Nj]2%)T^Rb^*Dh\7"9'"SW;JES&CUmEObZF(Pt#-N%_I/Cbh14CN?KYk8TE$Oq+C)eL07d?e:>G]5:/cBWJHDfl[pPu=REfjgpd6;CY=9INZSWgr(1PW'L_.8q5'a)$PrkV]TbF9p'S@c'Mu#-^'9%,b(8[l@K@QnNt0F?0\AChVlr<Hg:Jn_0:`6jP/S>5cR#W,!9e4,`X9@F:hm(H%&uX10A=3i^8TE^fsUW%.]arW?N?]^:'q91.SW^$p;?/T%u8/P#U7rq6Q0COGj](d;P\_[NSLAEANG;RO+4/NiHXeh0,Nd4B^WoDF^HUI;pgU-&ne9$<F&)Xo,p>!"&$dkYJCg+g@/#TI_YXp7Dn'cVs/6$ZJ?D-XA:gtm7t1.N^h;%%0#.Pr^-a\'-GH%$]t-`q2g2%(gqqMc9&93>rWZ!Qfp;`[)M`o!a7g4o4oiMb@m?s%=`%T1dngF*H>TkMA?']J%hF4=WG*0dD>[iPDMI^r=#A0K$4BtMP?jr>WW&gUYfPV_h&+\PfN`0+R.%!',tmNZ'H4U_r(bG8c^Kt&1O]=TLm1*aT\K'6TZpgj0h9]&HYOX\]tR!T]E$RpuoX9h/P$#hS5I%O3n@<MiR15-O^Z)p!Nj."`mk0rYN`H4,39W%RYbAZFAfu81j]=j'5HI6%fIJ1YU744uU6GY+NEc:@da&`R&P<%k"UTGBA)aIHP7GnW))kgMLW2^D#IdCq&`6D[qhKH8-dI@3Q.1HCK?<r4C=D)#@Z0T55/eH@\H?m'[h:Vt/.$0)YZbcdPMsZjI&mt;1=f+3"Gf/7hDEF9TH/T@#G>t"K]3W4Spn'c2cX5;*:F^9qn`94Z#X1a._!QHe>s<o<]?AU@+1(:\`V"I>JKh<=YU_$4_Q-%E3?Bq[E/%Tc$#>`uYn&e2Q/fQT'q4!nTg\'n7J\J=27c6foOIpt#e8;X)]M?N(bk=,)nrbj?*LbD,2LDN1$(E6XC6"Y,<oXg`Z(JG0kgg*K=IAm.C*'*1PNnj7aUa%a1O0`8'>B`f-O`7Og!m'G+<Xn\[&K[!gT>M*@[<qD=1&VcTD@OB8@gRZRC80nW@K9$jPaT2gjAjpXEg^kpRRV""N>e@qGi\:&PXHlf^Sa<gZZ'C'<Xd:8>V56[1PKaKh"m_h1eHQCW4HnLobM/2ftqUp$BiI$Sb])`G(eds_`:ImRiDCDIVP52&eA"H;aD?F??Bn9XEjK<DlM\3t'r/g45rgQ"q\f1V?PbU\L/o<t6qk["$g#A4aWOOb6/"@4%te+_)`2fJPkp".4K)MJH(&B0F=LMl,h/j4r/eY#^?9UeP6:JDf\b*30r:18T8St*Z8O)C)jS*>8o=!KJSC@\QF,(hH><nK]OGYG:2F9l3P-J5TO`Q`Xi[=n.\ZDm:@p$[:s`F_Xs6'D$`>r8C6n'MilKCQ&-OpX`Hhc;R;=D,FAh98H@r$Y"g]2T_DViI2;;X[B5iqDB?WQj&(o:90\MfCIDMM6[Mc00[!eXRC#^:.Zdo^*jq?U9T@6>p5B9l$t"('i<nftHF,/1o<FYQH2r9tsQ(rbH3%Vqs2*,p(7kO+Y*lbOB\ACOT,4R$qcn2hMQ^SC*4b!oU,9eT\<S?_k4R/QH+ACg?6G>H/QL9hVlJ&]0gM#X2h%;<a`2.cme[K1;bD\C,N(=u#2+%X=)*LN(nP9j>&Zkcr:`Wj@J&e[7P;6-qVhPO!/"$bXJn#sn$p#acW=^dB)7K26I=Bp`2`?,]g5U9[=B5hBu759I[EO"BJCDq"O\FGXnJ9S\73Yhs=-rs`?%E61`1'onW7L+Dp0)],O"03bWdA*"$s8F![[o'''@8:!GB^Kp<pT,cTZemp`ueeCF?*be=1(G"$;)oX3>U$:5`CGEgd/1X:*_29g8T9eZ".u`j:l]mJ";QaW+_M"1:;<*fDUSkfm=?4Clgq[uE`LhKrW[%iU*QpZg`#19p_bVCf6)V#eEe9%R$'@6FoZ18B15nI[hubA,"$RMi?2~>endstream
endobj
55 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3214
>>
stream
Gb"/j>B?:T&qJm</,JLpP#rP*cm;n#B4XZ[nO\+29<Pq+8iqci1biH!0X@I'IsM[iJhDR_D`1TJgCc47m[<LHB<VCtrYY1'1Bfc,_qB_E#(caZ$qmq"rrp"4PQ.0j8^:L,dnR`7\umZS()Fee9lCTl2F0-g28.iHX_s?"AO&4=nZc3F!6]7p`#pq9)CC+G7dmTSooiq&69DJdEHN%-^QUuA41AOc*)WombcM[[b1DRI%b2LkT0POT51pR^#n;;+DEWHS-t+Z,LaAq!$C[uBj\iDK46V0Hrd.YuTD5amOI/G:l*Y$dm7Ad?;5WLKibBD!+%WNJBQ1SS\GBdqZpq!9Y'Vi8iA1pKLmpQ>YSVR7g`MA;P%R@s$Ck[K;0U1]r+r&<E[t43DllZ?5j[3:^;Y0g1o9!NQ[('4K/2>_q'LojJ((s]>IOpI<E?9c?0GSeDaDV.K:)He$n<-Op`a7"2ueL76)9M4n)#u7N\_:d]ZVC9g:-&Dc5A1eQ]d!7io?+m0)Vg%FhKN*1H?=.V7[rhdJWO1c+hnU8>C!TGAPJJa[5b&:FtS%3F;>r:n,Z>L$,"P@a+fKOd'cDB-c[_8lk_P4g\&b4tap,7aWHM7lV/NTThl.TO'=G74)15QLg>=[_A'51EY#O3s'O[LNl:ZK&\R(Jg['Y>\VS7d$`'g!S+j0E;kcFhbLN_WEZe3g$cOK-^kLjWNVnr]G04Jd#/S0p#l1k@ssV\/][9od*]$Ci*De%c?q2En'V>\iG/aEQR8Ap?;&)G1]d'r'(@hS5bGZSJ9QnS6CJ:%X%k<dP"u%Fd>_^Q[>(fY1XL!@?8l=T^,&-YBko!9YZHT.pdC%"fNme-!@mYLk'7LAbkVN4-eKTU?6pDiC-8,g;t7Xd<`M&Y3qC4q2KcF@ZY?O/7'k)>*NnX@HH*$)L?^'.$c>TrqfU).)[^fO8-hW%eXGV!\3P9-Wk"lb7#")\WAKq#=g^4ClLiYcP=6\hJkp)?s1%\rMoY=P-VnIp+o@`h=2O.iV0*4WN?gV^R%J6Z^3<n#0<&?cll$_&9[\DO=-qUS4nFcG0]c^k65ZD6,JX[._9AfQGY9,@L8N<0djb,Fr/JXZJQ.\A1jE(il__?oX%/*D>"IT(&Oust5oDfL>9^iSd4b;hU*90mI%)V*<3g-t+3m2k38qTqPTRP"`=DRjG[i$&Tod,'BLpNLh"BDe,B2=lc9)CgUja#6:_o/sYuXf+jGN:Y#,q<jdc.LelLf/E`PZ&]d,e,tLADT)2;cn;$OqW"L/+h`CY3Oib7#[5EBL;L"02Hs`^57#9EXJeh>p&#<6:<F$1^QeP?!Y2!S<hGN#!>L<PRAl`j^Kg70?"@bPGSrU?jG^j:s^$.Dfn'2^"oV<^\]b>jT).=,<<e1<t(973]7%nC`jjDd.k\-G<0ofqs=jL$VibgmMeFqZ2KK`&tE[+l$G=7_GUQLK5RSRth7qcJ7=(<lCQGH]CLX)XTr:#7O+R!,kLO4XXdaDkhI?Z1*T3/1ZD'85Fo?#2X0T(ZM]MY0.&s3P$gbF7h5]n6Nu%C-%/$k&Q@?oaN9&O$.&*l(8g^("^r`o[&YIM,IA=p#&tq7<8++R(--"%.;9i"*\)s)U-Z6g%s'.6K0fIT[c%m$]\(m`7.1K!lPU'$L.0o0Y^&/^HaVh@;OIJXkI)8ZPl4fP74;@ZbrRG%6eY1BE;YNl:oO[h^tn3m972KYh6sGKS9n+QT;#".Wa*<"s6F-"UD.*Wa4*/AM\?/Oqp*@9KXLg,C#TRXsF`QB[HG4(eXS[>_Obt_O[N%N0oj>%&7VeUCa'p#p)VQWD*3.T(9$uOZ1d<6f#KZF]V6+Jl+r:;:^dXkP7aNmA8\IK%jGl9PDAu6A-uckq7'bR%/4c8mI?Pf=o#F&O_5X87"JH<j^Kf66(R"eh('ihk9`>*Y?=22A)P(_AD@f=<&peTHMfh56YCZbInN!d-H(63Kln-nnMuFI%3fsZ27AP7`cOmp..=fgYUR"q\$`%qc+O;MQ&R^L=Ya'Ol;U]hjiYkVp'jPc<uB8.9q$?]R6A3_a6UKW:L&OVkt.mM?IZYs3)]oMf%EC_G^FGDZrY0T-32j'LT]E'p-0"m4ZVkVV0Ja-8G7HUkpsk,f<K+X6lCWX6s9jZLpY+/RVt5G15cn'W2U>.h$^6r!9cU2oA\^:2RZS.P?YSfIsT<DNYn>=FEE*,Ej]8PXXPm7MBa(q2[)k.pJ62Lo?TH(3;0L2Zmr*f5BTk_62T&\5D^4ile<F2ZmsIqkM)2#Zm$/^@Lsr+Rt`;P-6'W<6'G16Vb\e3,#D[m1dG[?H!kk2J0I,*9=gE,Q=7EVnf+']d>#(c.&6;me%.S4tb.tI<";N:L+YY%JUf1QQhZV(/0=0j$8)9&D`A9Wr-Z&Kb&rP1>)'FG6`cPTH=dFoqLr.8:s4)((%q9XUWH0#\V3[)#h-H;T/e?SU5r(f_mi4FCKT#7[<)gisS0nN4=Z5[$n;Jgm8[$\YJ<V\Blk\cfZ;_$Tt^Ti"ACOJ3&Bc+Kc3o`&+.dTl'1h#WmBAj<3e?oBh:'IkoIuIG;q%"SoqXc/sI'd-DTagA@SaW8#K(-)mRc6Un!Da`at>5H%h/ebSNRX8TTUM5k;O?0+*E@$#b+Ds55dqW>Aea&[A#?s1Nd@-)<c=cH]]s*QRVN$;$Y#W+Q9iZD]pAtD*=o$s?b"r1V!d.Q6Mk+XN.on4^Bb:=[?Y?g!;kC2#im)!AjhiiVl!m>$Q.4<n"@TT$P*T%rp$(C,<cH+q#!.'941f:G,j.bDebK*66JiPuZ=e/`/17s08X&f,3K,>.g$MTd_\a-(tY/JR!&P_;^cbjKZp[A^<%V=D'-c-m^&,)l*[])(9RKZpkYBlgqWhohOIlJK4@Sa>OIdlLfLkoSaJiJX6H=Ys?Z&j@@`H<$;9=OU^kK\ugSZ2.L3fifXpG(IE3:7jH$rkMek"G+oLSTt-&KoUYcj2l*E]HWEo3%)\>7N^WrN,%'kXtcb61UJ]RWi)0)_IrFp!%W'HW`l/Mg=ji;6TT&Ej5`3iWEK@E/i2PUA_ObrK^m7AA!,=NRN8+fZ,J&pg8U#CqN7h`S"A`d5kABP/6),PMI4K/[9r+'jn)0cr[m7gu.#NMK]Z'em[<:_=kmoASnj:BPgS[eTbN":</bh[8saF"'oI2HNb5U:*5csk[I8Q[TV4@#/Vjp2;9f'n`F5UL'ZSe:aV_1+5sYRVu~>endstream
endobj
56 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3164
>>
stream
Gb"/)>BAfF%Xua-d*l$-Ahh"=jua5BB%TH`#0?5(;c?+BFp$S<gLI^63PU$n^1#7)2T]k(_^T1!G./d*Um=3D/V/^".K4+#!3Tq\^6([D@C?;1'i^Q[?nf4fE?V/7oo:.EOp6XnifC7"O2[ml'<EfkS>:6me[g>`aZ?dS"Tf%SNu(,a(QFF^jmVgbS9s1f*P-Li^_AL=%&]o,)mSVtT&9ssSOD,i'2kt5Ht'E.d,iS\NlV8gO$#bi=u66"$Ua]<q=Qt0-pelU7_1"*h`lL=fcMpFaOu!Zc^(E/aFZ&0kA[$K?f**/%<3WPS7&#jg7fbX]3bNp&.h=IYr=SYPq7aekUi.O$d"e'o8$!$N&"i"o%J.6/gMe"^<r,&b<'5o5m^"c!#hZ.l,/i8f.0onTTph5SUR!a%/P%Zlk1OF]YZ/f,(F:"N1S@3cTS8qX[S^/rL,?Q&5_Zml!Nr[*u@762H3D&o(V4gKqQ8']E:RS,l>c9.Gi-i"2CI8jMU!FI5/Kr1ChaOX24l9b6Dpp'aF.W)3ErtU(5Hs^ISZ[/*TgZ"MLRe.N"Tt(kuR?'crs>)M4JS-sIU4J]a6Grgc#RB@ko@QRj*`[L9(>j^cBmPVQj_X"_/h-MK;oQeu@KhgHalJgh'Ufmk/hN1/Npcs7]qa/Put;Hs9`/3L@qON%lb\/'fJPM/ehp_h_Q])9ut?)/Yaos0/n0E`mR'IClSGY(\o`76-VM3?rp\u6pq?9J$:\Fo=h.)`<g:[-&"J[1rPFUJ$LYp1cf!eM%TEY&XRm],#T>pl?g%d=_-6Ghqj7Ccou6.f!al0?\7FOa,4>t$8;6*o*?`'NePFQ8$&T1Fc9XkY'9_q:'u9oV!WSFDaT(54'_olI_j2U2]?"`SemSo'l@A965+W#5r9=3_DMi0V&!htd^aQLE2K`TF-#;ridgfBG3<\buMZ9$?ib^5\8ZP^8Oa09G`[pMt@h#.Djpp*-d_X5stK6*->cBjEr7kGlW>nnsmNoVS)9QD?-?6lXa/?\LVIX.]84YcSWTbj'=.(F')PcTE<!1ll.YO444QP=k:YGS20n8.E&F-t`=VD,G3_CF>Jn]6t1rUL6YeMHaO%/J6=YZ$IbA]3/DQS#SPEG)=0?StiW^]^PNM.f.*H-mI6`Fn40&%\H>hDG/;RDgd5de7(H#.?"uAcd">.lKd,8CG0.99.cY*q8NZ3KHXPE@U?1I)L:8B<^Ur-e,;#VO;4<_@\*.u,?7<eJ<%0.&b^ac'/)R9s->cF+;+48C`=MWb<93X\&]tun*DH^F3oq2p7l/SHDlu()RJO:m[G[MBt%W8$8p"hi[:_![M'/f;+3fLWVX`[T*1#eLVtbd7%f5G]ZLEiAJPL>.N:Wh=W"rTC]O!8@0!M%6;H8ajL"OD[;;s8$T>+3$Bj7GNn,ABNhk)08'RZ*DL&@K(KXO-_6k?=P<n[6Fs(V&)MO<NlU"*=p#?Z6C-J>"Bf$3R)u<mV6MN"4pcR#HN]V_k)l]92i)ihr):]IQY+8F(BQA6ln[E])l5([kSLh$E1?=1i!C@\<Iq(X923-l2f4.L"(1!*E@C=J$HRB8Ec_f[55H^<Y3SCFnf!q4Gm:"X;j^MNV;YDon(PL30L:XHuZ`c+$0aX4AS9Ji)r^I]f=oY\hd8!iOfLNfK+#Ed"nTHkF48!=^Uj/85!MaIZ#aIp=@\/OLK<?,Fk#`_2CgbBS"C]k?oN^SP9jkrmal>jg*`U8E*C_L^1AVJ#d'mrodm@ICYPtCoE%9U>@%h<@!Y@=2:#i6g#_jNF7in\+jV&Qpq+4Th[^0l=_s`jn?4i.u^T&'J=PZg?T$pO65CD*V\/!rnlrI_>\o0tHSZ;D'?M>dfjKV'Yq+4o=`.HKNYP\8;+1!!p?FT<_LJsb>D<(&tq\Za!fPX7*K!5uOB,(7W5>9bW&:[@p_!t+X*,!n4"uba829'%5M!jV1D?/qm?%&O#\A>[!N*>-kQghXOcjK3#`a=)aRmCud0H&)Q-A0-a#>J*DL@"3K252U1^6[m%*;Go]g+I2tCG0CmDGtVdGoufsi[+srTH!?VJeu5/*P=$2*H!bl#]ZE^C]B9G,t22R`fU5bY5t>L!mH'o,W\ZJ-*G%)UA-S;@0dguU?Q\^_j(!CLn1^YWQj2EpGNE6V,j/;2A[icH%o'l#F@=sEe9+!'bR>Y-E"XNCCGKf*RVn-AB=N\g)Ji#`2-cSrc;Ehi.tpA?_u=iQfSmA[*&f0\iXc1m^A*t'`S*lE,2nPUVKkA2*obgoUcg0m4<IB,?rh4o*GkWCZYr5V0jq,/`L19WpZ:sRP%-d3$Jd1I]_VAA/ODe.NTo]#hN/p;R]JiFcMt>Xr6iKcg>m(/B*ql_$a;?8%sID@ds0Y(jb3c&,R?l@'F?8!Ud[\;u9o5^:GXF&$?,Hs2sMjPZmY^,lnGXJ*iTag7.Q`M]Y6jVd%A,e`tnhP5i[)H"[_,Fh6K"N)_l[)%Bin0T%&j".lKaaeA:2bZ`C$<]l<5Cc'BHD]f@#2;$(:>?<p>S/jc_K/JIG9PX'_e@.&u^W=IAF#G:==39cUEQF0('S!BkL)crY<ZTs`NY<WFX7c/^G)/'G=VU@1&pV@K&0J%U2fhMU5*86RM57<F<m<a#Y?GYl-$BPYn>n$``Q9KD?'&l!:EiYt]@ue,?.(e0KZ41B@P,bZCKO5-'u;lJW.)8":F)+5</Q,Q7MLKU=BRPneB<:L,\ZXE]]%kZ59eYKCjt4J@JYEj5C#?p,e!#<Pf!j1^WnLMg&,H(s'*4c<C$<99\?"bm"bC[H(%B5=EtNg6L&eZf+saWVSaF'I!nq/H*U[/j*@?lmUjnjf2ck[Rm@u,#kNGinQ%MVY9_3n<M(Rq+LFdJ'i<e?+-CD(@PB,V%PtP&LsfKj40JL%&Rk_@9/QMF./X5*H5IQ%DX5dk_7;7*M5qmP:<jIV6dV>n_g0!US`N1DUEkJ,rZ.-ei`'nsBp8PpmHn<0+8/0.NEH*8I+TCu?f"h_nZ&HgeAHP<EcOD5od73\4<Ct*&(<m-q2MP8`<W`;6hsc_&Hi^7^/[8hj2'OkiRh24J:q]sG:Tc%J-@i6;%D3XKi,I#<mgk^la80"">gX=9BAb#P<^g$8MYB9IrF(\0ae-_BcjK#MP_5BeA3=tec\^13jDYN#+D9m_Zqgn"F3m2o`~>endstream
endobj
57 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 512
>>
stream
GatVZ@8#W7(^KR)q\7@,fc=C8C>6[nnQ*t%ENY9'5mX`T>IA<_dd>\r^K@u)W@nH#cp42`kq@Ml#1!b6J+FT:P/Grf#4)GA!Z[j5GQKTA]oMT"B+UenL0ak5;A>^0"R_d1H[XQu8$9aYX6\c?&:!%*UjYo)D'=K#""".'n#"2p9]Ff;G)tni*djTq7-?5G^uN#-MGpaM>aEVIAt5DYU"W+WiMk7TIfOs&rq"AmGN1b,Tu]=@pH)s>[7K?4=j#cH&EZI9's*rp('cfCEbnL#;Kg#sR+PN7]9P-P/<+IIS'm2&gurX;oue*XkBk_,'[W0W0S\Z@XF/?T]ZB\M>`1Sd@Ze4b>,,;=AS$R$f<mHi6)pD]KrS]_J5N`B'Z`C<efI'cU(;6m/)8#Hl2F"2.U.`)*D<:-?HehYi[gSED?^t__!>0)A[_inXCRNq=u@>EHeS8UI:?QVL]eQY#X6T'0b0EU'^_LA#C:Vi&>k,4+/0<Vg>?@5i=+q"s+/Q^!<hNM~>endstream
endobj
xref
0 58
0000000000 65535 f
0000000061 00000 n
0000000146 00000 n
0000000253 00000 n
0000000365 00000 n
0000000512 00000 n
0000000660 00000 n
0000000808 00000 n
0000000955 00000 n
0000001102 00000 n
0000001249 00000 n
0000001397 00000 n
0000001545 00000 n
0000001693 00000 n
0000001841 00000 n
0000001989 00000 n
0000002137 00000 n
0000002285 00000 n
0000002433 00000 n
0000002582 00000 n
0000002698 00000 n
0000002776 00000 n
0000002882 00000 n
0000002966 00000 n
0000003172 00000 n
0000003492 00000 n
0000003698 00000 n
0000003904 00000 n
0000004110 00000 n
0000004316 00000 n
0000004522 00000 n
0000004728 00000 n
0000004815 00000 n
0000005130 00000 n
0000005205 00000 n
0000005342 00000 n
0000005443 00000 n
0000005548 00000 n
0000005676 00000 n
0000005793 00000 n
0000005907 00000 n
0000006062 00000 n
0000006187 00000 n
0000006304 00000 n
0000006431 00000 n
0000006547 00000 n
0000006664 00000 n
0000006779 00000 n
0000006895 00000 n
0000006989 00000 n
0000007099 00000 n
0000008039 00000 n
0000008944 00000 n
0000009174 00000 n
0000009402 00000 n
0000013558 00000 n
0000016864 00000 n
0000020120 00000 n
trailer
<<
/ID
[<fc9a6eea06a9e2d4a6457f8f52705b23><fc9a6eea06a9e2d4a6457f8f52705b23>]
% ReportLab generated PDF document -- digest (opensource)
/Info 32 0 R
/Root 31 0 R
/Size 58
>>
startxref
20723
%%EOF

143
TICKETS_ROADMAP.md Normal file
View File

@@ -0,0 +1,143 @@
# Tickets, Gitea, and alpha smoke delegation
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-26 |
| Last modification date | 2026-05-26 |
| Co-authored | |
| Severity | medium |
| State | in progress |
| Document type | technical |
<!-- doc-meta:end -->
\newpage
\newpage
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Gitea (/app/androidcast_project/git/)](#gitea-appandroidcastprojectgit)
- [Alpha smoke tickets (typical DB id → block)](#alpha-smoke-tickets-typical-db-id-block)
- [Console features (implementation)](#console-features-implementation)
- [Attachments (deploy)](#attachments-deploy)
- [Google Workspace — phase 2 (not implemented)](#google-workspace-phase-2-not-implemented)
- [Agent / engineering queue (from your list)](#agent-engineering-queue-from-your-list)
- [Open tasks dependency graph](#open-tasks-dependency-graph)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Gitea (`/app/androidcast_project/git/`)
Public URL should return Gitea HTML (assets under `/app/androidcast_project/git/assets/…`).
**If clone/push fails while the page loads:**
1. **Clone URL** must include the subpath, e.g.
`https://apps.f0xx.org/app/androidcast_project/git/<owner>/<repo>.git`
2. **Gitea `app.ini`** on BE (`:3000`):
`ROOT_URL = https://apps.f0xx.org/app/androidcast_project/git/`
`DOMAIN = apps.f0xx.org`
3. **FE** `nginx.conf` ~983996: `proxy_pass http://artc0.intra.raptor.org:3000/;` only works if Gitea is configured for that subpath (your HTML manifest suggests it is).
4. **BE**: Gitea must listen on `10.7.16.128:3000` (or `0.0.0.0:3000`); no extra nginx required unless you proxy git through `:80` too.
Symptom checklist: 502 → Gitea down; broken CSS → `ROOT_URL` wrong; 401 on push → credentials / token; 404 on clone → remote path missing subpath.
---
## Alpha smoke tickets (typical DB id → block)
Ingest order from `tmp/alpha_smoke_round1.txt` (skips pure “pass” rows):
| id (typical) | Block | Theme | Delegation |
|--------------|-------|--------|------------|
| 1 | A3 | PIN/cast OK; **rotation + preview NOK** | App: sender preview / rotation crash |
| 2 | A4 | Video OK; **frame drops, low res, few TX losses** | App: UDP/encoder quality; monitor NDK only if native crashes appear |
| 3 | A5 | Audio works; **laggy sound** | App: AAC pipeline / jitter buffer |
| 4 | A6 | Notification resolution | Often lower priority (pass with notes) |
| 5 | A7 | Stop OK; **wrong lifecycle** (resume, notifications) | App: `CastForegroundService`, notification intents, task stack |
| 6 | A9 | Repeat soak; **lifecycle** again | Same as #5 |
| 7 | (manual) | **Delegation / process** | Console: assignees, workflow, comments (this doc) |
Confirm ids in UI (title contains `Alpha smoke A3` etc.).
---
## Console features (implementation)
| Feature | Status |
|---------|--------|
| Workflow state (`triage``in_progress``blocked` / `wont_fix``done`) | Migration `004` + UI |
| Assignees (0..n users, chip picker) | `assignees_json` + API |
| Comments (thread on ticket) | `ticket_comments` + API |
| Email notify reporter | Nice-to-have later |
| **Attachments** (files + links) | Migration `005` + ticket UI |
| **Google Suite** (OAuth, embed, Chat/Meet hooks) | Phase 2 — see below |
### Attachments (deploy)
```bash
mysql -u root -p androidcast_crashes < examples/crash_reporter/backend/sql/migrations/005_ticket_attachments.sql
chown -R www-data:www-data examples/crash_reporter/backend/storage
```
- **Links:** paste HTTPS URL; Google hosts auto-tagged (Docs, Sheets, Slides, Drive, Meet, Chat, Forms).
- **Files:** stored under `backend/storage/ticket_attachments/{ticket_id}/`, download via `/api/ticket_attachment.php?id=…`.
### Google Workspace — phase 2 (not implemented)
Requires Google Cloud project, OAuth consent, service account or user OAuth:
| Product | Integration idea |
|---------|------------------|
| Docs / Sheets / Slides | Picker API → attach link + optional embed iframe |
| Drive | File picker + shared-drive ACL |
| Meet | Attach recording link after call |
| Chat | Webhook or Chat API for ticket updates |
| Gmail | Optional notify assignees |
Until then, **paste share links** — the console labels them and opens in a new tab.
**Deploy migration (MariaDB, as root):**
```bash
mysql -u root -p androidcast_crashes < examples/crash_reporter/backend/sql/migrations/004_ticket_workflow.sql
```
---
## Agent / engineering queue (from your list)
1. **#1 (A3 NOK)** — Reproduce rotation + preview crash; separate from PIN path.
2. **#5 / #7 / #6 (lifecycle)** — Audit cold start, recents, notification tap, stop/receive; align with alpha A7/A9.
3. **#3 (laggy audio)** — Measure end-to-end latency sender → receiver; tune buffer / sync.
4. **#2 (TX losses)** — Log UDP gaps; recovery already OK — reduce loss if easy win.
5. **NDK** — Only if crash console shows native reports for these sessions.
Use ticket **comments** for findings; set **workflow** + **assignees** when you hand off.
---
## Open tasks dependency graph
**Master plan:** [20260608_ALPHA_PRIORITIES.md](20260608_ALPHA_PRIORITIES.md)
**Diagram:** [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md)
**Dev DNS help:** [tmp/20260608_help_request.md](../tmp/20260608_help_request.md)

278
TICKETS_ROADMAP.pdf Normal file
View File

@@ -0,0 +1,278 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 12 0 R /F5 13 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 762.0394 172.853 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 751.0394 199.845 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 740.0394 167.949 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 59.68504 729.6019 130.545 739.6019 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 59.68504 719.6019 224.3325 729.6019 ] /Subtype /Link /Type /Annot
>>
endobj
10 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 709.0394 192.861 720.0394 ] /Subtype /Link /Type /Annot
>>
endobj
11 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 14 0 R /Fit ] /Rect [ 45.68504 698.0394 154.637 709.0394 ] /Subtype /Link /Type /Annot
>>
endobj
12 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 30 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Annots [ 4 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 30 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
16 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 30 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 30 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 30 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 30 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
20 0 obj
<<
/Outlines 22 0 R /PageMode /UseNone /Pages 30 0 R /Type /Catalog
>>
endobj
21 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115606+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115606+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False
>>
endobj
22 0 obj
<<
/Count 8 /First 23 0 R /Last 29 0 R /Type /Outlines
>>
endobj
23 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 24 0 R /Parent 22 0 R /Title (Gitea \(/app/androidcast_project/git/\))
>>
endobj
24 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 25 0 R /Parent 22 0 R /Prev 23 0 R /Title (\376\377\000A\000l\000p\000h\000a\000 \000s\000m\000o\000k\000e\000 \000t\000i\000c\000k\000e\000t\000s\000 \000\(\000t\000y\000p\000i\000c\000a\000l\000 \000D\000B\000 \000i\000d\000 !\222\000 \000b\000l\000o\000c\000k\000\))
>>
endobj
25 0 obj
<<
/Count 2 /Dest [ 14 0 R /Fit ] /First 26 0 R /Last 27 0 R /Next 28 0 R /Parent 22 0 R
/Prev 24 0 R /Title (Console features \(implementation\))
>>
endobj
26 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 27 0 R /Parent 25 0 R /Title (Attachments \(deploy\))
>>
endobj
27 0 obj
<<
/Dest [ 14 0 R /Fit ] /Parent 25 0 R /Prev 26 0 R /Title (Google Workspace \204 phase 2 \(not implemented\))
>>
endobj
28 0 obj
<<
/Dest [ 14 0 R /Fit ] /Next 29 0 R /Parent 22 0 R /Prev 25 0 R /Title (Agent / engineering queue \(from your list\))
>>
endobj
29 0 obj
<<
/Dest [ 14 0 R /Fit ] /Parent 22 0 R /Prev 28 0 R /Title (Open tasks dependency graph)
>>
endobj
30 0 obj
<<
/Count 6 /Kids [ 14 0 R 15 0 R 16 0 R 17 0 R 18 0 R 19 0 R ] /Type /Pages
>>
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 857
>>
stream
Gat=*gN&c;&:O:Sm%^&,'s5gd*o5VKD)uNMVQV#[a>SkU(N;G2q[+=eV)AO=`n$g53IA,:8W,i._r?.X!do1MjoGK#\4](^34Mb'muIC+n7*d]7,!65:eD=73S`POR8BAU_?+1,Ot%E,'$T;?a_G?Gn_clf)]BM$:uj5Gb8iQ#&X"SWm&gk/2d!`X/A6eY+Of1)G``NnB"RaZ%HCM!^>A(=QN_C$$/?>9Y@,dh0S5ok4!n,T&X@#qd?+_%^+J%_@P8[Ar?a3T+*`D-6P>14I_#E#n'6F/ISB4E:OePS04f:i&;i/G'ftUnUhsRU66d@0fK4^)jY0=EAqr'N&gdgRBNSRYcFfE`(d0d4'SII[I>O]]R@R!,eTkJH#qnNhCL%X1DVjC*0X-Jr@/li:1MH0bOqpFP"CpPm.+6;lk>n67V_\V_&l);1_YhU3lu\/rNm@7/@k#pqi=P;bVn6ac-U$%$!HjXG&PSNtjJD<kGP^BOdjIm"m[8M3db69o>5/9VfHF6(%`<.XM[L,E05GLn&\Z(&0ZrI0M@#Ie]&hj(4F(WF@I+6B?nl0m_`dZr!$<9WGiBIkXa?R<U"]6(#A`g$k5%e"62#cahf/![qnXoOL_(&VcogX%UVhq@#e!=;8p$MF#Tat;]6LRgFd,2c^?qV@0Ror9lc<W6jp:]_o6V>eRjfs`3R[2*O/?B;^P=a4XPTcNEs)$N7-b'2(oF]t[4*JZnsZ7i@MN$P(t;9e69,L:;diW-U+1h4+Cp2a%0WjG<Wcd\F>[Wp#ioa8><2-@;6KTf<0\e-&rAN4Vj8`kBSt#Whg.1$,!Xr/=u$@V&[]g%b6/E5G#;+s&T47e+i3@af%YR2~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 692
>>
stream
Gatn#?Z2Df'ZJu$.IL]W#D:8C_qrh=WMXNI]-?Vg;WWKtgji+9B2u\E/kHs5D&udo+^p%@cL9P00ng/l]"OF^^h!P_MumQe/:b_cL>iGC(Sd</h62bNP%I9&"6Z2-;'+a]S0h[aU*KdQj_;dl+?X(X9Yo:)r$FSJck)'M2:u5fZ!LjDLo3^o=[6].QeK%Z.%[.-dVIb#F\4k-5G91dj=+VDLNKSGUV%9p*s\P+!hh,[<)j:g+$480Aa+hNq-\Y+J18di`AD;?V#],9Pm";5Q1POmrMEMiBOH,mWe)g!0T1rLc<6=u-]YqI:Q6f)K5<s/\SPLgm0lIrOM\3'loh;UCLi>H$a5L1`5DSHLW7,#nqP*[a2PLaqV$6Kb%6X\o2N#2b[qCk4YOj80f@PE\$7I8M,M3\0b0S_PJE8R)\7^=a_DArpmJnfF9gBq$3<\'.kRSt\qZQZ>U/BU@8.+KZaS4/kD+SaH$V*IgJ2MQ_Q=ecG#DC>QQ)m*ka"Rq65&gMp)KtHn=4%d"ULQM1f#R5+$Y:iAW.5f'(0*LliS$&P[C-ZG4kgFG^0./4.6)<gl]&!V=!c"_;>7YW6rpA[K7idGLIH-;=[^j#C\*a\96C@MsZ%S?o=MU6C1>T*VnW\n<d+F_Y\=CI&LUOdf%kuO,kRGKP$SV91VB`[e2IhfECH$%ZLAg%B+[L~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhNS@cJ`sQ<)AT7bBX2'SI1sre63!8eu#Hi*12+:VXOqE!h>Xcq$8qnjH_CLV:rJP%rsj5>UUDD.`Liah"L%cVntog).//WF]OI2_e/&`/?Tki+CUX9Pb^j~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`]*cDO&;9"kTAhN!`Q!btAh.P+,O??T$:5)Js2%0!'c(Chi*12':B,^cE"9mgcpTunnjM9CKRM'[P%nF[hj8K8<cdi[ag.prcc><c[4u7=;Tm#&O;KE.i_Ac@E&[+g9Rn-*~>endstream
endobj
35 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 4040
>>
stream
Gb!#_>Ar7U(4OT5+L"t`ZfB+>F`fIG$TDc\h.`?PPO@d)J:NbqRuJu!]$q$%]>&b)WJb=j=1>M<'JkbClL<TTq++[V.f@cVs4n,NYO9d(8jaLdG>"_LMLk3PU"=K9?V[tUVI:s?A#]a=kE^^"_hRn\IVYOJ/%Vit*>;Dke4l]dJn$=>W]D.6%)oKQN[5[HW4HTL:GC=>_NtoI?Rn".C$9Ge0GZ.*?"*QAPbT^c<bV&Y-'Y)"aUdV'1JBKb/]K.o&r&1TGS+S[9B,"UN;.&N-F4F[B)156f]<#:q9@`&KT^h#@8SsL9oU]5`=tFLWq^rh:$:k!H':GgbXJ-f^-hFZVnRp1R\Wf21@BY&HiFRD+E:)=(o;dR,;_l]m)@X@<mCQMS-+jH)'BhU/(uYdDNn!H9i<TsS\/5PfB31*Kk\1h9>^TDD:d"0-s9I_OqgDL-XM0YJ.HVlUK+BEW&/9:qh8ENke*c+Zq99\Z,$$20B4f_+%)3mVhch'+o5)r2K..3I-1.Wn[VDX0Kts4HcF3+I=>4A[-ID:a\URphZbp[rqpR*Ys0;WOREW/5b_U2$aR^uWlG@EceQ+o_B>Vj!`lc7&(%B/LB2e"X/^L(blbW"EqE[VedHEA="Cu[j7W*$K*]qud?01Kk0:m\236hco(r=Em0DQcO.u`6RTc1t28.&;qLN@a?IoWg1L..n_6`.l?s0,sT=L`S@r0EQelhr'd,X`@6O9)R.UM=%8mE)N-AdNkOYGaV&6HH^m/54er"la%d8Q"V1=)sO7Y2#V8gEVp-''2"8Q#[QGh`[&UI'ABR=dFD?qVDr8ZNSR!]A%5nXqFJ)-U6FkRSis5gQY!H9TNkP1e#kEC'a>4#K1<)RcACMM@\!#Qh"$6#D2,/,hCrgc5^$U<d3N,V9ChlV#BR7ao/N?nTeMP1Du3"/+Y0VlOrgM)4=FPD&NN>/fm"6(dc*^21T%2TT0&pIT&b%%/Rt=OV&>llMYI5*+f%Y'CP@ml>]Qp_0V*iqE'_8i=9rq51<bD8_(W3AR7G!uoG3HGj8FR#)"R,>aS:TQ&gncnkl"=gX55)UT!i6tK0FAajM<*4'$Q`7>L3C>bt1CYmbXRZ:NG(tfNSr,>A,]E*:T_V?Nd<8C?5Q]Sbr8!gCjo6,^u05&5'Eo[*1o]UAjBh5"4g8\U>1Z"`/h?G1k+<H@TE#F5uef6$i5lD;c4\5Ai+PHlm;j->)0[eu/(<58p&Ohe)Fcfqfdm+N&`a,FjEjCc_YZ?lS;&ua-3`>U\V:lDWC_P,(:"6@3]OoAR#BL&afKuJIS$;2K00iJ6/*?%rAI`B3S"iN+#-*.gDeV\i>C2Ut\7tIS_Y1X/Y%\uoY\s/L!oqS&,lkl/rPo0,&%kqe3kP'Z=-jW`dY#C#b96$<@grLg!V7D>`%)EEXd$)N'hbrp@fC7>2mejUDV0i?cAc<^?W*N!8A]UJ,@;`?_Q(Kj\/RJ?X(p0b[/p+G:N`?A:qgPLVUTmRQ"l]Jk3[qlk]YAHYNNM],tgc@:Sd^2O3e'WYDdABWpo!Df)[3Kph:L"A#q:q(nRFg8t;D)$MCOZUg2f:8A&j'!:m?X<l904h8WB!)#\WI)l([O&*?aGA@#i82A?1J$0*J-o.?c1GZq+a$/3&h-[[#V2"!;1_:VleICd(ZdD4k\LgO'>ZfL3fD0Wjhe).^_jf_g*%paS`f)84'Zmq%=$DJg20X%72[6o<9Ph$jNFCFk^\"J%<XNe23e71U.49t$Aa_LL#@aQ(cS*pW6K_6+=$P]n.q@'sV)>_R?"RPoN/sirg-J":V]XM'kBTtCm:NU%<gLNFs%m48n.'GBH6:c)\D5\ro2DF<h6g/hFn/oTO9S&-m&3`qJ-%OjX8o#J5ZQ=>?HQ<up$+NRRG)n7m17m^Hm^J$jO%t;9#nNMGTL;n4UR0JYEID#6cZ3!"mX>3q9K.&##^C"7:0eV>4b?HI<OHH=!c%-<O6B2Zq6r3Wb1mo5PA/Rq4834cpdYDM4X'>/MA+FcR-=m#k3s^5[0KOm!oo+ik5?o_S*FBoWr,D]:aD,5ajX[uCps?HOVM:,,'U\Y\"`MX)#^EsLa4Ah8[^d,G)aUWUA'i\$.;SY;!BKkn%>?EKFrEB4J:NGUJp7X">0B4)/)0FX+3O?,7EZIZKEid>e\mIL_1bDc!oaR[o)M,_ge:S'F3BIWM01Oa#>ArmC+AchX"XAGkBCc*)`g>lJC--AR]4C$R()"X$cAY%KUCK%`)X0"DG>$!`-dc]uRCk>6f+dAH8<Z!Pl:3!I-'unD5(n=9I3s]MXJa53t\SGb4_p(bZtSj18Z0INZYRLLU1j[S7W,Rk"]7[i\HcWh[f0$);]]0#TYhZpDoo+n;:&DhSk0`2@&H3e9[3f/%brM^h56Q)u!o\B:I-B1''@OMCa%<O[s._T6#i4D`@WP%P,e&7f7>d&/'[ACPd5Kjk8ngM1<q^)\J!mG0^@NnKJuqY+LDA+kuUrZ*Y.*C1M(Fj]U4*-+*paei$bQ.^B^:`3"((7Z\QkOn-+h0U['peL,K4L^6O-_Z;CMb1k.lCd(%)oC/%=5=(aHs_qH(5R%I<E'[:mmL2o3E*+VE`SgAKd?8oAC'/;2OJ)Hcf7;s4[V71]4kcSZSQsS<`.EJe5)l>UFscOOB+G(-?FR@=`ac(+f_7>.#hpDXb")r+[B8YHo+6+Z`bH6\,fF7+V14**$+Ih\[^l:e/7#\>uWnTL_<K's((::6Nf7Bp*L7Pb\03T*S"6unS$kZH:+'lTsPU]DEpT]a"G:?8rQ:8JA4hmI_$k5.Ip'^.aYOZ*X,UZLCZ(H!]Y&QW3lHDP54%:56>@)T"72NXs<C>InVtnLK;XCg`;]*X:!g'^4N6B2J'urMS>]f02d'iBCe[-.CrWFAG2Sr%r9IGl(etU1g0`g9k"GSpDDX5VA)0k-nX>2$e5,u?<%6iJoY<./uT>%pA/oEf<8D*rOTE&f?M!Xh:uE?I1I"T4%t2!TEhVB%t,O`FLUs(f<)/:[K+'OX8Vl<2Y;#Vc./>sFa"12Ru8q'64Wq:US6"@L7IgDCm9kWq'Mh/*c4H2;"'-,UrC+2]:1ra`eWVjZ?_;ApujP'&a'H]%8leM;6esU';Ef@o\]14Ie?Ou\_/3Y3"3G;&=IBm#``O/ZO_=.-9E@cR'jiECVC8)ksr,:DGNEYOL*r-rl#Kk'jXe4W(&WFH87R$(V#9qRLQb7]"rjRFg'JArSl$_OWVp7O=CB=;Y8/gh9E(3&pTH-7'l%8Q4HP1W@)5aU>h:,P.T'Ggo_g,LejXg@a![b@UJ@6M0tM`Sgh5PUCXsVKN5/(_tOc78sFr10nickeXV@EUc$WG%\7,'YP^KErHhQi4dFW+`)TS)Po\ZIRurdQ&lc_jpc%@6gi+ujJBgFDAt9BfleK,S$cF3.BpQQP,+FM8;H1M9l4"&Fbq^cOg/:7p&?fH1q3^!,M@722n9O;mr:]%_ahfBJ$Y!LV$ACreociD,4:T5W&D<Ude+t_830mrO[0FHbpneS@2?W.a'\a%qpZOL;kT7=[OLfV]jP'ZV!BQ"2gma+HWS;mLG'BrCRe0hRc_<"D9LOnqdc2L68;$t%ZV2eHOK!1Y-5t9D8\?@"_)TB'jVlec6V#9dTdA]2%3f9PSGTp-.9Qe.A#A4<768al-O:?g$)I)i:TT9d([o'g=qVS:NrV[Q.g<cOo#P>3E"/m8QJu<)-,/7[7*fc^RQrG=i8G5\38NKVUAtrOj,,.Y8'7mM);%MJk`35l7>s_RDZk=`"-C]K&I^ZP0"2k8$+rO2W1FP[_k9X_Y8OlMaSarSPFT*_GiIqE4kK=dT24\Y/gcN@=ni[VbSS#k$)[A"0^EC`VTAH6%PE9hN^s#[`Q`VtI0/=a(11n]`.2r$"*n9?:dDK<+ch7.AFJS"rF;NoM6Ba4;P4VK:Ei7\?VSM-W5(WMIcFiT4!o]V0C=7J^5N,A'k`M+kgbES9Mcb\/_keM"`EQao+DK_[\7EDL$G;BL-SoBhT@p3MG?k$<\t%$*1MhbBo^]h`.UIlJ%C\9rX5`_?"F~>endstream
endobj
36 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 980
>>
stream
Gat=*?#SFh&:Mm.(o]1L2Ds=pgZ7M=($TS"'Y&uEocXt%CQC[uZg7$362^W*3Y^CX:aiV[KBDKi1HT)&nEFEL4^A&8:8%W@!TT:&^kU\WOup0Kc@2@iNTW"b_GT5%+c7Ji+']:)Nm&3Y[^d6-A-&44i;OPLrm+fL&%[9t>bJIN#F%Y'&ToB-ppn<h_G`,>C&r+Xe505d5+S<5ncP&i*/K+/g'8na?EbLW)&s:O]B<HmD=eSNo:W5D@()rh#=^-$!DKB"S*Jb4*^"E`_*'\[A]kcR.k>G4,^b87j%G(V8@:/5'c:b?()EV*1V9&?,AcTb_'\)qA::'Zn$36jk7<]eKuC$pE_P)!N<cE$bH/"2W]C5lKshej0r8KM$]rM3H'Q,7[A(2S&(U<C]m@LF&9UuD=B2f*T1paWNr&;-0oUnSo_o<2N!GB@.#Kkf@\XDerJ&0?2:<Ad5$@Q8*`,eMA@eYpaUIk2]S"h?h\^Z84:'hVgmVUqL;!`$PR.:@o:-Hd_5q3oN"Q59HLHVp,rd?9I"tlCKB@[oGH-26nI,cPQn(Zl&UonGegRg]9Q+:o1q!YGV1L0%&oULXQH,*HroreZ3i"+_gV#_HZ<fV'mJP?<Q:BC1VGo&JOq-je9e/+o>6DFa?qW=e7UtXARq$+Cb*.2D8@<9!5,6r1PH)U=c.=QqLG>uC7]XqHD:/_q.K,dgC:,#UES"&!`IbO7Vo5T'kr(,XJe#Q<o4(bh,EM`>aJlDq+cM#T_]QT-V@C,U@\+$>fXMf@_cUU"Pj\\^6f3gqfBor#n$_DK0UCk.WJ17s=!d;+ck$*&4MI;5NT[YFS*BlGEc_3;/[?U/"o]Z*hM3tk"L2<U+AHM#PBdd*gcis^\W:B1O$b\44C+U7^;,!a!%$2j#EE(HrN/];A$1sNk/Qi,jjCML;$ad0?(>cG4kd!(?F]h%p`Ipl&:&(+NIV7?^t>SSh$[r3L2Z@mfe;5I/=c~>endstream
endobj
xref
0 37
0000000000 65535 f
0000000061 00000 n
0000000134 00000 n
0000000241 00000 n
0000000353 00000 n
0000000500 00000 n
0000000577 00000 n
0000000724 00000 n
0000000871 00000 n
0000001018 00000 n
0000001166 00000 n
0000001314 00000 n
0000001462 00000 n
0000001578 00000 n
0000001684 00000 n
0000001890 00000 n
0000002152 00000 n
0000002358 00000 n
0000002564 00000 n
0000002770 00000 n
0000002976 00000 n
0000003063 00000 n
0000003380 00000 n
0000003454 00000 n
0000003575 00000 n
0000003895 00000 n
0000004064 00000 n
0000004168 00000 n
0000004299 00000 n
0000004438 00000 n
0000004547 00000 n
0000004643 00000 n
0000005591 00000 n
0000006374 00000 n
0000006602 00000 n
0000006830 00000 n
0000010962 00000 n
trailer
<<
/ID
[<2691acc5e8ef1ad05f5101edc8066bb6><2691acc5e8ef1ad05f5101edc8066bb6>]
% ReportLab generated PDF document -- digest (opensource)
/Info 21 0 R
/Root 20 0 R
/Size 37
>>
startxref
12033
%%EOF

104
USB_HDMI_CAST.md Normal file
View File

@@ -0,0 +1,104 @@
# USB-C / HDMI and USB-tether cast (roadmap D & E)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-05-22 |
| Last modification date | 2026-05-22 |
| Co-authored | |
| Severity | medium |
| State | postponed |
| Document type | draft |
<!-- doc-meta:end -->
\newpage
\newpage
---
Android Cast streams over **WiFi** (UDP/TCP/QUIC). USB-C→HDMI adapters use Androids **wired external display** stack, which is separate from the cast protocol.
---
---
---
---
---
---
---
---
---
## Table of contents
<!-- toc -->
- [Layer model](#layer-model)
- [Phase D — external display awareness & future capture](#phase-d-external-display-awareness-future-capture)
- [Phase E — USB-tether transport](#phase-e-usb-tether-transport)
- [Tablet + adapter checklist (e.g. Doogee Tab G6 Max → HY300)](#tablet-adapter-checklist-eg-doogee-tab-g6-max-hy300)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Layer model
| Layer | Mechanism | This app today |
|-------|-----------|----------------|
| Wired mirror | `DisplayManager` + HDMI/DP alt mode | Not encoded by us; OS drives the sink |
| WiFi cast | MediaProjection + `CastTransport` | Primary product path |
| USB-tether cast (E) | IP over USB (RNDIS/ADB reverse) + same framing | **Stub** (`usb_tether` transport) |
## Phase D — external display awareness & future capture
**Implemented (foundation):**
- `WiredDisplayMonitor` — tracks non-default displays (HDMI, presentation, etc.).
- `ExternalDisplayCapturePolicy` — developer preference for future capture routing.
- Developer settings: live wired-display status + capture policy selector.
- `ScreenCastService` registers extended display listener (rotation + hot-plug).
**Not implemented yet (post-alpha):**
- Capture **secondary display** via `MediaProjection` / `createVirtualDisplay` on non-default `Display`.
- Android 14+ per-app capture rules and extend-vs-mirror modes.
- Automatic switch to external display when passive/active USB-C adapter works.
**User workaround when HDMI dongle fails:** LAN cast to a receiver (phone/box) connected to the projector by HDMI or built-in Android on the projector.
## Phase E — USB-tether transport
**Implemented (foundation):**
- `UsbTetherCastTransport` stub implementing `CastTransport`.
- `CastConfig.TRANSPORT_USB_TETHER` — selectable only when **Developer settings → USB-tether transport** is on.
- Factory routes to stub; connect/listen throw until RNDIS/ADB tunnel + discovery are wired.
**Future work:**
- Detect USB tether / ADB TCP forward (`adb reverse`).
- Optional discovery on `127.0.0.1` or tether interface IP.
- Same `CastProtocol` framing as UDP/TCP.
## Tablet + adapter checklist (e.g. Doogee Tab G6 Max → HY300)
1. **Settings → Display** — does a second screen appear when the dongle is plugged in?
2. **Passive** adapter: requires USB-C **DisplayPort Alt Mode** on the tablet port.
3. **Active** adapter: needs a chip/firmware pair the tablet supports (often still DP alt mode internally).
4. If the OS never sees an external display, **no app feature** can fix the cable path; use WiFi cast or receiver-on-HDMI.
See [ROADMAP.md](ROADMAP.md) for alpha and commercial tracks.

228
USB_HDMI_CAST.pdf Normal file
View File

@@ -0,0 +1,228 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 7 0 R /F4 9 0 R /F5 10 0 R /F6 11 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 12 0 R /Fit ] /Rect [ 45.68504 762.0394 89.70104 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 12 0 R /Fit ] /Rect [ 45.68504 750.4394 244.437 760.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 12 0 R /Fit ] /Rect [ 45.68504 740.0394 161.733 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 12 0 R /Fit ] /Rect [ 45.68504 729.0394 271.237 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
9 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
10 0 obj
<<
/BaseFont /ZapfDingbats /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
11 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font
>>
endobj
12 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 8 0 R ] /Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
14 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Outlines 19 0 R /PageMode /UseNone /Pages 24 0 R /Type /Catalog
>>
endobj
18 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115607+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115607+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False
>>
endobj
19 0 obj
<<
/Count 4 /First 20 0 R /Last 23 0 R /Type /Outlines
>>
endobj
20 0 obj
<<
/Dest [ 12 0 R /Fit ] /Next 21 0 R /Parent 19 0 R /Title (Layer model)
>>
endobj
21 0 obj
<<
/Dest [ 12 0 R /Fit ] /Next 22 0 R /Parent 19 0 R /Prev 20 0 R /Title (Phase D \204 external display awareness & future capture)
>>
endobj
22 0 obj
<<
/Dest [ 12 0 R /Fit ] /Next 23 0 R /Parent 19 0 R /Prev 21 0 R /Title (Phase E \204 USB-tether transport)
>>
endobj
23 0 obj
<<
/Dest [ 12 0 R /Fit ] /Parent 19 0 R /Prev 22 0 R /Title (\376\377\000T\000a\000b\000l\000e\000t\000 \000+\000 \000a\000d\000a\000p\000t\000e\000r\000 \000c\000h\000e\000c\000k\000l\000i\000s\000t\000 \000\(\000e\000.\000g\000.\000 \000D\000o\000o\000g\000e\000e\000 \000T\000a\000b\000 \000G\0006\000 \000M\000a\000x\000 !\222\000 \000H\000Y\0003\0000\0000\000\))
>>
endobj
24 0 obj
<<
/Count 5 /Kids [ 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R ] /Type /Pages
>>
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 861
>>
stream
Gat=*bAO(/']%q&mV7teMW="M/Gkrm3f>o?X=@3u3%\-]$m?Yan&HbLGYCFH6A^E`F+!EG280@G5$7g1'7e7+VRHiP+BSUN+s=CJRpbl$KV(&S,g)<MJC^/cmL+rZ_\S%i/e()Jas6+aXmCBG!Fq.(HHQ;bj!N8c>]LtB=&aN3Zt-m9aIco=CN__KJgl)ZrqRSO1]l9+R,Z*G0Fq4g4HB"aN08,dfC/r]*m`+SG_P&q-I"u]n9b'!'<dM=STl>-q?iqPk0M;^30>gKkE%;*U9<Md`j^5Z30d97o$c../0INk("K!EC;N)=Wi/^t;,D.Y;N1"_Xic9^ND^#AdPC]Q+]C)"khNu5giBu=4,;43F%AkNT\XK_oJj1,$F)e?X%qUga*IJP7We6fLPLF^V?ZqtjOd5Afh,Fk[F:%NP)F1?bMQi2Cu,Y7&kXF$U,n#(GPJU)/`o+JAU-coeEa@^eebu$(GAqJ,F0"AdiqHD@M,KrlGK\6T02W?pse5N0^nN]cI1ai:Z05&WHtcX=1JVF.i"e_BQ*8Z]u(XoPc4tS'?3(TX2-^L\W;'"7I_Rn%gcY=9"b9;_i;th@@>rKa'"\=rOj#_#=gO*mj=*RO5oO8k>"\bYEKHOE3M^9U=YD8-XLDc@[X&5``mYBP$VemF^ic#MN;<ZeV![m4BrSIG^nB'l\KdZ$YdeG<r;C:<\!T_GM=LqD<,$\?rdcbq>bASe(VaoPQO=V>,Vt:MTh=goc[M4r!(6G0Y.;4'gabhW^;LG6<"6I5k(5FVQ;?NL#20\_sLd=Z/-S&cP.k'd)56r-!DGFij.CK/khn9cPs3V#a/t)RX2apObd=H8WM2a\[%>'0_u"VIX+`~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 581
>>
stream
Gatn#>AMtY&;B$?/'_(5>.WQnBm&ZE;o_TUJJ>-q"[gqbCKn1t\\%/Sq=e#t0QHH2$"ZC1RaHNGbR>\7J#IeX)rNKhBL3hmC]OlGM!P=Ig#12,qTMdA.$6V[*64!CC$<>'@E&rma8db^,9A4?YnUnC0[#$N8UCkcA/ii+4[Y$[0[/%M;PRP@Y:HM$B.oAF7eXQ)J+r+?5N6V>#9BCtZjs(3hl%o>Yc6fs4G+TM0=]'A]:+)b^P-*@[o-7:N0t53oV9m4;C1IZh3R=4l9Q:nBl!5KMk/T?NEg=iq[ATQo"UXBM7i+Arg7#@FP<jE]:;``h:BT:Tgl\+<DKQ.(WG>8eD0f8bK*aG?^HIhQjkoW15tOFWAT&I^.,V4O(\]W-G`!AR?18f3$dGYB>F?0iZ)1@Js]JHlZ<2JQLkL?\)`cCK\,s\D^IR/^-J`W2_VhLBtZ=5rmVA8rd&thmJb;lBuW(&DDYDWY,Ek87CBRKW6H5LKtHjXIgc4JNJ1Rc#J8_K6h@R3h,KZCC1GqZ?P\DbEJ2qfVg%%,PXYOHie7aX`/G<n>ToL\8m4_PJJ$rD#D6YXh@Pk~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhM``PkDdF-fp`Q&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:emFgZ@onCPq82Ws8G-_Tt6^R9oWtig34W3;D"0MP=sG_Q,?.RGJoD!mbh^0(JJ'S*<~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`SgmmFhnNfa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B](9((,pZH62[FH7Fl==&J^W3%Z>&B5W+giG*2MH_7OtUjbJt$1U9Q2!n~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2685
>>
stream
Gau0E>Ar9+&q801R&:%!\!D0OZk!CUS8PZH)E2ibiW'/i7\Xb-P,OOAdertuUa0;Xb>t&Q"M:hT+m2&\%:t'MItmfLQiapm_<?LPL'b0i(q+%olk<XKc@29$25T\RY4(R4G_)o`]'\IjZS)Oi4D.CXis\^Af$"O1AgnS.k/d-?7pe%$eK8/8i$+(=M^,_WU&\&08l>]VO`EF:ff.\p<]^Nc`:XM9HI86CaE@mAmlrWJC7YNp&+7K%dHkm/qJq(63Bu-RIenM<=d*!jrH$G-G>D<oI2q-L",TQT:bJCT'P]MfVJ1prjA.M.&^[.@S<3rK=QBd6D@7g!&q5<4:THH[5qG:JS8_A&75t)G"q/^S-rq$KBr[]0JqYc'RW\8YQS/$"RcWd'\,Y5KF"G?q6JuUMY5:]p/BrCp@k8$_[g]1(_!s9,E.]A<Nb5i!H%`u_*7Ju@$1'ou"7ld+gXU\l\%2l";&Y%0TGoZ5;*5TUVL%*Zo2"Eb@le059fbH"qN[%F'Qh5^ds]\c/C:K6eNG!2<&Cfg4S:8CD6B*VcqM?!@>O0f.4kJ6,NW9,@f(pO1j,;tmGE5Rjd\FL[G?r&6]4?t(_]#s3p'asZ,"a;%3$hb>1Ac@>osWV`)B_'[%H<eRdTFV0-#@,_\L:&O`hd`l%*I!LA)(2cW6FZ,gW/l;4[J$%bo%FEmSYUbNm!sfOk&BW]Y">3U:*2lVIU``s,OC]@GT(TH712m"T%]=o2b^_#XH;=XQ`"$':rZTmte+0A#_M.tD4TSfY_c1D]"pE'&c0a,a2uE[g#7HT,Fgm?_%"h2/SsB"R=5FUqC#qR#W/.6;<aZu(:[UR4_7qaSIIAf4da(81BL5^(Wp-^i$nn!7?uq3TiE_W-mMBA&0:1;oJHMU`;+)obMgaR3;0oHCDahL'9"5OW%(@sk:;h-tIhW?;F$6md,Ma4Jef`V;S31jgh;r#Dgh8@mW88K)6bdoL=Mme!R5p?T(6+53,>+1-E"MMJC#A_NqKj7.bfqA]J3G,G)4B0-=)J+*@t6\+dX[$>`3h=2qFWERZXhs3A-oaT^m`Sk=2-Z(Hn;mV"&B57O?RK[L]>7:4*/JsLJ\Qs5Z5HHkJ\m'GH0!?A^jY'o$agF:V$1r*di>U"1i.X^0Z6BPka+.:>gI$Z[Sd;53'a"@<Mm=bA`-]&m4Uq$g*C87[W/a!(&2lZOQ`nk#Tde4nXJ>C'JsBJ,A7:mu^HKgZ9l\f*fZ7ff7qR:^iHahUV-6sL?W\'[N:4T.Bg@XOn@O40A]'<V8;Wn72PiYrA-7.1"c<Xd9W@UfBG#!8rHfsSlW`NL*D0VfhQHO_X;j<18K@0C"%p02$W+]\/5<(\Jmr8DLp7e8Ka)Ks(0HXo-mc.td2:PM[m0X9V8d;E">(m'nJ2aR\"Eqr@2$aBckN=lb1R&g.m7lNJh;oN5/Ld;s'?406V_>B>N:t=a#Kl3ZZE^:Kc98fC1\)Y1^YOL+[`[#"!#$Gh/uf#)!4E#lk`R.J&;%P1/oHgGUlFZWGIDO'?-%Yq24gf<6Kcn8aETtO1Bjs$u6%KJrU030d^a]Zp7<3a/D^nhSYKN5;=h6FV_ADTi\\'60j3AS,0Y?oNn"Hc!?*5rs>&V6TGd\1Up*iYECH:j)4W?L)"Z/aBA.td-seps3hUH"\USU6P*%5&[$(^fl'_`/".X6'*<6(7*oq@qW]9$\IsH@`kV81fFH]S_74u7A@=kq?IXo>QbBD^8mTY//6aH'-:s_`\*`DSFRcmCPGfA8C_(7XW)>Z`Z&F\afM=n^(s!Ra,j#qAaVc;+655uA,DKYOs&m?50oWHH?SV,5GRY;;S,-7([6#tf$Vt#-%:]c$(FQKfL:_l6.K$r2K5LST$@a]MB$(,No?Y^!>PYKPVV&d]:JWXD(:NK-Aj@UF\n'J<?>?gYMpKkomG7+Z])1g[CA.2_mN?nU=tP$8Cg^CDB5Xbdms"AXilHge3sN:VDG-d-TG2fjBDA[4@ipNN]4ZhUj>?p9%Y*(Up8D`fR&ulA6&>W(^h2j^8gI$5[:@#\j]mV?SCcu%O8/RN7WB]A5#f$ef!QRP@g1_)ZY)r>1l.TJC]BAZ)I[eLO['SXO_B7B,get8)n2?m$O'\IqqC,@c"2a2RSrMtQF0Z2^m9rk?8M9t!cO<_Z#&q65u`7qkp*ajdBNI?4s_X@PhVDu6GG*fIFS<c5lLAMO.>ns\!*42(Bm9Xe',]E%DU)>_)S+A;M-]691f3ki@c!baNmSsW",j<hlR\V/+E)DQED=?bLn:Q#$l<DBmUe&]@]hb;;sXpAi6J@$/V>,^`_-g+`D`WiDn9j+JQk%=9dm"Yf(]&BE.C.!i)MZp+_-/B7`\YgtHMmhL:B.rM.;'-Yc3(mM;'"-<T-`!n$[8F/un\^nnV2'hGVDoSH\`ULElDdb<1@6G_)F,r8.<l*,+AkG(Ji;gI\\Qo=*]U5Ldnd`[OO[ZLGS@&$8#lU&4`+'=;]k?aG"$p(8h5$HYY>VD=h[o:f7;713t2!eZC2f3nP9RVFni:tf,-#M<]`$6&fchct%KF@?'pRV/dH1JR_])gN-hQk3\n9To^Qpf2I/EFR=Ha['QYq4_e?AreT-<_=eM\7lbX19]jBhEXfUQqiPUO?i5cSc3XL%autl\sg>BQ!coAkO\qnhe8YHnZSc0aqEG3,l*m3]h`Im!lP9`;K~>endstream
endobj
xref
0 30
0000000000 65535 f
0000000061 00000 n
0000000144 00000 n
0000000251 00000 n
0000000363 00000 n
0000000511 00000 n
0000000658 00000 n
0000000805 00000 n
0000000882 00000 n
0000001029 00000 n
0000001144 00000 n
0000001228 00000 n
0000001334 00000 n
0000001540 00000 n
0000001782 00000 n
0000001988 00000 n
0000002194 00000 n
0000002400 00000 n
0000002487 00000 n
0000002812 00000 n
0000002886 00000 n
0000002979 00000 n
0000003130 00000 n
0000003258 00000 n
0000003645 00000 n
0000003734 00000 n
0000004686 00000 n
0000005358 00000 n
0000005588 00000 n
0000005816 00000 n
trailer
<<
/ID
[<b5d8df8d7d570acdede8840905e28dc4><b5d8df8d7d570acdede8840905e28dc4>]
% ReportLab generated PDF document -- digest (opensource)
/Info 18 0 R
/Root 17 0 R
/Size 30
>>
startxref
8593
%%EOF

76
VPN_DEMO_GAPS_20260613.md Normal file
View File

@@ -0,0 +1,76 @@
# VPN / Remote Access demo — gap report (2026-06-13)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-13 |
| Last modification date | 2026-06-13 |
| Co-authored | |
| Severity | medium |
| State | blocked by DEV |
| Document type | internal |
<!-- doc-meta:end -->
\newpage
\newpage
---
See also [REMOTE_ACCESS_VALIDATION.md](../REMOTE_ACCESS_VALIDATION.md), [REMOTE_ACCESS_IMPL.md](../REMOTE_ACCESS_IMPL.md).
---
---
---
## Table of contents
<!-- toc -->
- [Fixed today (prod BE)](#fixed-today-prod-be)
- [Fixed in repo (awaiting BE sync)](#fixed-in-repo-awaiting-be-sync)
- [Verified OK](#verified-ok)
- [Open (PO / device)](#open-po-device)
<!-- /toc -->
**Documentation index:** [README.md](README.md)
---
## Fixed today (prod BE)
- `config.php` **`remote_access` block** was missing — blocked real WireGuard connect credentials
- Installed via `examples/wireguard/install_be_remote_access.sh`
- **VPN traffic counters** in admin expanded device row (`WireGuardTrafficStats.php`)
- sudoers: `wg show wg0 dump` for nginx FPM user
## Fixed in repo (awaiting BE sync)
- **MariaDB `NULLS LAST`** — `listDevices()` ORDER BY broke dashboard on prod (HTTP 500); fixed to `(last_seen_at IS NULL), last_seen_at DESC`
- **Dashboard perf** — issue counts use indexed `reports.device_id` via `devices.external_id` instead of `payload_json LIKE`
- **Operational docs** — `REMOTE_ACCESS_IMPL.md` / `REMOTE_ACCESS_VALIDATION.md` now use prod **45340** and **172.200.2.0/16** (proposal doc still uses example 51820)
- **Orphan wg peer cleanup** — `WireGuardPeerProvisioner::pruneOrphanPeers()`, hourly via `purge_remote_access.php`; manual `scripts/sync_wg_peers.php --dry-run|--apply`
- **CLI defaults** — `ra_udp_debug.sh` / `ra_lib.sh` help and fallbacks match prod subnet
## Verified OK
- `wg0` on BE, endpoint `ra.apps.f0xx.org:45340`
- UDP 45340 open on public IP
- Heartbeat disable/enable API
- Mobile boot restores RA mode (`RemoteAccessCoordinator.onBootIfNeeded`)
- App wait-state + diagnostics (idle vs fail-reason); 30s heartbeat timeout
## Open (PO / device)
1. Full `ra_e2e_cli.sh` — needs admin login on laptop
2. UDP handshake + ping — needs phone or `ra_udp_debug.sh --apply`
3. SSH passthrough — RSSH track, not WG v1
4. Persistent traffic history — live stats only today
5. Whitelist + device E2E — human validation in progress
Journal: `/shared/journal/cluster0.journal.tsv` (tag `vpn-demo`).

216
VPN_DEMO_GAPS_20260613.pdf Normal file
View File

@@ -0,0 +1,216 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 8 0 R /F4 9 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 762.0394 123.493 773.0394 ] /Subtype /Link /Type /Annot
>>
endobj
5 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 751.0394 160.389 762.0394 ] /Subtype /Link /Type /Annot
>>
endobj
6 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 740.0394 86.58904 751.0394 ] /Subtype /Link /Type /Annot
>>
endobj
7 0 obj
<<
/Border [ 0 0 0 ] /Contents () /Dest [ 10 0 R /Fit ] /Rect [ 45.68504 729.0394 114.157 740.0394 ] /Subtype /Link /Type /Annot
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
9 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
10 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Annots [ 4 0 R 5 0 R 6 0 R 7 0 R ] /Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0
/Trans <<
>> /Type /Page
>>
endobj
12 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/Outlines 17 0 R /PageMode /UseNone /Pages 22 0 R /Type /Catalog
>>
endobj
16 0 obj
<<
/Author (Android Cast project) /CreationDate (D:20260623115607+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260623115607+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (VPN / Remote Access demo \204 gap report \(2026-06-13\)) /Trapped /False
>>
endobj
17 0 obj
<<
/Count 4 /First 18 0 R /Last 21 0 R /Type /Outlines
>>
endobj
18 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 19 0 R /Parent 17 0 R /Title (Fixed today \(prod BE\))
>>
endobj
19 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 20 0 R /Parent 17 0 R /Prev 18 0 R /Title (Fixed in repo \(awaiting BE sync\))
>>
endobj
20 0 obj
<<
/Dest [ 10 0 R /Fit ] /Next 21 0 R /Parent 17 0 R /Prev 19 0 R /Title (Verified OK)
>>
endobj
21 0 obj
<<
/Dest [ 10 0 R /Fit ] /Parent 17 0 R /Prev 20 0 R /Title (Open \(PO / device\))
>>
endobj
22 0 obj
<<
/Count 5 /Kids [ 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R ] /Type /Pages
>>
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 871
>>
stream
Gat=*gMWKG&:O:SbbGinS"770UYB%L.W(@djXF(qbW7%-OCFa&J):D7F/b/86M?;-4_N'acE`'fm=WCX49G<;_8S851@Q'0JdCj`iQ<,<)<'aN+c14b'qhr]Pe4flXI2n?EQNcN=&J>bd?;$A+akA6:>ED-i'l3#"-g>(`c;=4+XeU7f'W\qI_>OY([th,,Vq-Fccp2.JUN.u`\b@)muX"4,gG7]bU?C'W[>fZcHa4P"7I3V9ff?ShELf\:e5+3JY_KZK_F&YJ.oJQ!V9]8Q=rI%$fgP0M83kU^:a=AoD*!E2g`j3;2(N.6)h"SNmr_B\r]itTH'G6RkD\4cJFuY3/;KfR9d\Vk$_`HWI`%!jij?;%AWgOOFg:oHRr%d34!r=Ys-I6:>lr.H21$gkiOIW&d,&1&bc5bZBK[JQ*conf,6)78l90f[_tM=4BtH+OU-j0']qo8\Mc&%3]\qgN_l\'_]dSO:=n1&,jc2e$.Ns\`%YKlZ+o"[p#2)>DP'$K\0&pb\-eGp\F:0AYsYN4S;+5HnM;[1!^CI=m2isY*E4-<%c(l=)K,LR##Q"dMf:j*JDm)G"X&OI5+217FdjFb<U0;P?2u&eaio."1s@kur=.qM9G"A%NXXn)%*c(a4$&o,AXUblRfHbQE-_-/Tm:Sm/sH)l13!NOk.QQB_>I`3R5FW(H`4^4hqNrH9FGS3P1KDI@p9cE]=[a2l;l&7QS9p`Zu2OrBj+_C>B56Bmi(I<<fuLG6/=pZPhQ2K'h1,)\jE>"6<+<=5_oc`)[Kd!eWa&X;!@:Yjga,>)=.6p5#%^p$6:\D/^XVs.qk!GMpj!;oB*.)K&iAn@m@Y#&rGfdXlUC:q^WV>!2G$=)LfO~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 500
>>
stream
Gatn":J]_!(l.SY'Ye/2_>(Fu38"a/]>K/B,2uS$aq>L!XdqJ'_RH/$<bR+,.YKA+;Z&T%oCF%*O,\n.g&U:>:8\4--s!!o,q(B#.7SeV['*.;9/FO#BWW/=GnS`JBuVqPPm!:rb&mocQ,jV*\/WD:dMmp#MG7W;4[Y$s0J:N*CBM=<j]uM(f:JX[5k1shs,-1T5NSNqb8m19%1-aA:[O(>dJ:A$`F@U*?fQO>3'"i!ahQEC3X!q,BMiY?gKA_::!gVcl6m[@I?i*[$$KmTdHp$.>k+5_E0*.sN/Qr`1Rt0tcaGT!"W,+Qk0sbY/g/HFI6f\hSFVp6If*TpeBUoX&!"=&&1qhu;lIoags3*i(R(oQo#KG]Agt5\GtO6:C+P%bBMI#[n$'7gZ,P:La8UVP(K3G#ZSeFFG@,N,T:3cT0$\Sd\8mSW<VJ\alpG`h'-a",rO<MjgWQ+`qfhp5*$mJ;/=qVn9G(i'['>GaXT%6foG8"7n$;~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 139
>>
stream
Gar'`YmS?5&;9"kTAhM``PkDdF-fp`Q&W"d+=ILEo_f.2\OmWL@,P7/g.oDqYAb!JMpO^O:emFgZ@onCPq82Ws8G-_Tt6^R9oWtig34W3;D"0MP=sG_Q,?.RGJoD!mbh^0(JJ'S*<~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 137
>>
stream
Gar'`YmLOu%#!S':[scdO`SgmmFhnNfa)j*5s`"IhnSYU[Z\E7%"&Zajn-an%&[XG#`[mI'eeX5F5#B](9((,pZH62[FH7Fl==&J^W3%Z>&B5W+giG*2MH_7OtUjbJt$1U9Q2!n~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1927
>>
stream
Gatm<D,]Fb&H;+$k`9s96P:#`mO(uhjVF*rG.4W3.V_KYQo/E[1DA9<qsi:%!,;e,dOhLdR<e&bVn8ShNXZd'g"A/`!h>=EEFjdi^h<q\93`>,TfY=3I:TO*\fPRn4j+$/ZpbtXp$phb\'b_&O8/#M^Q5I4=jE()KY<Z9&._7R9P-@"BpfccW!sUH)CY]2h<WfmGLF%3kRNB3+.[lLZ8:e%hN2;n;HW9k&,Q'i*5gtVN;bSYJ(pli6e]7c+4/gf<mu-aDt&1^hJ+.bi0%]QF[1WGCcD9@NJ_Ql:`RLudC=5&s(AQ%&8G;EObKGs8=s9p9;DjP]K8<[@(50[G*u$Gpt?H%%r>PB/9Yk,5%Htu=*U'7X9Bf>nMs/Vaa.BDW`In'fhuKATg?JY>-BrU^!fLTpVdL]W.H<sMT9cT4-#s%6Gk+E&cC^QI86aM!EFqYTHTCJA?k862AM=XrGqP2,LMF7VGW!RXZ.o$b9#n'M3r$?TUp5"B7oCE-W;pK"U7h^$Mmr.5NK?akW]81TI$/RG^p@&Z"?lK?,0sCi=_dL0qiTCVM8p+!sVXc@,r&=hR[6^?TZb:X-1i.&udHnQOip_V4g/DcAJcMR"JS8rXX&X_K<=%\2,9OT(D^hXp3L>.OH[EgN#NEb_WN7M*Oo%"6J7Tr-9+.;]Jb'X*M8m/\Ch=i4,YIe'%>,F>r+Sr?:ep_bh#Q/3sZU,HgZM(,'F8EVbS<[s+1p4#m6VVGc2*#cV!$Kh5qi6cNj#g6^3#!1mb]O$CHs9mT5Wj'"j9Sg_V<$.[BAK$eUdI'S=jp%i6DGYnK<Vu;D"m;cuK_W7d4530;>jgh<`ggO$4bS\#\V2:;$=a;VpGZ_V\KTG];MQfUpo9_unV[PdEMX\rLLD0Lu#oZQf.<J;!pU+3+5\kkGGumDj,&C5"J\"2sJfUj@$c(3aTQm\\psc@)UNe[d)DR8p_R6&@lSVtKGQ:=B\Bjjhdn(++=:MmMA!SS4M,:b5gQ5I)8!+Cc?sRK^ZeLL?Ke0n1r_i;Nf>qJgA)pPsD'gE^(lcub=tOgB[UK[7i'</7=6O'>cd!/k`[nmWXHaC=#qSIoUSkD::Js2+QJ((\oEgh^.?-6Q4eGf,jG?nH6bnn@q8=,9h#R+o.%fe'h1,)Y9p4_>!oX'5CKU].Mgc0enVj?-Y*93T:=:haKP[+&2snVY'YsYZT9ehKYUIVFG_Vd5PQpm7U/;p^ndJ@Z1hO<#+RZkBjUW^6N$B_,I]Xa53W6k_0(o]B8<=J1]-kDR0G-Fu]20+K'1HNaj!,.VIeLtnLNU(5QLqKIl.LmKnL;rcqrIf5Bcn$(B4Wrk7bI/%"r63'0.YtmjF:T;l7'WF)feBF&SnW'r'4oAHWpOF[/JCR\JA,(ihf%Um&lQgpjfsd/4IX'gXj&r/NObP>R2K?*`G0f7!t"2oO^#jkDNdUW'\"3g]&[dSKn6b(Y3SZcj]nC7-QfL=9\h\QLGI1>Udlh*n>6m_E)O;VP^1D@lI00I*"c[+[',W.o2YDe+;,ng:0&N78Cs/N_0;^p-(ZbgXA'3Wt^KN/ql$89nJ1fcre;M1&=hNkF5"M_f%:UU59XW"dPS.5)e?!fcaL9@VlM'K&.Gr-JKt.="@E]*<_m#%Pp+E/jc^N]tl'$^;_u9%#814Th=R''^:"-l[!4#iXd!_T3s=JiTFiUfbJV6:>\Tu"1kuF#$?h([_Q=$42EPZ8aiQ^Xba.tQqUB@;JZoM0-tP;eNA8<EY#,1&bropR[KkijC^Y(GF`WSdNZY2"'.8<@FiDdJIcPY>].91*7h4)B,ml`D?""1KO+2p*8J?^P:UPJb1@RuPTm!Q,3p%5X4;)LWp.0@d41"=+d2!u2=>pM,JIsJILD%J3R$6aVkga"nG'k$YNZ_4mC0q%LhGK0bjLpT9]NgL"%W@np^/mR~>endstream
endobj
xref
0 28
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000488 00000 n
0000000635 00000 n
0000000783 00000 n
0000000930 00000 n
0000001045 00000 n
0000001150 00000 n
0000001356 00000 n
0000001598 00000 n
0000001804 00000 n
0000002010 00000 n
0000002216 00000 n
0000002303 00000 n
0000002633 00000 n
0000002707 00000 n
0000002812 00000 n
0000002941 00000 n
0000003047 00000 n
0000003149 00000 n
0000003238 00000 n
0000004200 00000 n
0000004791 00000 n
0000005021 00000 n
0000005249 00000 n
trailer
<<
/ID
[<b2cd8347e0a4b196bec35dc8a81f419b><b2cd8347e0a4b196bec35dc8a81f419b>]
% ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R
/Root 15 0 R
/Size 28
>>
startxref
7268
%%EOF

View File

@@ -0,0 +1,408 @@
# Media service (SFU / MCU) — AI deployment prerequisites
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R1 |
| Creation date | 2026-06-12 |
| Last modification date | 2026-06-12 |
| Co-authored | |
| Severity | medium |
<!-- doc-meta:end -->
**Document type:** Requirements / handoff (pre-SPEC)
**Date:** 2026-06-12
**Audience:** PO / sysop → AI agent for full deploy + tune on fresh Alpine HVM(s)
**Related:** [docs/specs/20100612_1_scaling.md](../docs/specs/20100612_1_scaling.md), [docs/DRs/20100612_1_scaling.md](../docs/DRs/20100612_1_scaling.md), [examples/sfu/README.md](../examples/sfu/README.md), [docs/20260608_BE_SERVICES_and_infra.md](../20260608_BE_SERVICES_and_infra.md) §SFU
**PDF:** `tmp/20260612-MEDIASERVICE-AI-requirements.pdf` — regenerate with `.venv-pdf/bin/python docs/_pdf_build/md_to_pdf.py` on the copy under `tmp/`
**Code names (product):** **SPIDERWEB** = SFU-alike, **SPIDER** = MCU-alike ([draft glossary](../docs/drafts/20100611_1_net_topology_glossary.txt))
---
---
---
\newpage
## Table of contents
<!-- toc -->
- [1. Executive answer — HVM vs Docker vs monolith BE](#1-executive-answer-hvm-vs-docker-vs-monolith-be)
- [2. What you give the AI on day zero](#2-what-you-give-the-ai-on-day-zero)
- [3. Recommended instance layout (up to 3 HVMs)](#3-recommended-instance-layout-up-to-3-hvms)
- [4. Network, DNS, and firewall prerequisites](#4-network-dns-and-firewall-prerequisites)
- [5. Integration with existing BE (isolation model)](#5-integration-with-existing-be-isolation-model)
- [6. SFU vs MCU stack choice](#6-sfu-vs-mcu-stack-choice)
- [7. Packages and software (installed later by AI)](#7-packages-and-software-installed-later-by-ai)
- [8. Sizing and bandwidth (200/200 Mbit context)](#8-sizing-and-bandwidth-200200-mbit-context)
- [9. AI workflow after prerequisites are met](#9-ai-workflow-after-prerequisites-are-met)
- [10. Out of scope for first media PoP](#10-out-of-scope-for-first-media-pop)
- [11. PO checklist (copy before handoff)](#11-po-checklist-copy-before-handoff)
- [12. Changelog](#12-changelog)
<!-- /toc -->
\newpage
---
## 1. Executive answer — HVM vs Docker vs monolith BE
### Recommendation (DR-aligned)
| Approach | Verdict | Why |
|----------|---------|-----|
| **SFU/MCU on current androidcast BE monolith** | **No** | PHP/MariaDB/RSSH/Gitea share CPU/RAM/NIC; `pm.max_children=5` today; blast radius; [INFRA.md](../docs/INFRA.md) keeps `:8089` as **other** Janus stack |
| **Fresh dedicated Alpine HVM(s)** | **Yes (primary)** | Isolated **media plane**; snapshots; UDP/WebRTC without touching hub/crashes |
| **Docker on dedicated HVM** | **Yes (preferred delivery)** | Reproducible Janus/mediasoup/coturn/ffmpeg; easy rollback; same host still “empty” at handoff |
| **Docker on existing BE** | **No** | Does not isolate bandwidth or failure domain |
**Practical model:** PO provides **13 empty Alpine HVMs** with SSH + DNS. AI installs **Docker (+ compose)** on them and runs **SPIDERWEB (SFU)** or **SPIDER (MCU)** stacks in containers. **Control plane** (signaling auth, room lifecycle, RBAC) talks to existing crashes BE via **HTTPS/OpenAPI-style APIs** — no co-location of MariaDB or PHP-FPM with media.
```mermaid
flowchart TB
subgraph Today["Today — monolith BE (unchanged)"]
FE["Gentoo FE apps.f0xx.org"]
BE["Alpine BE :80<br/>PHP MariaDB Gitea RSSH"]
end
subgraph New["New — media PoP (13 HVMs)"]
MED["media-1 Alpine HVM"]
DOCK["Docker: SFU + TURN + signaling sidecar"]
end
MOB["Android app"] --> FE
FE --> BE
MOB -.->|"WebRTC UDP/TCP<br/>post-alpha"| MED
DOCK --> MED
BE -.->|"REST: token room RBAC<br/>OpenAPI"| DOCK
class New fill:#e8f5e9,stroke:#2e7d32
```
**Not too heavy:** one HVM + Docker Compose is enough for **lab/stress**; three HVMs match **prod-shaped** split (SFU / TURN / transcode). Start with **1**; split when metrics or PO asks.
---
## 2. What you give the AI on day zero
Minimum handoff — **no media packages required yet** on the empty machine.
### 2.1 Access
| Item | Required | Example |
|------|----------|---------|
| **SSH target** | Yes | `ssh media@media-lab.example.org` or intra `10.7.x.x` |
| **Sudo** | Yes | passwordless `sudo` for `apk`, `rc-service`, docker |
| **SSH key** | Yes | laptop → HVM; optional: HVM → git read for deploy scripts |
| **Jump host** | If no public SSH | `ProxyJump` via `f0xx-monstro` / `alpine-be` (document in `~/.ssh/config`) |
### 2.2 Machine identity
| Item | Required | Notes |
|------|----------|-------|
| **Hostname** | Yes | e.g. `media-1.intra.raptor.org` |
| **OS** | Yes | Alpine 3.x x86_64 (match prod BE) |
| **vCPU / RAM** | Yes | See [§8](#8-sizing-and-bandwidth-200200-mbit-context) |
| **Root disk** | ≥ 40 GiB SSD | containers + logs |
| **Intra IP** | Yes | `10.7.0.0/8` address stable across reboots |
| **Public reach** | For external test | DNS name OR FE proxy only (see [§4](#4-network-dns-and-firewall-prerequisites)) |
### 2.3 PO-owned (snapshots, DNS, firewall)
| Item | Owner | AI needs |
|------|-------|----------|
| **VM snapshots** | PO | “Snapshot before AI session” + rollback name |
| **External DNS** | PO | A/AAAA or CNAME for WebRTC/signaling test |
| **Firewall / DNAT** | PO | UDP/TCP port list from [§4](#4-network-dns-and-firewall-prerequisites) opened on router/FE |
| **TLS certs** | PO or AI via DNS-01 | Lets Encrypt on FE or on media host |
### 2.4 Integration secrets (existing BE — read-only API)
| Item | Required | Purpose |
|------|----------|---------|
| **Crashes base URL** | Yes | `https://apps.f0xx.org/app/androidcast_project/crashes` |
| **Service account or API token** | Yes | Mint rooms / validate session (new `sfu` API or extend OpenAPI) |
| **RBAC model** | Reference | Same pattern as remote access / short links |
| **Gitea / deploy** | Optional | Pull `android_cast` branch `feature/sfu-relay` or deploy tarball |
AI does **not** need MariaDB root on monolith BE if all integration is **HTTP APIs**.
### 2.5 Decisions PO should state (or defaults)
| Decision | Default if silent |
|----------|-------------------|
| **Topology code name** | **SPIDERWEB** (SFU) first; MCU later |
| **Engine** | **mediasoup** (greenfield) — Janus on `:8089` is **other** stack, avoid unless PO picks Janus on **new** host |
| **Instance count** | **1** for lab; **3** for prod-shaped |
| **Public path** | Subdomain `media.apps.f0xx.org` → FE → media HVM (keeps TLS at FE) |
| **TURN** | **coturn** on same HVM (lab) or instance 2 (prod) |
---
## 3. Recommended instance layout (up to 3 HVMs)
### 3.1 Lab — single HVM (minimum handoff)
```mermaid
flowchart LR
subgraph HVM1["media-1 (single Alpine HVM)"]
NG["nginx :443 reverse proxy"]
SIG["signaling sidecar<br/>Node or Go"]
SFU["mediasoup / Janus SFU"]
TURN["coturn STUN/TURN"]
FF["ffmpeg worker optional"]
end
Client["App / browser"] --> NG
NG --> SIG
Client --> TURN
Client --> SFU
SIG --> SFU
BE["Existing BE PHP"] -.->|HTTPS| SIG
```
All services in **one docker-compose** stack; fine for stress and first WAN cast tests.
### 3.2 Prod-shaped — three HVMs
| VM | Role | Containers / daemons |
|----|------|----------------------|
| **media-1** | **SPIDERWEB SFU** | mediasoup (or Janus), nginx edge, signaling sidecar |
| **media-2** | **TURN + signaling** | coturn, optional dedicated signaling if scaled |
| **media-3** | **Transcode / MCU path** | ffmpeg workers, optional MCU (SPIDER) if not SFU-only |
```mermaid
flowchart TB
FE["FE TLS"] --> M1["media-1 SFU"]
FE --> M2["media-2 TURN"]
APP["Android"] -->|UDP WebRTC| M1
APP -->|UDP TURN| M2
BE["Monolith BE APIs"] -->|REST| M1
M1 --> M3["media-3 ffmpeg optional"]
```
PO can hand **one** machine first; AI documents split boundaries so instances 23 are **additive**, not rewrite.
---
## 4. Network, DNS, and firewall prerequisites
### 4.1 Traffic paths
```mermaid
flowchart TB
Internet["Internet clients"]
Router["Router 200/200 Mbit"]
FE["Gentoo FE"]
M["Media HVM(s)"]
BE["Androidcast BE :80"]
Internet --> Router --> FE
FE -->|"HTTPS signaling<br/>wss / REST"| M
FE --> BE
Internet -->|"UDP WebRTC<br/>after DNAT or 1:1"| M
```
**Do not** attach media to androidcast **`:8089`** vhost ([INFRA.md](../docs/INFRA.md) §2).
### 4.2 Ports to plan (PO opens on FE/router → media HVM)
| Port / proto | Service | Notes |
|--------------|---------|-------|
| **443/tcp** | HTTPS / WSS signaling | Via FE reverse proxy recommended |
| **3478/udp,tcp** | STUN/TURN (coturn) | Required for many NATs |
| **4915265535/udp** | WebRTC media (ICE) | Range size depends on engine; document in SFU SPEC |
| **8080/tcp** | SFU HTTP API (internal) | **Not** public; FE or intra only |
| **22/tcp** | SSH | Intra or VPN only in prod |
Exact ICE port range: set in mediasoup/Janus config during deploy; PO must allow **UDP range** on path to media HVM.
### 4.3 DNS names (examples)
| Name | Points to | Use |
|------|-----------|-----|
| `media.apps.f0xx.org` | FE → proxy → media-1 | Public signaling + health |
| `turn.apps.f0xx.org` | FE or media-2 | TURN URI in app |
| `media-lab.intra.raptor.org` | 10.7.x.x | AI + sysop intra testing |
---
## 5. Integration with existing BE (isolation model)
Media HVM **must not** share MariaDB or PHP-FPM with crashes console. Integration is **API-only**:
```mermaid
sequenceDiagram
participant App as Android app
participant FE as FE nginx
participant BE as Crashes BE PHP
participant SIG as Media signaling
participant SFU as SFU engine
App->>FE: HTTPS session / cast token
FE->>BE: existing auth APIs
App->>SIG: WSS create room (bearer)
SIG->>BE: POST validate token / RBAC
BE-->>SIG: room allowed + ttl
SIG->>SFU: create room
App->>SFU: WebRTC publish/subscribe UDP
```
| Integration | Method | Existing artifact |
|-------------|--------|-------------------|
| Health gate | GET | `api/sfu_health.php` (stub today) |
| Feature flag | config | `config.php``sfu.enabled`, URLs |
| Room mint / revoke | **New REST** | OpenAPI under crashes or `media-api` submodule |
| Auth | Bearer + session RBAC | Same as remote access / short links pattern |
| Metrics | POST ingest | Optional → `graph_upload.php` or new series |
| No direct DB | — | Media DB (if any) is **local** to HVM (Redis/SQLite) |
**OpenAPI:** extend [examples/crash_reporter/backend](../examples/crash_reporter/backend) or new `backend/media-signaling` repo later — AI needs **written** API contract before prod wire-up.
---
## 6. SFU vs MCU stack choice
| | **SPIDERWEB (SFU)** | **SPIDER (MCU)** |
|---|---------------------|------------------|
| **Use** | Multiparty cast; subscribers receive per-stream | Mix down; single output; higher CPU per room |
| **Engine candidates** | **mediasoup** (preferred greenfield), Janus VideoRoom | Janus MCU plugin, ffmpeg bridge, custom |
| **CPU** | Moderate per stream; scales with subscribers | High per room (encode) |
| **Latency** | Lower | Higher |
| **First milestone** | **Yes** | Phase 2 / media-3 |
**Janus on existing `:8089`:** treat as **foreign** — do not piggyback androidcast nginx. New Janus instance on **media HVM** only if PO rejects mediasoup.
**Docker vs bare metal on HVM:** use **Docker** for SFU/TURN/signaling; host only needs Alpine + docker + nginx + firewall tools.
---
## 7. Packages and software (installed later by AI)
PO delivers **almost empty** Alpine; AI installs when handoff complete.
### 7.1 Host (Alpine packages)
```text
# Baseline (all media HVMs)
openssh sudo bash curl wget git ca-certificates
nginx openssl iptables ip6tables
docker docker-cli-compose
# Optional host tools
tcpdump bind-tools jq python3
# If bare-metal fallback (no Docker) — not recommended first
# janus-gateway mediasoup — prefer official containers
```
### 7.2 Container images (typical compose)
| Service | Image / build | Role |
|---------|---------------|------|
| **mediasoup** or **janus** | Official or pinned Dockerfile in repo | SFU |
| **coturn** | `coturn/coturn` | STUN/TURN |
| **signaling** | Custom Node/Go from repo | Rooms, BE API client |
| **nginx** | `nginx:alpine` or host nginx | TLS termination at edge or intra |
| **ffmpeg** | `jrottenberg/ffmpeg` or alpine | Transcode sidecar (media-3) |
| **redis** | `redis:alpine` | Room state optional |
### 7.3 Repo deliverables (AI produces after deploy)
| Artifact | Location (proposed) |
|----------|---------------------|
| `docker-compose.yml` | `examples/sfu/deploy/` or `backend/media-pop/` |
| nginx FE fragment | `examples/sfu/deploy/nginx.fe-media.snippet` |
| BE config keys | `config.example.php``sfu.*` (already stubbed) |
| Smoke script | `examples/sfu/scripts/smoke_sfu.sh` |
| OpenAPI | `examples/sfu/openapi.yaml` |
---
## 8. Sizing and bandwidth (200/200 Mbit context)
WAN **200/200 Mbit** sits behind FE; media UDP may be **DNAT** to media HVM.
| Profile | vCPU | RAM | Notes |
|---------|------|-----|-------|
| **Lab / stress (1 HVM)** | **48** | **816 GiB** | SFU + TURN + signaling; ~1030 parallel WebRTC streams |
| **Prod SFU node** | **8+** | **16 GiB** | Dedicated NIC budget; see [SPEC §12.3](../docs/specs/20100612_1_scaling.md#123-network-bandwidth-and-egress) |
| **TURN only** | **2** | **4 GiB** | Mostly relay bandwidth |
| **ffmpeg node** | **8+** | **1632 GiB** | CPU-bound transcodes |
**Bandwidth rule of thumb (SFU):**
| Streams | Approx WAN need |
|---------|-----------------|
| 10 × 1 Mbps video | **~1015 Mbps** + overhead |
| 50 × 1 Mbps | **~5080 Mbps** — approaches 200 Mbit shared with rest of estate |
| 100+ viewers | **CDN/SFU PoP or second site** — not single 200 Mbit |
Stress test: measure on **intra** first, then **public DNS** through FE.
---
## 9. AI workflow after prerequisites are met
```mermaid
flowchart TD
A["PO: empty HVM + SSH + DNS + firewall list"] --> B["AI: verify ssh sudo intra ping"]
B --> C["AI: apk + docker + compose"]
C --> D["AI: deploy SFU stack lab compose"]
D --> E["AI: nginx FE fragment + BE sfu.enabled URLs"]
E --> F["Smoke: sfu_health + WSS + ICE"]
F --> G{"PO: load test OK?"}
G -->|yes| H["Document + optional 2nd/3rd HVM split"]
G -->|no| I["Tune UDP range CPU docker limits"]
I --> F
```
| Step | AI action | PO action |
|------|-----------|-----------|
| 0 | — | Snapshot empty VM |
| 1 | Probe CPU/RAM/NIC, `apk update` | Confirm DNS/firewall |
| 2 | Install docker, compose, pin versions | — |
| 3 | Bring up mediasoup + coturn + signaling | — |
| 4 | FE nginx `location /sfu/` or subdomain proxy | Reload FE nginx |
| 5 | Enable `sfu.enabled` + URLs on monolith BE **only** | Sync config.php |
| 6 | Run smoke + optional load/WebRTC client | Review metrics |
| 7 | Snapshot “golden” media PoP | — |
---
## 10. Out of scope for first media PoP
- Replacing LAN UDP cast (alpha path unchanged)
- Moving crashes/tickets/Gitea to media HVM
- Full GEO policy ([SPEC §14](../docs/specs/20100612_1_scaling.md#14-global-availability-and-geo-policy)) — later with F3/F10
- Production payments / recording storage
- Merging with Janus stack on BE `:8089`
---
## 11. PO checklist (copy before handoff)
```text
[ ] Alpine 3.x HVM provisioned (13); specs noted
[ ] SSH + sudo for AI; key installed
[ ] Intra IP 10.7.x.x stable
[ ] External DNS name for test (or FE-only proxy agreed)
[ ] Firewall: UDP 3478 + WebRTC range → media HVM
[ ] Snapshot taken (empty baseline)
[ ] Decision: SPIDERWEB (SFU) first — mediasoup default
[ ] Crashes BE base URL + API token for room validation
[ ] Confirm: do NOT use androidcast :8089 / monolith BE for SFU
[ ] Optional: 2nd/3rd HVM reserved for TURN + ffmpeg
```
---
## 12. Changelog
| Date | Change |
|------|--------|
| 2026-06-12 | Initial prerequisites — HVM vs Docker, 3-instance layout, BE API isolation, PO checklist |
---
**Documentation index:** [docs/README.md](../docs/README.md)

View File

@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Build **/*.pdf for every **/*.md (recursive; skips _pdf_build/, drafts/)."""
from __future__ import annotations
import sys
from pathlib import Path
_BUILD_DIR = Path(__file__).resolve().parent
if (_BUILD_DIR.parent / "docs").is_dir():
ROOT = _BUILD_DIR.parent
DOCS = ROOT / "docs"
else:
ROOT = _BUILD_DIR.parent
DOCS = ROOT
sys.path.insert(0, str(_BUILD_DIR))
from md_to_pdf import build_pdf_from_markdown # noqa: E402
SUBTITLE_BY_REL = {
"specs/20100611_3_url_shortener.md": "URL shortener service — SPEC (post-alpha, s.f0xx.org)",
"DRs/20100611_3_url_shortener.md": "URL shortener service — design review",
"specs/20100612_1_scaling.md": "Platform scaling — SPEC (clusters, bandwidth, GEO, cloud path)",
"DRs/20100612_1_scaling.md": "Platform scaling — DR (architecture, bandwidth, GEO policy, task graph)",
"DRs/20260618_repos_reorganizing.md": "Repository & microservice split — frozen DR R1.1",
"specs/20260618_repos_reorganizing.md": "Repository & microservice split — SPEC R1 (normative)",
"20260620_postgres_timescale_db_platform.md": "PostgreSQL, TimescaleDB, platform DB adapters — advisory",
}
def iter_markdown_files() -> list[Path]:
out: list[Path] = []
for md in sorted(DOCS.rglob("*.md")):
if "_pdf_build" in md.parts or "drafts" in md.parts:
continue
out.append(md)
return out
def rel_label(md: Path) -> str:
try:
return str(md.relative_to(DOCS))
except ValueError:
return md.name
def main() -> int:
md_files = iter_markdown_files()
if not md_files:
print("No markdown files found", file=sys.stderr)
return 1
ok = 0
for md in md_files:
out = md.with_suffix(".pdf")
rel = rel_label(md)
subtitle = SUBTITLE_BY_REL.get(rel, f"From {rel}")
try:
build_pdf_from_markdown(md, out, doc_subtitle=subtitle)
ok += 1
except Exception as e:
print(f"FAIL {rel}: {e}", file=sys.stderr)
print(f"Built {ok}/{len(md_files)} PDFs under {DOCS}/")
return 0 if ok == len(md_files) else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,384 @@
#!/usr/bin/env python3
"""Build 20260521_next_gen_steps_egress_review.pdf from summary text and diagram PNGs."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import (
Image,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
from page_footer import NumberedCanvas
ROOT = Path(__file__).resolve().parents[2]
BUILD = Path(__file__).resolve().parent
OUT_PDF = ROOT / "20260521_next_gen_steps_egress_review.pdf"
MM = [
("diagram_current_arch.mmd", "diagram_current_arch.png", "Current sender path (1 encode, N unicast sends)"),
("diagram_multicast_proposal.mmd", "diagram_multicast_proposal.png", "Proposed multicast setup (handshake then one media flow)"),
]
def render_mermaid() -> None:
for mmd, png, _ in MM:
src = BUILD / mmd
dst = BUILD / png
subprocess.run(
[
"npx",
"--yes",
"@mermaid-js/mermaid-cli@11.4.0",
"-i",
str(src),
"-o",
str(dst),
"-b",
"white",
"-w",
"1400",
"-H",
"900",
],
cwd=str(ROOT),
check=True,
timeout=180,
)
def p(text: str, style) -> Paragraph:
return Paragraph(text.replace("\n", "<br/>"), style)
def build_pdf() -> None:
styles = getSampleStyleSheet()
title = ParagraphStyle(
"DocTitle",
parent=styles["Title"],
fontSize=18,
spaceAfter=12,
textColor=colors.HexColor("#1565C0"),
)
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=14, spaceAfter=8)
h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=12, spaceBefore=10, spaceAfter=6)
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)
story = []
story.append(p("Android Cast — Next-gen egress review", title))
story.append(
p(
"Summary of sender 1:N behavior, bandwidth model, limitations, and feasibility "
"of single-stream multicast plus multi-tier (full / 720p / 480p) renditions. "
"Generated 2026-05-21.",
small,
)
)
story.append(Spacer(1, 0.4 * cm))
# --- Current state ---
story.append(p("1. Current sender behavior", h1))
story.append(
p(
"<b>How many receivers?</b> Up to 5 when MULTI_RECEIVER_ENABLED "
"(CastConfig.MAX_RECEIVERS). Sender opens one CastSession per selected device; "
"failed handshakes are skipped if at least one peer connects.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>CPU / GPU / memory:</b> Capture and encode run once per cast. "
"CastFanoutPump duplicates each encoded frame to every session. "
"Network send and per-peer UDP/FEC state scale with N; encode cost does not.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Bandwidth:</b> Per-receiver unicast duplicate of the same bitstream — not IP multicast. "
"Rough LAN egress ≈ (video + ~96 kbps audio) × N, plus per-peer protocol overhead.",
body,
)
)
story.append(Spacer(1, 0.5 * cm))
img1 = BUILD / MM[0][1]
if img1.exists():
story.append(p(MM[0][2], h2))
iw = 16 * cm
story.append(Image(str(img1), width=iw, height=iw * 0.45))
story.append(Spacer(1, 0.4 * cm))
# Table current limits
story.append(p("1.1 Current limitations (sender-focused)", h2))
limits = [
["Area", "Limitation"],
["Primary peer", "First connected peer sets negotiated MIME, adaptive tuning, and network feedback."],
["Codec", "Single encoder; all peers must tolerate the same bitstream."],
["Send path", "One CastFanoutPump thread; serial fan-out to N sessions."],
["WiFi", "Uplink often saturates before CPU on 35 receivers."],
["Topology", "Sender dials out; no shared LAN broadcast stream today."],
]
t = Table(limits, colWidths=[3.2 * cm, 13.3 * cm])
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t)
story.append(PageBreak())
# --- Proposal 1 ---
story.append(p("2. Proposal: single outgoing stream (multicast)", h1))
story.append(
p(
"<b>Goal:</b> One encoded stream on the air; only handshake-authorized receivers consume it; "
"save ~(N1) × bitrate vs todays unicast fan-out.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Feasible</b> with UDP IP multicast + unicast handshake (group address, port, sessionId, PIN). "
"Handshake-only targeting without multicast does <b>not</b> reduce bandwidth. "
"TCP/QUIC cannot deliver true 1× WiFi airtime.",
body,
)
)
story.append(Spacer(1, 0.3 * cm))
img2 = BUILD / MM[1][1]
if img2.exists():
story.append(p(MM[1][2], h2))
iw = 16 * cm
story.append(Image(str(img2), width=iw, height=iw * 0.55))
story.append(Spacer(1, 0.4 * cm))
story.append(p("2.1 Approach comparison", h2))
approaches = [
["Approach", "1× sender airtime?", "Notes"],
["Unicast fan-out (today)", "No", "Same encode; N × send()."],
["UDP multicast", "Yes on good LAN", "MulticastLock; AP client isolation risk."],
["Sender server, N TCP clients", "No", "Flip connect direction; still N wire copies."],
["LAN relay receiver", "Sender 1×", "Relay pays duplicate bandwidth."],
]
t2 = Table(approaches, colWidths=[4.5 * cm, 3.2 * cm, 8.8 * cm])
t2.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t2)
story.append(Spacer(1, 0.3 * cm))
story.append(
p(
"<b>Protocol notes:</b> CastFramingContext.sessionId and CastSessionGate already support "
"session binding. FEC/NACK must become group-scoped (one retransmit cache, resend to multicast). "
"Existing code paths: MultiCastCoordinator, CastFanoutPump, UdpCastTransport.",
body,
)
)
story.append(PageBreak())
# --- Proposal 2 ---
story.append(p("3. Proposal: multi-tier streams (e.g. full + 720p + 480p)", h1))
story.append(
p(
"<b>Goal:</b> Several encoded renditions from one capture; each receiver picks a tier at handshake "
"(e.g. full quality vs 480p for bandwidth-limited devices).",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Feasible</b> via multiple encoders fed from one capture (EGL / scaled Surfaces into separate "
"MediaCodec instances). Cost scales with <b>active</b> tiers (~23 HW encoders typical phone limit; "
"3× software VP9 is heavy). Audio usually stays one AAC stream.",
body,
)
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Bandwidth:</b> Unicast per tier → egress ≈ sum of each peers tier bitrate. "
"Multicast per tier → one group per tier; airtime ≈ sum of tiers with ≥1 subscriber.",
body,
)
)
story.append(Spacer(1, 0.3 * cm))
story.append(p("3.1 Tier architecture options", h2))
tiers = [
["Option", "Verdict"],
["Multiple HW encoders from one capture", "Recommended v1."],
["One encode + CPU re-encode", "Too expensive for live cast."],
["SVC / simulcast in one bitstream", "Poor Android HW support; defer."],
["Receiver-only downscale", "Saves sender encode, not downlink bandwidth."],
]
t3 = Table(tiers, colWidths=[6 * cm, 10.5 * cm])
t3.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t3)
story.append(PageBreak())
# --- Rollout ---
story.append(p("4. Recommended rollout", h1))
rollout = [
["Phase", "Deliverable", "Benefit"],
[
"A — Tier selection (unicast)",
"Handshake tier; 720p + 480p encoders; send chosen tier per peer",
"Per-device quality without multicast; no AP surprises",
],
[
"B — Multicast one tier",
"UDP multicast publish after unicast handshake",
"~(N1) egress for same-quality 1:N",
],
[
"C — Multicast per tier",
"Separate multicast group per tier; lazy encoder start",
"Combines (1) and (2) on good LANs",
],
]
t4 = Table(rollout, colWidths=[3.5 * cm, 7.5 * cm, 5.5 * cm])
t4.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t4)
story.append(Spacer(1, 0.4 * cm))
story.append(p("4.1 Summary judgment", h2))
summary = [
["Idea", "Feasible?", "Main win", "Main cost"],
[
"(1) Multicast + handshake gate",
"Yes (UDP)",
"~(N1) uplink on good LAN",
"Transport refactor; FEC/NACK; AP testing",
],
["(1) Unicast only", "Trivial", "Auth only", "No bandwidth win"],
["(2) Multi-tier encode", "Yes", "Per-receiver quality", "23× encode resources"],
["(1) + (2) combined", "Yes", "Multicast per tier", "Highest complexity"],
]
t5 = Table(summary, colWidths=[4.2 * cm, 2.2 * cm, 4.5 * cm, 4.6 * cm])
t5.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t5)
story.append(Spacer(1, 0.5 * cm))
story.append(
p(
"<b>Key code paths today:</b> CastConfig.MAX_RECEIVERS, MultiCastCoordinator, "
"CastFanoutPump, ScreenCastService (primary peer), UdpCastTransport, CastFramingContext, "
"CastSessionGate.",
small,
)
)
doc = SimpleDocTemplate(
str(OUT_PDF),
pagesize=A4,
leftMargin=1.8 * cm,
rightMargin=1.8 * cm,
topMargin=1.5 * cm,
bottomMargin=1.5 * cm,
title="Android Cast — Next-gen egress review",
author="Android Cast project",
)
doc.build(story, canvasmaker=NumberedCanvas)
print(f"Wrote {OUT_PDF}")
def main() -> int:
render_mermaid()
build_pdf()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""Build docs/GIT_FLOW.pdf from structured content and Mermaid diagrams.
Diagrams: Mermaid CLI renders PNG via headless Chromium (text + layout correct).
Do NOT pipe Mermaid SVG through rsvg-convert — Mermaid uses foreignObject HTML labels
that librsvg drops (boxes only, no text).
Requires: reportlab, npx (@mermaid-js/mermaid-cli), Chromium (puppeteer bundled).
Regenerate: python3 docs/_pdf_build/build_git_flow_pdf.py
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader
from reportlab.platypus import (
Image,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
from page_footer import NumberedCanvas
ROOT = Path(__file__).resolve().parents[2]
BUILD = Path(__file__).resolve().parent
CONFIG = BUILD / "mermaid-config.json"
OUT_PDF = ROOT / "docs" / "GIT_FLOW.pdf"
# (mmd, png, caption, mermaid width px, height px, scale, max width cm in PDF)
DIAGRAMS = [
("git_flow_diagram_branches.mmd", "git_flow_diagram_branches.png",
"Branch model (green master)", 1800, 900, 2, 15.5),
("git_flow_diagram_feature.mmd", "git_flow_diagram_feature.png",
"Daily development", 2400, 2000, 2, 14.0),
("git_flow_diagram_release.mmd", "git_flow_diagram_release.png",
"Release: next → master", 2400, 2000, 2, 14.0),
("git_flow_diagram_hotfix.mmd", "git_flow_diagram_hotfix.png",
"Hotfix decision and flow", 1300, 1100, 2, 14.5),
]
MAX_DIAGRAM_HEIGHT_CM = 16.5
DOC_VERSION = "1.3"
DOC_DATE = "2026-05-23"
DOC_AUTHOR = "Anton Afanaasyeu"
DOC_SOURCE = "docs/GIT_FLOW.md"
def render_mermaid() -> None:
if not CONFIG.exists():
raise FileNotFoundError(CONFIG)
for mmd, png_name, _, w_px, h_px, scale, _ in DIAGRAMS:
src = BUILD / mmd
dst = BUILD / png_name
if not src.exists():
raise FileNotFoundError(src)
subprocess.run(
[
"npx", "--yes", "@mermaid-js/mermaid-cli@11.4.0",
"-i", str(src),
"-o", str(dst),
"-c", str(CONFIG),
"-b", "white",
"-w", str(w_px),
"-H", str(h_px),
"-s", str(scale),
],
cwd=str(ROOT),
check=True,
timeout=180,
)
def diagram_size_cm(png_path: Path, max_width_cm: float) -> tuple[float, float]:
w_px, h_px = ImageReader(str(png_path)).getSize()
max_w = max_width_cm * cm
max_h = MAX_DIAGRAM_HEIGHT_CM * cm
scale = min(max_w / w_px, max_h / h_px)
return w_px * scale, h_px * scale
def p(text: str, style) -> Paragraph:
return Paragraph(text.replace("\n", "<br/>"), style)
def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> None:
data = [headers] + rows
t = Table(data, colWidths=col_widths)
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t)
def add_diagram(story, png_name: str, caption: str, h2, max_width_cm: float) -> None:
path = BUILD / png_name
if not path.exists():
return
iw, ih = diagram_size_cm(path, max_width_cm)
story.append(p(caption, h2))
story.append(Image(str(path), width=iw, height=ih))
story.append(Spacer(1, 0.35 * cm))
def build_pdf() -> None:
styles = getSampleStyleSheet()
title = ParagraphStyle(
"DocTitle", parent=styles["Title"], fontSize=18, spaceAfter=10,
textColor=colors.HexColor("#1565C0"),
)
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=12, spaceAfter=6)
h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=11, spaceBefore=8, spaceAfter=4)
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)
bullet = ParagraphStyle("Bullet", parent=body, leftIndent=14, bulletIndent=6)
story: list = []
story.append(p("Android Cast — Git flow (green master)", title))
story.append(Spacer(1, 0.15 * cm))
add_table(
story,
["Field", "Value"],
[
["Version", DOC_VERSION],
["Published", DOC_DATE],
["Author(s)", DOC_AUTHOR],
["Markdown source", DOC_SOURCE],
["Diagrams", "Mermaid CLI → PNG (Chromium); proportional embed in PDF"],
],
[4.0 * cm, 12.5 * cm],
)
story.append(Spacer(1, 0.25 * cm))
story.append(
p(
"Diagrams are rendered by headless Chromium (not rsvg/SVG rasterize) so labels stay visible. "
"Regenerate after editing docs/_pdf_build/git_flow_diagram_*.mmd.",
small,
),
)
story.append(Spacer(1, 0.3 * cm))
story.append(p("1. Goals", h1))
add_table(
story,
["Goal", "Mechanism"],
[
["Always shippable master", "Merge to master only when CI/tests pass"],
["Safe integration", "Features land on next first"],
["Clear production line", "Release tags only on master"],
["Fast production patches", "hotfix/* from master, then sync to next"],
],
[4.5 * cm, 12 * cm],
)
story.append(Spacer(1, 0.4 * cm))
story.append(p("2. Branches", h1))
add_table(
story,
["Branch", "Role", "Direct commits?"],
[
["master", "Production; green; annotated tags vX.Y.Z", "No — merge only"],
["next", "Integration / release candidate", "No — merge only"],
["feature/*, fix/*", "Short-lived work", "Yes; merge → next"],
["hotfix/*", "Urgent fix for released code", "Yes; merge → master first"],
],
[3 * cm, 8.5 * cm, 4 * cm],
)
story.append(Spacer(1, 0.3 * cm))
add_diagram(story, DIAGRAMS[0][1], DIAGRAMS[0][2], h2, DIAGRAMS[0][6])
story.append(PageBreak())
story.append(p("3. Daily development", h1))
for line in [
"git fetch && git checkout next && git pull",
"git checkout -b feature/short-description",
"Commit on topic branch; run tests before merge",
"Merge topic → next (after CI green)",
]:
story.append(p(f"{line}", bullet))
story.append(Spacer(1, 0.15 * cm))
story.append(
p(
"<b>Topic branch cleanup:</b> optional locally. On the server, enable "
"<i>delete branch on merge</i> (Gitea/GitLab/GitHub) so merged feature/* "
"branches are removed automatically on origin.",
body,
),
)
story.append(Spacer(1, 0.15 * cm))
story.append(
p(
"<b>Agents:</b> never push directly to master or next unless the user explicitly "
"requests a release or hotfix merge.",
body,
),
)
add_diagram(story, DIAGRAMS[1][1], DIAGRAMS[1][2], h2, DIAGRAMS[1][6])
story.append(PageBreak())
story.append(p("4. Release (next → master)", h1))
for line in [
"Confirm next is QA-ready and CI green",
"Merge next → master",
"Smoke / CI on master",
"Tag on master: git tag -a v0.1.2 -m \"Release v0.1.2\"",
"Sync: git checkout next && git merge master && git push",
"Continue feature branches from updated next",
]:
story.append(p(f"{line}", bullet))
add_diagram(story, DIAGRAMS[2][1], DIAGRAMS[2][2], h2, DIAGRAMS[2][6])
story.append(PageBreak())
story.append(p("5. Hotfixes — recommended strategy", h1))
story.append(
p(
"<b>Default (option 1):</b> branch hotfix/* from <b>master</b> when the bug affects "
"released code. After fix and tests: merge → master, tag patch (e.g. v0.1.3), then "
"<b>merge master → next</b> so integration does not miss the fix.",
body,
),
)
story.append(Spacer(1, 0.15 * cm))
story.append(
p(
"<b>Exception (option 2):</b> bug exists only on next — branch fix/* from next, "
"merge to next only; ships with the next normal release.",
body,
),
)
story.append(Spacer(1, 0.15 * cm))
story.append(
p(
"<b>Why not hotfix only on next?</b> next often contains unreleased features; "
"merging next to master for a patch would ship extra code or block the fix.",
body,
),
)
add_diagram(story, DIAGRAMS[3][1], DIAGRAMS[3][2], h2, DIAGRAMS[3][6])
story.append(p("5.1 Sync cheat sheet", h2))
add_table(
story,
["Event", "Action"],
[
["After release + tag", "merge master → next"],
["After hotfix + tag", "merge master → next"],
["Feature done", "merge topic → next only"],
["Wrong base branch", "rebase topic onto latest next"],
],
[4.5 * cm, 12 * cm],
)
story.append(PageBreak())
story.append(p("6. Branch protection and CI", h1))
add_table(
story,
["Rule", "master", "next"],
[
["CI required before merge", "yes", "yes"],
["No WIP direct push", "yes", "yes"],
["Annotated release tags", "yes", "no"],
["Force-push", "never", "maintainer only, rare"],
["Delete branch on merge", "n/a", "yes (topic branches)"],
],
[5.5 * cm, 5 * cm, 5 * cm],
)
story.append(Spacer(1, 0.2 * cm))
story.append(
p(
"<b>Force-push on next:</b> keep disabled by default. Allow only when you intentionally "
"rewrite integration history — never on master. Prefer fixing on topic branches before "
"merging to next.",
body,
),
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("7. Cursor / agent checklist", h1))
for line in [
"Branch from next for features and non-production fixes",
"Run tests before recommending merge to next",
"Releases and tags only with explicit user approval",
"Hotfix released code from master; sync master into next after tag",
"Never force-push master; avoid force-push on next unless user requests",
]:
story.append(p(f"{line}", bullet))
story.append(Spacer(1, 0.4 * cm))
story.append(
p("Regenerate: python3 docs/_pdf_build/build_git_flow_pdf.py", small),
)
doc = SimpleDocTemplate(
str(OUT_PDF),
pagesize=A4,
leftMargin=2 * cm,
rightMargin=2 * cm,
topMargin=2 * cm,
bottomMargin=2 * cm,
)
doc.build(story, canvasmaker=NumberedCanvas)
print(f"Wrote {OUT_PDF}")
def main() -> int:
try:
render_mermaid()
except subprocess.CalledProcessError as e:
print("Mermaid render failed:", e, file=sys.stderr)
return 1
except FileNotFoundError as e:
print(e, file=sys.stderr)
return 1
build_pdf()
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Build docs/GRAFANA_vs_others_graphvis_pivot.pdf decision memo."""
from __future__ import annotations
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
from page_footer import NumberedCanvas
ROOT = Path(__file__).resolve().parents[2]
OUT_PDF = ROOT / "docs" / "GRAFANA_vs_others_graphvis_pivot.pdf"
SOURCE_MD = ROOT / "docs" / "GRAFANA_vs_others_graphvis_pivot.md"
def p(text: str, style) -> Paragraph:
return Paragraph(text.replace("\n", "<br/>"), style)
def _cell(text: str, style: ParagraphStyle) -> Paragraph:
safe = (text or "").replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
return Paragraph(safe.replace("\n", "<br/>"), style)
def add_table(story, headers: list[str], rows: list[list[str]], col_widths) -> None:
styles = getSampleStyleSheet()
head = ParagraphStyle("TblHead", parent=styles["Normal"], fontName="Helvetica-Bold", fontSize=9, leading=11)
body = ParagraphStyle("TblBody", parent=styles["Normal"], fontSize=9, leading=11)
data = [[_cell(h, head) for h in headers]]
for row in rows:
data.append([_cell(c, body) for c in row])
t = Table(data, colWidths=col_widths)
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("WORDWRAP", (0, 0), (-1, -1), "CJK"),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]
)
)
story.append(t)
def build_pdf() -> None:
styles = getSampleStyleSheet()
title = ParagraphStyle(
"DocTitle",
parent=styles["Title"],
fontSize=18,
spaceAfter=10,
textColor=colors.HexColor("#1565C0"),
)
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=14, spaceBefore=12, spaceAfter=6)
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=10, leading=14, alignment=TA_LEFT)
small = ParagraphStyle("Small", parent=body, fontSize=9, textColor=colors.grey)
bullet = ParagraphStyle("Bullet", parent=body, leftIndent=14, bulletIndent=6)
story: list = []
story.append(p("Android Cast — Graph Visualization Pivot", title))
story.append(p("Scope: LAMP + nginx backend, no data duplication pipeline, role-based analytics.", small))
story.append(Spacer(1, 0.25 * cm))
add_table(
story,
["Field", "Value"],
[
["Date", "2026-06-02"],
["Context", "apps.f0xx.org + BE Alpine (nginx + php-fpm + MariaDB)"],
["Constraint", "No cron ETL/data copy/pipeline complexity"],
["Output", "Decision memo: Grafana vs custom PHP dashboards"],
["Markdown source", "docs/GRAFANA_vs_others_graphvis_pivot.md"],
],
[4.2 * cm, 12.2 * cm],
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("1. Executive summary", h1))
story.append(
p(
"• Grafana is viable only if it can query MariaDB directly (or via native SQL datasource) with strict RBAC mapping and SSO/session bridging.<br/>"
"• If direct DB + auth integration becomes complex, custom PHP dashboards are lower risk and fit your stack with zero infra additions.<br/>"
"• Recommended path: <b>hybrid</b> — build canonical SQL views + metrics contract once; start with custom PHP pages, optionally add Grafana later reusing same SQL.",
bullet,
)
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("2. Option comparison", h1))
add_table(
story,
["Option", "Pros", "Cons / Risks", "Stack impact"],
[
[
"Grafana (direct DB)",
"Fast charting, polished visuals, alerting, flexible panels",
"Session/RBAC integration effort; slug scoping must be enforced in queries; additional service to operate",
"Adds Grafana service only (no ETL required)",
],
[
"Custom PHP + nginx",
"Native app auth/session reuse; direct BE logic access; deterministic tenant filtering",
"More frontend/dev work for visual polish; chart UX must be implemented",
"No new runtime dependencies",
],
[
"Hybrid (recommended)",
"Ship fast with PHP now, preserve Grafana option later; one metrics SQL contract",
"Needs discipline in shared metrics layer",
"Minimal immediate changes",
],
],
[3.1 * cm, 4.4 * cm, 5.0 * cm, 3.9 * cm],
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("3. Role-based graph pack (suggested)", h1))
add_table(
story,
["Role", "Core dashboards", "Example charts"],
[
[
"User",
"Own slug, own devices/sessions",
"DAU devices, avg cast duration, avg recv sessions/day, success rate, app version split",
],
[
"Slug admin",
"All slug analytics + health",
"Active/passive users, bandwidth send/recv 1d/7d, install source pie (Play/OTA/custom), NTP source usage and correction savings",
],
[
"Platform admin",
"Cross-slug global + reliability",
"Crashes per slug/device/user, trend by app version/SDK, links to ticket IDs/issues, top regressions by fingerprint",
],
],
[2.7 * cm, 5.1 * cm, 8.6 * cm],
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("4. Effort and cost estimate (engineering)", h1))
add_table(
story,
["Track", "Initial delivery", "Hardening", "Total estimate"],
[
["Grafana direct DB", "4-7 dev days", "5-8 dev days", "9-15 dev days"],
["Custom PHP dashboards", "5-9 dev days", "3-6 dev days", "8-15 dev days"],
["Hybrid phase-1 PHP then Grafana", "6-10 dev days", "optional +4-7 days", "10-17 dev days"],
],
[4.3 * cm, 3.8 * cm, 3.8 * cm, 4.5 * cm],
)
story.append(
p(
"Assumptions: existing MariaDB schema with crash/ticket/session tables, role data available; excludes major schema migration.",
small,
)
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("5. Recommendation under your constraints", h1))
story.append(
p(
"• Do not introduce ETL/duplication. Keep one source of truth: MariaDB + live SQL views.<br/>"
"• Implement a metrics SQL layer now (views/materialized semantics via SQL only, no copied store).<br/>"
"• Build custom PHP dashboard pages first for guaranteed auth/session + slug scoping correctness.<br/>"
"• Re-evaluate Grafana after metrics contract stabilizes; adopt only if direct DB + RBAC mapping is clean.",
bullet,
)
)
story.append(Spacer(1, 0.35 * cm))
story.append(p("6. Proposed next implementation slice", h1))
add_table(
story,
["Step", "Deliverable", "ETA"],
[
["1", "Define metrics dictionary + SQL views for user/slug/admin scopes", "1-2 days"],
["2", "Add heartbeat metric: time source in use + ntp correction savings counters", "0.5-1 day"],
["3", "Create PHP dashboards: user and slug-admin pages with 1d/7d selectors", "3-5 days"],
["4", "Add admin reliability board + crash/ticket links", "2-3 days"],
["5", "Optional Grafana POC over same SQL views", "2-3 days"],
],
[1.2 * cm, 11.9 * cm, 3.3 * cm],
)
doc = SimpleDocTemplate(
str(OUT_PDF),
pagesize=A4,
leftMargin=1.8 * cm,
rightMargin=1.8 * cm,
topMargin=1.6 * cm,
bottomMargin=1.6 * cm,
title="Grafana vs Others GraphVis Pivot",
author="Android Cast project",
)
doc.build(story, canvasmaker=NumberedCanvas)
print(f"Wrote {OUT_PDF}")
if __name__ == "__main__":
build_pdf()

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Build docs/20260602_REVERSE_SSH_proposals_summary.pdf from markdown source + heading TOC."""
from __future__ import annotations
from pathlib import Path
from md_to_pdf import build_pdf_from_markdown
ROOT = Path(__file__).resolve().parents[2]
SOURCE_MD = ROOT / "docs" / "20260602_REVERSE_SSH_proposals_summary.md"
OUT_PDF = ROOT / "docs" / "20260602_REVERSE_SSH_proposals_summary.pdf"
def build_pdf() -> None:
build_pdf_from_markdown(
SOURCE_MD,
OUT_PDF,
doc_subtitle="Generated from markdown — rev. 13 + Appendix 1 (WG test) + Appendix 2 (WG production)",
)
if __name__ == "__main__":
build_pdf()

View File

@@ -0,0 +1,15 @@
flowchart LR
subgraph once [Once per cast]
Cap[MediaProjection capture]
Enc[Video and audio encoders]
Q[CastFanoutPump queues]
end
subgraph perPeer [Per receiver today]
S1[CastSession 1 UDP or TCP]
S2[CastSession 2]
SN[CastSession N]
end
Cap --> Enc --> Q
Q -->|same encoded payload| S1
Q --> S2
Q --> SN

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,9 @@
sequenceDiagram
participant S as Sender
participant R1 as Receiver 1
participant R2 as Receiver N
S->>R1: Unicast handshake PIN codec tier
S->>R2: Unicast handshake
Note over S: One encoder one sessionId
S-->>R1: Multicast media packets
S-->>R2: Same packets

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,16 @@
%% Branch model — render PNG via mermaid-cli (Chromium). Do not rasterize SVG with rsvg.
flowchart TB
M["master\n(always green, tagged releases)"]
N["next\n(integration / release candidate)"]
F["feature/* fix/*\n(fork next)"]
H["hotfix/*\n(fork master)"]
F -->|"merge + CI green"| N
N -->|"release: merge + CI green"| M
H -->|"urgent patch + CI green"| M
M -->|"sync after release or hotfix"| N
style M fill:#c8e6c9,stroke:#2e7d32
style N fill:#bbdefb,stroke:#1565c0
style F fill:#fff9c4,stroke:#f9a825
style H fill:#ffccbc,stroke:#e64a19

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,14 @@
sequenceDiagram
autonumber
participant Dev as Developer or agent
participant Topic as feature/* or fix/*
participant Next as next
participant CI as Tests / CI
Dev->>Next: fetch, checkout, pull
Dev->>Topic: create branch from next
Dev->>Topic: commits
Dev->>CI: run tests
CI-->>Dev: pass
Dev->>Next: merge topic branch
Note over Next: delete branch on merge<br/>(origin server setting)

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,21 @@
flowchart TD
Start([Defect found])
Q{Affects released code\non master / latest tag?}
Start --> Q
Q -->|Yes| A[Branch hotfix/* from master]
A --> B[Fix + tests]
B --> C[Merge to master]
C --> D[Tag patch on master e.g. v0.1.3]
D --> E[Merge master into next]
E --> End([Done])
Q -->|No, only on next| F[Branch fix/* from next]
F --> G[Fix + tests]
G --> H[Merge to next only]
H --> End2([Ships with next release])
style C fill:#c8e6c9
style D fill:#c8e6c9
style E fill:#bbdefb
style H fill:#bbdefb

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -0,0 +1,16 @@
sequenceDiagram
autonumber
participant Next as next
participant Master as master
participant CI as Tests / CI
participant Tag as tag vX.Y.Z
Note over Next: feature complete,<br/>QA ok
Next->>CI: full test suite
CI-->>Next: green
Next->>Master: merge next to master
Master->>CI: smoke on master
CI-->>Master: green
Master->>Tag: annotated tag on master
Master->>Next: merge master into next (sync)
Note over Next: continue features<br/>from synced next

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

48
_pdf_build/md_common.py Normal file
View File

@@ -0,0 +1,48 @@
"""Shared markdown heading / slug helpers for docs PDF and TOC scripts."""
from __future__ import annotations
import re
TOC_START = "<!-- toc -->"
TOC_END = "<!-- /toc -->"
def github_slug(text: str) -> str:
"""GitHub/Cursor-compatible heading anchor."""
text = text.strip().lower()
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
text = re.sub(r"[*_]+", "", text)
text = re.sub(r"[^\w\s-]", "", text)
text = re.sub(r"\s+", "-", text)
text = re.sub(r"-+", "-", text)
return text.strip("-")
def collect_headings(lines: list[str]) -> list[tuple[int, str, str]]:
"""Return (level, title, slug) for ## and ### headings."""
out: list[tuple[int, str, str]] = []
used: dict[str, int] = {}
for line in lines:
m = re.match(r"^(#{2,3})\s+(.+?)\s*$", line)
if not m:
continue
level = len(m.group(1))
title = m.group(2).strip()
if title == "Table of contents":
continue
base = github_slug(title)
if not base:
continue
n = used.get(base, 0)
slug = base if n == 0 else f"{base}-{n}"
used[base] = n + 1
out.append((level, title, slug))
return out
def strip_md_link_text(title: str) -> str:
t = re.sub(r"\*\*([^*]+)\*\*", r"\1", title)
t = re.sub(r"`([^`]+)`", r"\1", t)
return t

393
_pdf_build/md_to_pdf.py Normal file
View File

@@ -0,0 +1,393 @@
"""Render a markdown file to PDF with TOC from ## / ### headings (internal links + outline)."""
from __future__ import annotations
import re
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import (
Flowable,
PageBreak,
Paragraph,
Preformatted,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
from md_common import TOC_END, TOC_START, collect_headings, github_slug, strip_md_link_text
from mermaid_render import (
cached_mermaid_png,
diagram_flowables,
ensure_diagram_caption_style,
)
from page_footer import NumberedCanvas
def _xml(text: str) -> str:
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
def _inline_md(text: str) -> str:
text = _xml(text)
text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
text = re.sub(r"`([^`]+)`", r'<font face="Courier" size="7">\1</font>', text)
text = re.sub(
r"\[([^\]]+)\]\(#([^)]+)\)",
r'<a href="#\2" color="#1565C0">\1</a>',
text,
)
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
return text
class AnchorParagraph(Paragraph):
"""Paragraph with PDF bookmark anchor."""
def __init__(self, text, style, *, anchor: str, outline_level: int):
super().__init__(f'<a name="{anchor}"/>{text}', style)
self._anchor = anchor
self._outline_level = outline_level
class OutlineDocTemplate(SimpleDocTemplate):
def afterFlowable(self, flowable: Flowable) -> None:
if isinstance(flowable, AnchorParagraph):
self.canv.bookmarkPage(flowable._anchor)
self.canv.addOutlineEntry(
flowable.getPlainText(),
flowable._anchor,
level=max(0, flowable._outline_level - 2),
)
def _cell(text: str, style: ParagraphStyle) -> Paragraph:
return Paragraph(_inline_md(text), style)
def _parse_table_row(line: str) -> list[str]:
line = line.strip()
if line.startswith("|"):
line = line[1:]
if line.endswith("|"):
line = line[:-1]
return [c.strip() for c in line.split("|")]
def _is_table_sep(line: str) -> bool:
return bool(re.match(r"^\|?[\s\-:|]+\|?$", line.strip()))
def build_slug_by_line(lines: list[str]) -> dict[int, str]:
"""Map source line index → anchor slug (same rules as collect_headings)."""
out: dict[int, str] = {}
used: dict[str, int] = {}
for i, line in enumerate(lines):
m = re.match(r"^(#{2,3})\s+(.+?)\s*$", line)
if not m:
continue
title = m.group(2).strip()
if title == "Table of contents":
continue
base = github_slug(title)
if not base:
continue
n = used.get(base, 0)
slug = base if n == 0 else f"{base}-{n}"
used[base] = n + 1
out[i] = slug
return out
def build_pdf_from_markdown(
source_md: Path,
out_pdf: Path,
*,
doc_title: str | None = None,
doc_subtitle: str = "",
) -> None:
lines = source_md.read_text(encoding="utf-8").splitlines()
headings = collect_headings(lines)
slug_by_line = build_slug_by_line(lines)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"DocTitle",
parent=styles["Title"],
fontSize=15,
spaceAfter=5,
textColor=colors.HexColor("#1565C0"),
)
toc_title = ParagraphStyle(
"TocTitle",
parent=styles["Heading1"],
fontSize=12,
spaceBefore=4,
spaceAfter=6,
textColor=colors.HexColor("#1565C0"),
)
toc_item = ParagraphStyle(
"TocItem",
parent=styles["Normal"],
fontSize=8,
leading=11,
leftIndent=0,
)
toc_sub = ParagraphStyle(
"TocSub",
parent=toc_item,
leftIndent=14,
fontSize=7.5,
leading=10,
)
h1 = ParagraphStyle("H1", parent=styles["Heading1"], fontSize=11, spaceBefore=10, spaceAfter=4)
h2 = ParagraphStyle("H2", parent=styles["Heading2"], fontSize=9.5, spaceBefore=6, spaceAfter=3)
body = ParagraphStyle("Body", parent=styles["Normal"], fontSize=8, leading=10.5, alignment=TA_LEFT)
bullet = ParagraphStyle("Bullet", parent=body, leftIndent=12, bulletIndent=4, fontSize=8, leading=10.5)
small = ParagraphStyle("Small", parent=body, fontSize=7, textColor=colors.grey)
code_style = ParagraphStyle("Code", parent=body, fontName="Courier", fontSize=6.5, leading=8)
tbl_head = ParagraphStyle("TblHead", parent=body, fontName="Helvetica-Bold", fontSize=7, leading=9)
tbl_body = ParagraphStyle("TblBody", parent=body, fontSize=7, leading=9)
story: list = []
# Title from first # line if not overridden
if doc_title is None:
for line in lines:
if line.startswith("# "):
doc_title = line[2:].strip()
break
doc_title = doc_title or source_md.stem
story.append(Paragraph(_inline_md(doc_title), title_style))
if doc_subtitle:
story.append(Paragraph(_inline_md(doc_subtitle), small))
story.append(Spacer(1, 0.15 * cm))
# Optional first-page metadata table (doc template block)
meta_start = next((i for i, ln in enumerate(lines) if ln.strip() == "<!-- doc-meta:start -->"), None)
meta_end = next((i for i, ln in enumerate(lines) if ln.strip() == "<!-- doc-meta:end -->"), None)
if meta_start is not None and meta_end is not None and meta_end > meta_start:
meta_rows = [ln for ln in lines[meta_start + 1 : meta_end] if ln.strip().startswith("|")]
if len(meta_rows) >= 2:
headers = _parse_table_row(meta_rows[0])
data = [[_cell(h, tbl_head) for h in headers]]
for row_line in meta_rows[2:]: # skip separator row
row = _parse_table_row(row_line)
padded = row + [""] * (len(headers) - len(row))
data.append([_cell(c, tbl_body) for c in padded[: len(headers)]])
if data and len(headers) > 0:
col_widths = [5.2 * cm, 11.0 * cm] if len(headers) == 2 else [(16.2 * cm) / len(headers)] * len(headers)
meta_tbl = Table(data, colWidths=col_widths)
meta_tbl.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 4),
("RIGHTPADDING", (0, 0), (-1, -1), 4),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
]
)
)
story.append(meta_tbl)
story.append(Spacer(1, 0.2 * cm))
# Page 1: title + metadata, page 2: TOC
story.append(PageBreak())
# Table of contents (clickable internal links)
story.append(Paragraph("Table of contents", toc_title))
for level, title, slug in headings:
text = strip_md_link_text(title)
link = f'<a href="#{slug}" color="#1565C0">{_xml(text)}</a>'
style = toc_sub if level == 3 else toc_item
story.append(Paragraph(link, style))
story.append(Spacer(1, 0.2 * cm))
story.append(
Paragraph(
f"<i>Source: {source_md.name} — section links work in PDF viewers that support internal anchors.</i>",
small,
)
)
story.append(PageBreak())
# Body — skip title, TOC block, index link
skip_toc = False
in_code = False
code_lang = ""
code_lines: list[str] = []
ensure_diagram_caption_style(styles)
i = 0
while i < len(lines):
line = lines[i]
if line.strip() == TOC_START:
skip_toc = True
i += 1
continue
if line.strip() == TOC_END:
skip_toc = False
i += 1
continue
if skip_toc:
i += 1
continue
if line.startswith("# ") and not line.startswith("## "):
i += 1
continue
if line.strip() == "## Table of contents":
i += 1
continue
if line.strip().startswith("**Documentation index:**"):
i += 1
continue
if line.strip() == "<!-- doc-meta:start -->":
while i < len(lines) and lines[i].strip() != "<!-- doc-meta:end -->":
i += 1
i += 1
continue
if line.strip() == "<!-- doc-meta:end -->":
i += 1
continue
if line.strip() == "\\newpage":
story.append(PageBreak())
i += 1
continue
if line.strip().startswith("```"):
if in_code:
block = "\n".join(code_lines)
if code_lang == "mermaid" and block.strip():
try:
png = cached_mermaid_png(block)
story.extend(diagram_flowables(png, styles))
except Exception as exc:
story.append(
Paragraph(
f"<i>Diagram render failed ({_xml(str(exc))}) — source:</i>",
small,
)
)
story.append(Preformatted(block, code_style))
story.append(Spacer(1, 0.08 * cm))
else:
story.append(Preformatted(block, code_style))
story.append(Spacer(1, 0.08 * cm))
code_lines = []
code_lang = ""
in_code = False
else:
in_code = True
code_lang = line.strip()[3:].strip().lower()
i += 1
continue
if in_code:
code_lines.append(line)
i += 1
continue
if line.startswith("### "):
title = line[4:].strip()
slug = slug_by_line.get(i, github_slug(title))
story.append(
AnchorParagraph(_inline_md(title), h2, anchor=slug, outline_level=3)
)
i += 1
continue
if line.startswith("## "):
title = line[3:].strip()
slug = slug_by_line.get(i, github_slug(title))
story.append(
AnchorParagraph(_inline_md(title), h1, anchor=slug, outline_level=2)
)
i += 1
continue
if line.strip() == "---":
story.append(Spacer(1, 0.1 * cm))
i += 1
continue
if line.strip().startswith("|") and i + 1 < len(lines) and _is_table_sep(lines[i + 1]):
headers = _parse_table_row(line)
i += 2
rows: list[list[str]] = []
while i < len(lines) and lines[i].strip().startswith("|"):
rows.append(_parse_table_row(lines[i]))
i += 1
ncol = len(headers)
if ncol:
width = (16.2 * cm) / ncol
data = [[_cell(h, tbl_head) for h in headers]]
for row in rows:
padded = row + [""] * (ncol - len(row))
data.append([_cell(c, tbl_body) for c in padded[:ncol]])
t = Table(data, colWidths=[width] * ncol)
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F5F5")]),
("LEFTPADDING", (0, 0), (-1, -1), 3),
("RIGHTPADDING", (0, 0), (-1, -1), 3),
("TOPPADDING", (0, 0), (-1, -1), 2),
("BOTTOMPADDING", (0, 0), (-1, -1), 2),
]
)
)
story.append(t)
story.append(Spacer(1, 0.08 * cm))
continue
if line.strip().startswith("- "):
story.append(Paragraph(f"{_inline_md(line.strip()[2:])}", bullet))
i += 1
continue
if line.strip().startswith("_") and line.strip().endswith("_") and len(line.strip()) > 2:
story.append(Paragraph(f"<i>{_inline_md(line.strip()[1:-1])}</i>", small))
i += 1
continue
if not line.strip():
i += 1
continue
story.append(Paragraph(_inline_md(line.strip()), body))
i += 1
doc = OutlineDocTemplate(
str(out_pdf),
pagesize=A4,
leftMargin=1.4 * cm,
rightMargin=1.4 * cm,
topMargin=1.3 * cm,
bottomMargin=1.3 * cm,
title=doc_title,
author="Android Cast project",
)
doc.build(story, canvasmaker=NumberedCanvas)
print(f"Wrote {out_pdf} ({len(headings)} TOC entries from markdown headings)")

View File

@@ -0,0 +1,17 @@
{
"theme": "neutral",
"flowchart": {
"htmlLabels": true,
"curve": "basis"
},
"sequence": {
"diagramMarginX": 120,
"diagramMarginY": 28,
"actorMargin": 90,
"noteMargin": 24,
"messageMargin": 48,
"boxMargin": 14,
"mirrorActors": false,
"useMaxWidth": false
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Some files were not shown because too many files have changed in this diff Show More