initial
165
20260521_next_gen_steps_egress_review.pdf
Normal file
1087
20260602_REVERSE_SSH_proposals_summary.md
Normal file
1064
20260602_REVERSE_SSH_proposals_summary.pdf
Normal file
454
20260607-2FA-email-mobile-auth-flow.md
Normal 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; “Can’t 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.5–1 day ops |
|
||||
| **P1 — Register + verify** | Pending table, mailer, verify link, captcha | 2–3 days |
|
||||
| **P2 — TOTP login** | Enroll QR + challenge on login | 1–2 days |
|
||||
| **P3 — Admin tools** | Recovery email, clear lockouts, audit | 1 day |
|
||||
| **P4 — WebAuthn** | Passkey enroll/login (Chrome/Android) | 1–2 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) |
|
||||
549
20260607-2FA-email-mobile-auth-flow.pdf
Normal file
370
20260608_ALPHA_PRIORITIES.md
Normal 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.1–2.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.3–5.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 (G2–G6) | 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.1–2.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.1–2.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 |
|
||||
430
20260608_ALPHA_PRIORITIES.pdf
Normal file
278
20260608_BE_SERVICES_and_infra.md
Normal 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 `18000–18999` |
|
||||
| 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.
|
||||
397
20260608_BE_SERVICES_and_infra.pdf
Normal file
160
20260608_STREAM_DUMP_DEBUG.md
Normal 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
|
||||
289
20260608_STREAM_DUMP_DEBUG.pdf
Normal 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
@@ -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 doc’s 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
@@ -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<hYdWj5L6P)^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
|
||||
258
20260620_postgres_timescale_db_platform.md
Normal 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 | Medium–High | `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):** 1–2 weeks focused for schema + repos + CI, **not** blocking SPEC Steps 2–5 if Postgres is chosen before `ac-platform-db` migrations are copied out of the monolith.
|
||||
|
||||
**Duplication window:** dual-write (application writes both) for 24–72h *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 |
|
||||
444
20260620_postgres_timescale_db_platform.pdf
Normal 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
@@ -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 Wi‑Fi 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**, **B1–B4**, and **C1** pass on your reference sender + receiver pair. **Full alpha** additionally requires backend deploy and **F1–F5** (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**, **B1–B4**, **C1**) **and** backend items above **and** **F1–F5** **and** **G1–G6** 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
@@ -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
@@ -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 Wi‑Fi. 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 Wi‑Fi; 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
@@ -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
@@ -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 0–10** |
|
||||
| **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 0–10** |
|
||||
|
||||
**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 | 10–40 ms PCM chunk | **0–1 frame** (~0–33 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 (3–8)** | Short FIFO | Ring + STFT state | **100–400 ms+** | **Movie cast**, VOD-like |
|
||||
| **Heavy stream ML** | Long context | Streaming neural | **200 ms–2 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 3–4 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** | Small–medium | Trivial | Yes (tiles) |
|
||||
| **Single: tone / CLAHE-lite** | LUT + luma histogram tiles | Low–med | 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 | Med–high | Medium | **+1 frame** | Yes |
|
||||
| **2-frame: residual / delta enhance** | `enhanced = f(curr) + α·(curr − warped_prev)` | Medium | Yes | Maybe | Medium | Med–high on repetitive motion | **+1 frame** | Yes |
|
||||
| **2-frame: RIFE-lite / FILM-small** | Interpolate between frames | Med–high | Borderline 720p | Unlikely 1080p30 | High if custom | High for large gaps | Timing care | Yes |
|
||||
| **Stream: IIR TNR / motion-mask accumulate** | 3–4 frame buffer | Medium | Yes | Maybe | High | High static; trail risk | **+2–3 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 | +10–20 ms | Yes (infer thread) |
|
||||
| **2-window: Wiener / min-stats STFT** | 20 ms, 50% hop | Medium | Yes | Low | Med–high | +1 hop (~10 ms) | Yes |
|
||||
| **2-window: PLC** | Predict from last 2 frames | Low–med | Yes | Low | High only on loss | Keep clock | Yes |
|
||||
| **Stream: multiband + AGC** | Ring buffer envelopes | Low–med | Yes | Low | Medium intelligibility | 5–30 ms | Yes |
|
||||
| **Stream: DeepFilterNet2** | Stateful STFT+GRU | Med–high | Yes modern ARM | NNAPI | High | 20–40 ms | Yes |
|
||||
|
||||
---
|
||||
|
||||
## 5. Single vs 2-frame vs stream — decision matrix
|
||||
|
||||
| Criterion | Single (2.2) | 2-frame (2.1) | Stream (3–8) |
|
||||
|-----------|--------------|---------------|--------------|
|
||||
| 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) | Low–medium | Medium + motion | High static / medium action |
|
||||
| Quality gain (audio) | Medium | Med–high | 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` | **3–4** (new GLES: decode → FBO → display) |
|
||||
| **2.2 Audio receiver** | `AudioDecoder` PCM before `AudioTrack` | **2** |
|
||||
| **2.1 Video sender** | Between capture and `VideoEncoder.prepare()` Surface | **4–5** (intermediate EGL Surface, rotation) |
|
||||
| **2.1 Audio sender** | Before `AudioEncoder` | **2–3** |
|
||||
| **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 @ 720p–1080p30 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 | **3–4** |
|
||||
| **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 | Medium–high | **3** |
|
||||
| **5** | Sender audio light prep | 2.1 | SpeexDSP / RNNoise → AAC | Low–medium | **2–3** |
|
||||
| **6** | Sender video denoise only | 2.1 | Bilateral / fast NLMeans (not SR) | Low–medium (bitrate) | **4–5** |
|
||||
| **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) | 1–2 | Double-buffered FBO / AHardwareBuffer |
|
||||
| CPU fallback (NEON) | 2–4 | 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**; **2–4 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 (3–4)** 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 |
|
||||
|
||||
**User’s 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
@@ -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
@@ -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
@@ -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[hh-?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
@@ -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
@@ -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>4Z9rcuZOEF+;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
@@ -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
@@ -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
|
||||
394
DRs/20100611_3_url_shortener.md
Normal 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 6–7).
|
||||
- **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 | 4–7 | 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)
|
||||
565
DRs/20100611_3_url_shortener.pdf
Normal file
676
DRs/20100612_1_scaling.md
Normal 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 (3–7 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 | 2–4 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 | Low–med | 2 GiB | Tiny | be-vpn |
|
||||
| ffmpeg (future) | **Sustained high** | 8–32 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 **1–7 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) | 5–20 | Normal |
|
||||
| OTA drop (1000 devices × 40 MB APK) | **200–400** burst 10 min | **CDN required** |
|
||||
| Recording download (F3) | 50–200 per hot file | Object store + CDN signed URLs |
|
||||
| SFU fan-out 100 viewers | 50–500+ | 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 F5–F9 | 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 | F6–F8 |
|
||||
|
||||
**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:** 3–7 ([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** F1–F9; 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? | P7–P9 |
|
||||
| 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 O1–O7, 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
426
DRs/20260616_rssh_routed_egress.md
Normal 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 today’s 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 | 2–3 d |
|
||||
| P2 | tun2socks + `VpnService` + route prefs | 5–8 d |
|
||||
| P3 | Bastion NAT + self-test + docs | 2–3 d |
|
||||
| P4 | Unit/integration tests, soak | 3–5 d |
|
||||
|
||||
**Total if pursued:** ~3–4 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
|
||||
675
DRs/20260616_rssh_routed_egress.pdf
Normal file
375
DRs/20260617_codec2_ulbw_voice.md
Normal 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 PO’s 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** (32–128 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 **< 3–4 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** | 700–3200 bps | ULBW voice-only | **Integrate** (post-6.1) |
|
||||
| **Opus SILK NB** | ~6–12 kbps | General narrowband when link allows | Finish JNI; keep in AUTO above Codec2 only when bandwidth sufficient |
|
||||
| **Speex NB** | ~2–15 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 150–300 ms | Document; jitter buffer 80–120 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 |
|
||||
669
DRs/20260617_codec2_ulbw_voice.pdf
Normal file
1043
DRs/20260618_repos_reorganizing.md
Normal file
1084
DRs/20260618_repos_reorganizing.pdf
Normal file
40
DRs/20260620_live_cast_control_plane.md
Normal 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.
|
||||
178
DRs/20260620_live_cast_control_plane.pdf
Normal 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
@@ -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 GitHub’s “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
142
GRAFANA_vs_others_graphvis_pivot.md
Normal 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`
|
||||
327
GRAFANA_vs_others_graphvis_pivot.pdf
Normal 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�@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
@@ -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
@@ -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
@@ -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 > 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
@@ -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
@@ -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 | cast01–03 reachable | **Ongoing** — lab VMs; DNS `c1–c3.acl0.f0xx.org` noted in `cluster.env` |
|
||||
|
||||
---
|
||||
|
||||
## Postponed / off graph (DNS + mail)
|
||||
|
||||
| ID | Owner | Note |
|
||||
|----|-------|------|
|
||||
| **2.1–2.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.1–6.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["cast01–03<br/>c1–c3.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.1–6.6** | Developer (~99%) | 0.1 ✓ | Open |
|
||||
| **9.x** | Agent (low) | — | Open |
|
||||
| **cluster0** | Agent | NFS/DNS | cast01–03 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 (c1–c3.acl0.f0xx.org), 6.1/6.3–6.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** — cast01–03 integration tests
|
||||
7. **6.1 / 6.3–6.6** — app LAN quality
|
||||
8. **9.x** — landing polish
|
||||
306
OPEN_TASKS_GRAPH.pdf
Normal file
191
OPUS_SPEEX_VALIDATION.md
Normal 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 Wi‑Fi ↔ 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 Wi‑Fi or `tc netem` on Linux AP if available).
|
||||
|
||||
---
|
||||
|
||||
## Pass criteria
|
||||
|
||||
- All **automated** tests green (`validate_opus_speex.sh`).
|
||||
- Matrix **A1–A4** and **B1–B2** 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
@@ -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
@@ -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 0–99):
|
||||
|
||||
```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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 **1–7 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
@@ -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&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
@@ -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 1–4 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 DNAT’d, 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).
|
||||
289
REMOTE_ACCESS_VALIDATION.pdf
Normal 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
@@ -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 | ☐ B4–B5 |
|
||||
|
||||
### 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:** Wi‑Fi 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 Wi‑Fi 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) Reed–Solomon + `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
@@ -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
@@ -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` ~983–996: `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
@@ -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
@@ -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 **Wi‑Fi** (UDP/TCP/QUIC). USB-C→HDMI adapters use Android’s **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 |
|
||||
| Wi‑Fi 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 Wi‑Fi cast or receiver-on-HDMI.
|
||||
|
||||
See [ROADMAP.md](ROADMAP.md) for alpha and commercial tracks.
|
||||
228
USB_HDMI_CAST.pdf
Normal 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
@@ -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
@@ -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
|
||||
408
_pdf_build/20260612_MEDIASERVICE_AI_requirements.md
Normal 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 **1–3 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 (1–3 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 | Let’s 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 2–3 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 |
|
||||
| **49152–65535/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)** | **4–8** | **8–16 GiB** | SFU + TURN + signaling; ~10–30 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+** | **16–32 GiB** | CPU-bound transcodes |
|
||||
|
||||
**Bandwidth rule of thumb (SFU):**
|
||||
|
||||
| Streams | Approx WAN need |
|
||||
|---------|-----------------|
|
||||
| 10 × 1 Mbps video | **~10–15 Mbps** + overhead |
|
||||
| 50 × 1 Mbps | **~50–80 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 (1–3); 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)
|
||||
67
_pdf_build/build_all_docs_pdf.py
Executable 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())
|
||||
384
_pdf_build/build_egress_review_pdf.py
Normal 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."],
|
||||
["Wi‑Fi", "Uplink often saturates before CPU on 3–5 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 ~(N−1) × bitrate vs today’s 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× Wi‑Fi 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 (~2–3 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 peer’s 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",
|
||||
"~(N−1) 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)",
|
||||
"~(N−1) 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", "2–3× 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())
|
||||
342
_pdf_build/build_git_flow_pdf.py
Normal 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())
|
||||
216
_pdf_build/build_graphvis_pivot_pdf.py
Normal 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("&", "&").replace("<", "<").replace(">", ">")
|
||||
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()
|
||||
24
_pdf_build/build_reverse_ssh_proposals_pdf.py
Normal 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. 1–3 + Appendix 1 (WG test) + Appendix 2 (WG production)",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_pdf()
|
||||
15
_pdf_build/diagram_current_arch.mmd
Normal 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
|
||||
BIN
_pdf_build/diagram_current_arch.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
9
_pdf_build/diagram_multicast_proposal.mmd
Normal 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
|
||||
BIN
_pdf_build/diagram_multicast_proposal.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
16
_pdf_build/git_flow_diagram_branches.mmd
Normal 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
|
||||
BIN
_pdf_build/git_flow_diagram_branches.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
14
_pdf_build/git_flow_diagram_feature.mmd
Normal 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)
|
||||
BIN
_pdf_build/git_flow_diagram_feature.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
21
_pdf_build/git_flow_diagram_hotfix.mmd
Normal 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
|
||||
BIN
_pdf_build/git_flow_diagram_hotfix.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
16
_pdf_build/git_flow_diagram_release.mmd
Normal 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
|
||||
BIN
_pdf_build/git_flow_diagram_release.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
48
_pdf_build/md_common.py
Normal 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
@@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
|
||||
|
||||
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)")
|
||||
17
_pdf_build/mermaid-config.json
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
_pdf_build/mermaid_cache/07a5cd069b637cab.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
_pdf_build/mermaid_cache/1d2a0f76fd56e1a5.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
_pdf_build/mermaid_cache/2e7a43ec4681d483.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
_pdf_build/mermaid_cache/3111adc4a19bf320.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
_pdf_build/mermaid_cache/3635a4d16c6483fd.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
_pdf_build/mermaid_cache/3ae0480d9417495d.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
_pdf_build/mermaid_cache/4005e1350215307a.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
_pdf_build/mermaid_cache/422109ab8f08520c.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
_pdf_build/mermaid_cache/442b6b0bf19a42e4.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
_pdf_build/mermaid_cache/4ddef865fc76605b.png
Normal file
|
After Width: | Height: | Size: 80 KiB |