diff --git a/docs/20260610_SERVICES.md b/docs/20260610_SERVICES.md new file mode 100644 index 0000000..299324c --- /dev/null +++ b/docs/20260610_SERVICES.md @@ -0,0 +1,336 @@ +# Android Cast — exposed services catalog + +_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 + + +- [Validation snapshot](#validation-snapshot) +- [Hub landing](#hub-landing) +- [Console home](#console-home) +- [Issues (crash reports)](#issues-crash-reports) +- [Tickets](#tickets) +- [Graphs](#graphs) +- [Remote access](#remote-access) +- [RBAC / access control](#rbac--access-control) +- [APK builder](#apk-builder) +- [Git (Gitea)](#git-gitea) +- [OTA updates](#ota-updates) +- [Heartbeat (device)](#heartbeat-device) +- [Auth & account security](#auth--account-security) +- [Non-HTTP services](#non-http-services) +- [Validation scripts](#validation-scripts) + + +--- + +## 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) + +--- + +## 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` diff --git a/docs/20260610_SERVICES.pdf b/docs/20260610_SERVICES.pdf new file mode 100644 index 0000000..70ce67d --- /dev/null +++ b/docs/20260610_SERVICES.pdf @@ -0,0 +1,532 @@ +%PDF-1.4 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 32 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 716.2874 115.501 727.2874 ] /Subtype /Link /Type /Annot +>> +endobj +5 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 705.2874 88.37304 716.2874 ] /Subtype /Link /Type /Annot +>> +endobj +6 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 694.2874 97.26104 705.2874 ] /Subtype /Link /Type /Annot +>> +endobj +7 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 45.68504 683.2874 123.037 694.2874 ] /Subtype /Link /Type /Annot +>> +endobj +8 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 672.8499 120.135 682.8499 ] /Subtype /Link /Type /Annot +>> +endobj +9 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 38 0 R /Fit ] /Rect [ 59.68504 662.8499 109.2825 672.8499 ] /Subtype /Link /Type /Annot +>> +endobj +10 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 59.68504 652.8499 133.4625 662.8499 ] /Subtype /Link /Type /Annot +>> +endobj +11 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 45.68504 642.2874 71.02104 653.2874 ] /Subtype /Link /Type /Annot +>> +endobj +12 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 59.68504 631.8499 120.135 641.8499 ] /Subtype /Link /Type /Annot +>> +endobj +13 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 59.68504 621.8499 103.4475 631.8499 ] /Subtype /Link /Type /Annot +>> +endobj +14 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 59.68504 611.8499 164.295 621.8499 ] /Subtype /Link /Type /Annot +>> +endobj +15 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 45.68504 601.2874 71.91704 612.2874 ] /Subtype /Link /Type /Annot +>> +endobj +16 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 59.68504 590.8499 120.135 600.8499 ] /Subtype /Link /Type /Annot +>> +endobj +17 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 39 0 R /Fit ] /Rect [ 59.68504 580.8499 155.9775 590.8499 ] /Subtype /Link /Type /Annot +>> +endobj +18 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 570.2874 100.813 581.2874 ] /Subtype /Link /Type /Annot +>> +endobj +19 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 559.8499 120.135 569.8499 ] /Subtype /Link /Type /Annot +>> +endobj +20 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 549.2874 125.709 560.2874 ] /Subtype /Link /Type /Annot +>> +endobj +21 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 538.8499 120.135 548.8499 ] /Subtype /Link /Type /Annot +>> +endobj +22 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 528.2874 87.92504 539.2874 ] /Subtype /Link /Type /Annot +>> +endobj +23 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 517.8499 120.135 527.8499 ] /Subtype /Link /Type /Annot +>> +endobj +24 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 507.2874 82.58104 518.2874 ] /Subtype /Link /Type /Annot +>> +endobj +25 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 45.68504 496.2874 92.82104 507.2874 ] /Subtype /Link /Type /Annot +>> +endobj +26 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 485.8499 82.20004 495.8499 ] /Subtype /Link /Type /Annot +>> +endobj +27 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 40 0 R /Fit ] /Rect [ 59.68504 475.8499 119.2875 485.8499 ] /Subtype /Link /Type /Annot +>> +endobj +28 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 41 0 R /Fit ] /Rect [ 45.68504 465.2874 111.485 476.2874 ] /Subtype /Link /Type /Annot +>> +endobj +29 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 41 0 R /Fit ] /Rect [ 45.68504 453.6874 129.725 463.2874 ] /Subtype /Link /Type /Annot +>> +endobj +30 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 41 0 R /Fit ] /Rect [ 45.68504 443.2874 115.469 454.2874 ] /Subtype /Link /Type /Annot +>> +endobj +31 0 obj +<< +/Border [ 0 0 0 ] /Contents () /Dest [ 41 0 R /Fit ] /Rect [ 45.68504 432.2874 106.149 443.2874 ] /Subtype /Link /Type /Annot +>> +endobj +32 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +33 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 ] /Contents 74 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 73 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 + /Trans << + +>> /Type /Page +>> +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 75 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 73 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +39 0 obj +<< +/Contents 76 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 73 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +40 0 obj +<< +/Contents 77 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 73 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +41 0 obj +<< +/Contents 78 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 73 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +42 0 obj +<< +/Outlines 44 0 R /PageMode /UseNone /Pages 73 0 R /Type /Catalog +>> +endobj +43 0 obj +<< +/Author (Android Cast project) /CreationDate (D:20260610172108+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260610172108+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Android Cast \204 exposed services catalog) /Trapped /False +>> +endobj +44 0 obj +<< +/Count 35 /First 45 0 R /Last 72 0 R /Type /Outlines +>> +endobj +45 0 obj +<< +/Dest [ 38 0 R /Fit ] /Next 46 0 R /Parent 44 0 R /Title (Validation snapshot) +>> +endobj +46 0 obj +<< +/Dest [ 38 0 R /Fit ] /Next 47 0 R /Parent 44 0 R /Prev 45 0 R /Title (Hub landing) +>> +endobj +47 0 obj +<< +/Dest [ 38 0 R /Fit ] /Next 48 0 R /Parent 44 0 R /Prev 46 0 R /Title (Console home) +>> +endobj +48 0 obj +<< +/Count 3 /Dest [ 38 0 R /Fit ] /First 49 0 R /Last 51 0 R /Next 52 0 R /Parent 44 0 R + /Prev 47 0 R /Title (Issues \(crash reports\)) +>> +endobj +49 0 obj +<< +/Dest [ 38 0 R /Fit ] /Next 50 0 R /Parent 48 0 R /Title (Related endpoints) +>> +endobj +50 0 obj +<< +/Dest [ 38 0 R /Fit ] /Next 51 0 R /Parent 48 0 R /Prev 49 0 R /Title (Upload via curl) +>> +endobj +51 0 obj +<< +/Dest [ 39 0 R /Fit ] /Parent 48 0 R /Prev 50 0 R /Title (Bulk / synthetic issues) +>> +endobj +52 0 obj +<< +/Count 3 /Dest [ 39 0 R /Fit ] /First 53 0 R /Last 55 0 R /Next 56 0 R /Parent 44 0 R + /Prev 48 0 R /Title (Tickets) +>> +endobj +53 0 obj +<< +/Dest [ 39 0 R /Fit ] /Next 54 0 R /Parent 52 0 R /Title (Related endpoints) +>> +endobj +54 0 obj +<< +/Dest [ 39 0 R /Fit ] /Next 55 0 R /Parent 52 0 R /Prev 53 0 R /Title (Create via UI) +>> +endobj +55 0 obj +<< +/Dest [ 39 0 R /Fit ] /Parent 52 0 R /Prev 54 0 R /Title (Create via curl \(session cookie\)) +>> +endobj +56 0 obj +<< +/Count 2 /Dest [ 39 0 R /Fit ] /First 57 0 R /Last 58 0 R /Next 59 0 R /Parent 44 0 R + /Prev 52 0 R /Title (Graphs) +>> +endobj +57 0 obj +<< +/Dest [ 39 0 R /Fit ] /Next 58 0 R /Parent 56 0 R /Title (Related endpoints) +>> +endobj +58 0 obj +<< +/Dest [ 39 0 R /Fit ] /Parent 56 0 R /Prev 57 0 R /Title (Fake session data \(dev / BE\)) +>> +endobj +59 0 obj +<< +/Count 1 /Dest [ 40 0 R /Fit ] /First 60 0 R /Last 60 0 R /Next 61 0 R /Parent 44 0 R + /Prev 56 0 R /Title (Remote access) +>> +endobj +60 0 obj +<< +/Dest [ 40 0 R /Fit ] /Parent 59 0 R /Title (Related endpoints) +>> +endobj +61 0 obj +<< +/Count 1 /Dest [ 40 0 R /Fit ] /First 62 0 R /Last 62 0 R /Next 63 0 R /Parent 44 0 R + /Prev 59 0 R /Title (RBAC / access control) +>> +endobj +62 0 obj +<< +/Dest [ 40 0 R /Fit ] /Parent 61 0 R /Title (Related endpoints) +>> +endobj +63 0 obj +<< +/Count 1 /Dest [ 40 0 R /Fit ] /First 64 0 R /Last 64 0 R /Next 65 0 R /Parent 44 0 R + /Prev 61 0 R /Title (APK builder) +>> +endobj +64 0 obj +<< +/Dest [ 40 0 R /Fit ] /Parent 63 0 R /Title (Related endpoints) +>> +endobj +65 0 obj +<< +/Dest [ 40 0 R /Fit ] /Next 66 0 R /Parent 44 0 R /Prev 63 0 R /Title (Git \(Gitea\)) +>> +endobj +66 0 obj +<< +/Count 2 /Dest [ 40 0 R /Fit ] /First 67 0 R /Last 68 0 R /Next 69 0 R /Parent 44 0 R + /Prev 65 0 R /Title (OTA updates) +>> +endobj +67 0 obj +<< +/Dest [ 40 0 R /Fit ] /Next 68 0 R /Parent 66 0 R /Title (Layout) +>> +endobj +68 0 obj +<< +/Dest [ 40 0 R /Fit ] /Parent 66 0 R /Prev 67 0 R /Title (Publish \(operator\)) +>> +endobj +69 0 obj +<< +/Dest [ 41 0 R /Fit ] /Next 70 0 R /Parent 44 0 R /Prev 66 0 R /Title (Heartbeat \(device\)) +>> +endobj +70 0 obj +<< +/Dest [ 41 0 R /Fit ] /Next 71 0 R /Parent 44 0 R /Prev 69 0 R /Title (Auth & account security) +>> +endobj +71 0 obj +<< +/Dest [ 41 0 R /Fit ] /Next 72 0 R /Parent 44 0 R /Prev 70 0 R /Title (Non-HTTP services) +>> +endobj +72 0 obj +<< +/Dest [ 41 0 R /Fit ] /Parent 44 0 R /Prev 71 0 R /Title (Validation scripts) +>> +endobj +73 0 obj +<< +/Count 5 /Kids [ 33 0 R 38 0 R 39 0 R 40 0 R 41 0 R ] /Type /Pages +>> +endobj +74 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1005 +>> +stream +Gau1.;,>q#&BE]('_aGL8VG`QPB:_[qpR"T/X6-ACF-D7Pcn==)7P$.o>:$ct(GnFZT,F;1r:2X1"aR'?JH(^AG*\CT.dUrcr1+P]-C.psn;BU8@"cT_u'bldBWPAoP@IIMTFo6DWI>sb6".r&i>!&5G$f8o?JbBi&:YC=;<=`Q#m%Ar9I=X`U/+%F!:&)0*D]ImcDol&Ys8:B_t<,E?G^/-?=+Ip+ki6M`P):N&8c3B!bAcZ'l8m=++Fb!#lLEJ=u7g0R1Fij8$VkW/d$V&WWUSLiJ-&Gu^o;5EuKPS]'BQ`D:_#+7mgM.&l?uBbl&69PhqsZRq`1aAnaCVUj:@:X,C\gd+l5*m'"XVYQiKNEK_BB7;gKroi%ARW&&!+[?h4l:%@4`OnJc,qlb7!-0u$_j6g#DdIW\H?GpT^)S./FB1q-(=\XrT^^BN54Ni1f]^W"%2$%P0V`;`k@^O2gL1*P4PCDZKCgMg+r>b@ur(ZIeH7YfWMI6X3m?5u\:B>LTQ&(h`/IL2:@QNh>l?@o9;jPp8a=5l\_"*T5uB%j+$Gjq:>]&jas\l^@tJgY>gS_pKqgsu~>endstream +endobj +75 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 3476 +>> +stream +Gb!#_=``=W&q9SYkaIs>;%fAXI5mMGb2L:&E=u'Cm(U!\"];MBQm+Nnj^TD+[r6nc8;[YNA27AL,Coh6NNX:\mRCC])1RnCIs2"aC(A_%$7ICJDa;\r*Te/W2F%'08BFBeL1K"mXFk*=P+als^5(P>U/KFf,Qm0J:h9QNJJ9uG3'kZJ'us;I53!AOr^hKU8J8Ts"]\UJ'A5KFA0OQI>L).7`D/r+SUj\ZlqsCjMF@qg(Q3)3+)IQSOE"4#o398_+RX$sTnh/BP?#)&c&7A'1)HP%CSPcLpEMi3+"J0%H(`75a$ld3,<*o+Yp0T43cA[#L2GJiY_QMmmZp+D#qJ!+f:SUhj6?&@V5R9;k<3FPGamT\GK6NG^g[6U8#Q=40ePQ'uEXU@)/5%?4Gl,_a)ne=.t48\s?g-El%N^$Scb`@rUkP+=rgJ,,$b#SIcRB_S[ZA4Ju?efb^n9P/p)Pm$!^4b0N?^oU.Nf(88q!%oG+HBg*A`)@:"j3V#AKI,5I*.F`%1)7TBoI?_Vc0F(MDUE2,L%G-<'$mC$\i'E44H7;J2)g\_jbFTd^f"j+)O=I)Ch2*6Xg.qUUK8V!T-gVXH"#6+1sFO[r,Ca$I?5\iOc[>98[N(28.1R:7s^HK(BJh=mscS/[PJ]^='O!XdWcUB+k$6I00"@\Qq)5s2kk,VUUAe@5#+_'Kug]9Z6s+"8+ZYlbcRk("maY8-+I_'"OnLt9o+2qd(5F=@7$je'N:fE5[@P<00d?.S=O-ZCrZ&.9$%LI_.3qf(DWo/?hjnka@7N+uV)il^,=C")SOdhGNZ[YTa2Y'5@*#>($)U)02e/L[(:Il+DdqgK_>"?g@^,qepbAr\s+]48]UqA?Pq@K@$`8L48C_#oHT0Dt+irH!dj5%c*QP&'k"oXS#a;l0OMtH?.2su\l2LaHLkhd4eV(o^q:Und\"e;1%I*5u+,f;G*[/^0MrBV"u+[e.hi^!U(idh[GXb(eY^L*$I>IAQ?J`c.YG=RespLTq/Zca-RV@U#?9A^jZ:Ec_ht.!Wlqq*)`r^0fut.jpQpl6s,X;cUg^;:7RDE67b"XVaik!3Wc=?)j?#S#;&218a\/8G\C[o`'bMaQ&5]rkS'^oIqibk"&P:dGPiIc->qUD=g9&%^pKj.bO`kmsmG`TYBt6O87n%S^VTC_+=S%NPa-Jl6c4er*01K0s9YnZ3q"6a[aXP_[g:m\bFn8:NS@ETSM/!Rg7USGhtL)od@I>fDd%@?^4IKU@r_B9/Sdl%`Gb%16^>"2DRI%nTTh_a)f!',#uAh&F76b7]eQ'3qIQ1_8g7ts/"#3L-:'Z5t2Ge04i:4`>jBY#9_!LnB(lJ)9(7@H(l4IRR@KY,^Vo?1R,KW*M^UUll]Kk`a9,.8$b`dT#;/[71&:#F8,N8/3j_7e3D]j9,_'s%<&_-n_eb1_TZ=<>K/OA5Ot:XMH@Ld+D)11/K>KkK^I;n+bSpkdXeMU:,Qd+Wc;-#>0bV=NTT&f_5LrbP0rMfBi]_8Xp@@Z7M,DNKI,^`0'I2V-qh'GN;Ek>0MDV>4O(_Kfeg8bA;uABB$UWI\!*/*/$KLfoqja3MVm,8q@R%?Z*XFLkL'/CS7V?X$+fYO\Gs+YJ7@2qceC$<0e-jr']hbq.PZm@QgD)$mlo0HnG)a8VoPDUBqt1oSca#thX5b[$kf57I1kLDPr)5'p@_Kj/dI>7KPVrjB*pD)AQqh2:Xq9I]7&*LY7s"S!aH[6"fd\g0TS@<"-=W6*]N&D5mA6'Gs$j^*,["4S1YjY&eA!,o7`o7a9jWd`TRlJa-6XRLi>KY@TPF`9<*.FY[,'1n-??64mMX%9#+gp$GE);R_lmBjQ^hS"a4Wr-!I&PY-mb8^r:MP/,(UX!4mnB(co#o(OpS8RNcd5??>$hct"0jKH,jqfdP998>##/dRd]o<6:BLKD=SsQYp/$[j57&oKrmkDW<-Uc%A\PVGZF%GQ.LU8O*@;#nPi`pBsE'5,B;0#2L=c.28]SfZXhn#2qM/l][aXg]>!TE*LGB$Wd;tb4SF4n#C5edX`6SrLl"W8^:Kr/jb&9k1nO>na-,\Hk9jsR5@"G:"*2HOrN''Ck30r_Ymk48(2Xe4-6eUg4F#%:D^$MMTiE*[TCJ_l7(c2W(Ca>FdoK[/]$a5F*rROO;=`ZE:alo)B[DF`FdPS:PNM]!66:M3[/17I9F?b<=oG`P@KJi$\opY3r,=N6K8e.@#`I_nF:iiMCqJ[3$dR,.4%:T>KLG?iBd+80'#P#Ai('+r7N<,j?159*IOs_BaE8%3/a-$5H:VNH**gGj',NS9GMf%g">t5\t7#"9md\8U;&MUjZ&E>WfGR"LM=@$USH0hWUG_aOoR.1&@emLGI.6g)=sp,%7r78]d,>s13ut5YN%@,EFendstream +endobj +76 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2874 +>> +stream +Gb!;fHZ.Xu')t1g@c6WZdRC8Cos1n0pYgrcF:,Bp:/^RPO>`-M0RHQ7'!@Uo9VRF@WTDZp5ZZf])'.#(jb?oiLAOBek=ZpRq9?bPBm6*%D8QUEgl?UD%m'1P[r4H2?:7LU/'"$a-X=_WVn-5Je"_]1`:io1SpY^,Y[KZV+9mD0&hD$eP.HaO)lPO7%I0KL4u5Ah8G@QY,7/Uiu+"!(C&mj%FpnY2[4-)%gptp\`-"!8NhrD[fcZ4%g.L+>S1i/GLMMmJK)du/9Yi@,DJ_/7n^LqIRg6r@2*FhMMSEo-WV*,]KM1U$dU\RFgmW+^(u4c'h:9K?L^J.)S>K3Ul;c1kTM(ss5FBbE2H+\h0BMZ-Zj^N@]2ppWEj2[XgE0L4$T[8,tF2qUbq4&$S2m/b,5#HR[ST$qhU3[(OU-cT027m?A:<+:X9('aY*(6?[4lK8$qRiL$32G_eh+0-6.A$/M5qg0V]An%Racq>S?F"'SN3/Of+S*#h?,M(2M5.`mJV1gge(;&r,M(e(A$Q>dQIQiIMnf.QAgdT?01l1Qb5RCL^24S`u*r-"i[*(m;X"R\]P[;%fE!BMg[OAOF^j]\m`=j(Bbf>juBs$aKUGP?I^bkC82anYs__OJ3L:64Q5]J>11j*esLZ5FeL11W-U(\LB\cE+il&SA5r*-;.SYe8:7aQhbhpkqs=a:D2\V[UXRb$i"XD9mH%gI@l[dBW-bsnOQcm=ZTQ?9G9NT?+sP=O;oJH-+"0UjjPP,DXjKjg9prPm92jL)RVN`k*:SRinkB8%S=&gml\CsCg0maGb$T!9-0UR2ssVgAuh==/%+=cIUtIBg&d?sqY#/BoZfG@r-,!4pXm,+;Md=-3n-_6lD([Y#dC:p2Xe`:F]W/I>[ne,J:s'R$#W.(*c223TF+,j]H:gBcttYoEaR2ds'E78bIhLQlCtimF=j)q6$B"X<7.fP?%lkg'A>DSi<'AJoiA^`oW.T!3=7rB':pYiGtJ.WUq/W!m73iuXj%[3rkfWkjf;,#VV3#=s'eU@N8;8qUd];7m>Ha*A^[m(q/iX_=sf_8*'Png'@GCd11rPCA-FY\i'u!LEC@8=3Tl]oJd9Le8pBV>=44V"E@OL[Ls6e!.Fb!CbD`g&d)Xq+R,IWQ+H1G3D?"X'$1gc)%ZAbUu+Xc@Fd#7UXHs?4oTYRTF?Wd=ut15IX-.1=E%?mnV_OT.>@k.AlBW^Ruau"_TJ#W6D=ueS\eeqKuJL`$A[^Wf(dcE*0sP"-80el$g[Q0%$"@%=JJqEE3u&f2^C*/!g!ljAAqm,c.k11%-:m!0-S6gDo@#`=O.q`R=d0TbJ@rmF'C091eblsLIQ&hd+#ID60DiWOhH\0k3qKFqbkG\;=l2Anh&McXPFD]a]J2lMf.P%7s5,sNO**\jPps_h"$cE\[7m,pZ!_<(M<W"K&a$\VK!#>qe7O3,^WE1RB^0Z,r2GL[6edlA4UH*S`'Sn_)Gc#m#.]'L;@eeC7^j7)f,da^0Dfo#Q0-qW1+>(V&,bk?9:uIC_TSb=bbMH7s5W&,ML\=)jAVpY^#4=V;p#-e)AqiLaW&q-B50q=&?Gf8B6\+6&1jW#iQN_dsB9Y^4MYiXU.lg/^c:7kaPaJG"qL_OJJ$;X[V(P5&jNO/>LUa?UHc,4>OSi@e%tp5]1P8'gr!61/Ra31pp#<$+eG3;UHLCjW2V^:c#I7kkB%X,CE#Q4VZR=RY!/^==L2>XCX^&\VKEaG)UkF$d??UVELr^Y!gYZZ-!erT/Wk(J!p,)UB_4@#1p6=u2*:`5U+T'2t0Xdi>fa@X"n)E]Xkj'&Vg/i@^Tf2"AL1Ok:W4;EJ"+n4t%N<:"qh0"M>'HMJ5!$(^`&LAM,jU5QVBWei=eoV'$W0O]Y&_;K4V`:&"Wb`j)K.\qs4`2%?@-/(l]JY2/gs&lUV3h%F3QL4*?E-m*1l/RuYr,Re%4cB;#.c\cCP%;1LO.)Tp@`2r6uJnja.-]7epp^EZKaP?#tXk6KVu3n<.1#!FAM[fAFPhC\b*eL-gV^d[FP-(/^T262(oc1=(Z4?SK3>>VD*Vt"@_5pc36]^%^eX2E3=384SRB89UojXmR;04R,;f?m.n-cpE/98`sQ=**#:Jlc7`dZ)tI]XCMHW_W0fWT!KMHNh>3PhelO20l]=@j\.6q+GVOAY6d?2j[[NE23j%5OWGR+/m5ja7OpM#EG.-O^J$2"@P(JSJLs\DH2Ig15`"(5ZA`-a#q1#k##R6p^P]?aaQq5K8mQm_`("d>.KQendstream +endobj +77 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2787 +>> +stream +Gb!#^lYkfA'*"Q<@_NhD('u9a0.T;lE-ARK2V3(](W?CU%,#6/D.H57kWJ9Ps8*6O=dEX@VX/2I#SMpTBP'4Wre?X<3rTuLhrtd3Ad$gX!t5O37kcK'_<;AH7q[=A)$QjJ';oL/(:^M?FORSc3Ln(oNKTW:^h=%=b(mBc1`JYkZT@S9SDUYa//\02.8/,3@.Xmt5>!B>i_Pa`LD%4%&l(tpL+Y]3@D-M\^U)$^-:4jcRlcCZ&4q2EBmu5@jACDT,r)%8Olf.i*XB$NT8IX*j)8``Zmkau"\Z7IcN/C'k*bYt>^(EP^VB$J^OecRlC(;s+r\YfC+;@DTWi7%0n,e,?.KfV$+*=`=:75e;j&\pGQ(QL;I7O:%XUi_NCHp5@@m+H*Ced6^6baDZ,BA2Q_gU]JpEcsW7OaHON)!'P7$\\ZOQR9m5Bg+Ep^*eCEuhnqgB[HoDhuIT8O$I"\Hi:=/_\VQ%4R]eY#'lODjk'kR5NEbI>q"sH;oVcQY#1Pm+5FpaSo"PI;$b'U6EbjADgea+kUI:kCiJ^=9]0SIJNH20!_<'>JS6M@'$7?tPRK#6E_"::E_oN#QBh>7:7$TBqU`13fl:if\qV_`fM#]-KGF]GOHi[5VN[K*\V9Q:4sn]^_)Z$X1WI+K,>gBQ#^(.a1bi,O!D+ksucD\Vt*F@'YVCk2H:15Jp+);E_HFT3DaK!"c*dLL@+VK]7CO>VKRj?TVnFSkhSlkF.%HZ]\O3NI?2h0H&D$JkNLg*pDHFi9-/DF^-6!ZqM^'OH?.(Stm0XdP<=uW;:1-Yq9]0S+e1MUs6g\p*)/9u:X\dd'*F_""fa8HF&-"-c3eO,<1tW8SiO?tNc13:IRJG6(1P)X8tF+_J'N^B3n8H>3e%RWcXi/IC'Q<;Q]3+6MPZ0399@0V@D*R9V;$WSl!-@We4$i9R&:VuThY)GC79"ItIMnMr9SVu%7-_emSQW`%UF?R:6\NBZV_t+[7ogQp*%J@h)oC_Y[*$JE]%6Cb3T4ju^[@t9Fgg")a8(q&N],$;2mf(Mk)OUJhqHMI\j:.K=^.@\l5;uC#eILP$_LpAJ^l$%i$ApB.*bb!kGAe0f=]!qf<5Q^?_ei!A.3@)qf;F>r8Dg>`i2sRC]P:o`^Z$-%L+`Ne45P!j2XrMk6K7&tD8p)b]6m(eZ[7=K=h"eLU(Qf^8\O6U-otR\p2kT*Got+:t23iZldt@Fq^P@\2Nsf\&4+;2SSF0)R,,Tjmp`lWPieXa!B0Kg,k]q3LI%T/N^l""Qf?gMXT$-:AEiGCg3f+qm1PitW*j/&C5rG%t*_9/e@\m:O!SX#f">;]ZdQdq7(H7n\(*>J+]8=kF"RVK.X_DDJ^X?ps%0YiE*Z+o[[0iW;>`6^h@QeFh]`b;2dU$6C'3qrqE5W-%SeY:HG,d7mZh%%[:aT`3f%Jo%'s%_b[pa^+57P^R.LnF5EN4o@YN(BrY:4V[g_.mF`.42endstream +endobj +78 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2621 +>> +stream +Gb!l!lYkNI'*%C7@UPiFV[6_RLE=Fni4*sY,#&bU\_[Rb!_'4:-5;dm=*EAnpUB$2?Qj,]>h[fhJ:lk6b8JT@G@uB2.g'nk)!V(W*oPQgZmu4e!qQj486/R6RsIQ]E+:L;Z)W/tc;ubk$_J!To\D`mrMs%;eA-C:g(&5l\gmIsXfAd1MJ6NrhRsUhlh"our<7(QtMMhN,>jep0IebpE\>pB`iM:k::[>sO.abA\R$`%$b:FO"1M;N3,G^\c0s4:,)i>--W6>2VkI3[n_HYVBE7p5+*#o3sZ3d0/^J[O\)R7_;4.-%.$Z?DHc5O#QOb_2>A`iQgVuVB_o`9o%Co=?XXm-](s1$1@$<-,aoO3)s50_rhT9+M_]+aQ3(-SO2[$6nY(G"Nila"n%>odbetT1#9'YmrD1T1H0JQ$J-1(H^$rcR,Nic2aT)C"7Hd+THlIhr!Uqe;r5:()OZK'4IO\SbSXr<"\357cRVe:c^W&d6-="Y6SGr%K6dhU3@Tqf)W0Z,-*urMkN_ZZn#0SG5"t#H'""pmo!A4q,#XqA)q"t!'F4)(+A.0Dna/Z(;oeA1<'3'&4kbBl2,F_@X6d0k4YMg/e"\d@(0lsh2F'=AkE&sN0aSgA^[NW*UY/gk(?J(Qs2P[;>/]1K@n&B=_$_lD;((?4<_5T5FMSH#m5N+He2gr4&Rd"ga+qr%BW.t*g;ZuAoYlIfj5XUsI1gEi$'4*.OiX;WHJND'qWo<2f^"B:io5pD0#Yim4M9F\G8jD*[^ep$38BhKMDShg\@X3""Rj-0WnBHL8!:Q*8'P/d=s:Kn,qMJ7YB?^emD5jBj(gW$(#s6GS'S!Af"TJ>8&gaA\!r\`43jW8V;&pfm@7U(>F:_dr4jNH@R%_9YL1D<0CD"2E%.SU!nT`V@U'/lb4RHtYI3`),4@26`PNLRjI09,0MD.$f1XN?2,AJ"!d4fak"X;L-6;]I6E\V?d\H0^aq(^H7=17.1916>N)DB#=0W7(S^FmMBdsr(p-e@OW4\O.]^rIgUph;KE'&JJes"51>FeqEDf9H<\sE%jV^AmdM"Jc:C0?\9o!JUKPJt4"AI]N)St:W-kJcDC5:1I]X!VSFY:PYE$&ji\=pVr+?%:S_G0t,;aZ]N#(b9O-#M$Z7R5ajG&l,.lYn)rjCG>T69`"I3R[Bbk6N;HsBT5*)?-%%336#j"MYiMF.(:iadQl%iAHk.Fq5%*fi/cWtG`m-d;-tk1-g-Ob+g.o*:cdIJir0`a1nY_)"'B`f5=^eO6K1riWW];7L_a)<8:H.dp=\k-sb[/L]Cnm`,#;[6lWq<^6_T0LVAn(B>DEkl'Oo,05ndO=l%.ho4,&ppJ>kCs!r/!EjZ/0(SJ1aoF=nE/pq^emVq9Dc(LUA7X,`m[sPjB5O>-c)+q_0,2A_D,$Ria3mOiZQ\r^j)gq0+"Y=3+?+'OUrm(Eh3KECT6MUW&YRNHWFlNI30^UpSX!H(?F8oN&I"Zt*"&o[&6WL3K%/B5'kjhlg:W_qiW$2"2(n^*rE;T6eQ+im5d?Utp8`U[dXIU1Y[)6iYC",hYa.rWpRuYeEBu4k<8Wh1sR&G:UFSL'-`,oIpVK94./nkM>J]X1f%;R)pYP_16]`Oob*QdMQg9?goXRqSB?R9WM)r,s);biaaX;W,@&%~>endstream +endobj +xref +0 79 +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 +0000004382 00000 n +0000004530 00000 n +0000004646 00000 n +0000005060 00000 n +0000005166 00000 n +0000005286 00000 n +0000005370 00000 n +0000005448 00000 n +0000005654 00000 n +0000005860 00000 n +0000006066 00000 n +0000006272 00000 n +0000006359 00000 n +0000006676 00000 n +0000006751 00000 n +0000006852 00000 n +0000006958 00000 n +0000007065 00000 n +0000007223 00000 n +0000007322 00000 n +0000007432 00000 n +0000007537 00000 n +0000007678 00000 n +0000007777 00000 n +0000007885 00000 n +0000008001 00000 n +0000008141 00000 n +0000008240 00000 n +0000008352 00000 n +0000008499 00000 n +0000008585 00000 n +0000008740 00000 n +0000008826 00000 n +0000008971 00000 n +0000009057 00000 n +0000009165 00000 n +0000009310 00000 n +0000009398 00000 n +0000009500 00000 n +0000009615 00000 n +0000009733 00000 n +0000009845 00000 n +0000009945 00000 n +0000010034 00000 n +0000011131 00000 n +0000014699 00000 n +0000017665 00000 n +0000020544 00000 n +trailer +<< +/ID +[<58c8102b70531cef7ffe0ad93695a99d><58c8102b70531cef7ffe0ad93695a99d>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 43 0 R +/Root 42 0 R +/Size 79 +>> +startxref +23257 +%%EOF diff --git a/docs/README.md b/docs/README.md index 98380b0..29d4789 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,6 +47,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu | [20260608_STREAM_DUMP_DEBUG.md](20260608_STREAM_DUMP_DEBUG.md) | FR 0.1 stream dump spec. | | [OPEN_TASKS_GRAPH.md](OPEN_TASKS_GRAPH.md) | Open tasks dependency graph (Mermaid). | | [20260608_BE_SERVICES_and_infra.md](20260608_BE_SERVICES_and_infra.md) | BE service map, validation status, SFU preview (gray). | +| [20260610_SERVICES.md](20260610_SERVICES.md) | Exposed services catalog — web UI, APIs, curl, validation (2026-06-10). | | [USB_HDMI_CAST.md](USB_HDMI_CAST.md) | Roadmap D/E: HDMI awareness, USB-tether transport. | --- diff --git a/docs/_pdf_build/build_all_docs_pdf.py b/docs/_pdf_build/build_all_docs_pdf.py index f80a11f..31565ab 100644 --- a/docs/_pdf_build/build_all_docs_pdf.py +++ b/docs/_pdf_build/build_all_docs_pdf.py @@ -22,6 +22,7 @@ SUBTITLE = { "REMOTE_ACCESS_IMPL.md": "WireGuard v1 — implementation and deploy", "REMOTE_ACCESS_VALIDATION.md": "Linux CLI validation — device sim + E2E", "INFRA.md": "Production topology — FE, BE, ports", + "20260610_SERVICES.md": "Exposed services catalog — URLs, APIs, validation — 2026-06-10", } diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index 0c91591..7f769ef 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -523,6 +523,13 @@ body { --footer-h: 40px; } align-items: center; justify-content: center; min-height: 100vh; + min-height: 100dvh; + padding: 3rem 16px calc(var(--footer-h, 40px) + 1.5rem); + box-sizing: border-box; +} +.login-page .login-card { + position: relative; + z-index: 6; } .login-card { background: var(--surface); diff --git a/examples/crash_reporter/backend/public/index.php b/examples/crash_reporter/backend/public/index.php index 366086e..d7bb107 100644 --- a/examples/crash_reporter/backend/public/index.php +++ b/examples/crash_reporter/backend/public/index.php @@ -162,6 +162,10 @@ if ($route === '/login' && $_SERVER['REQUEST_METHOD'] === 'POST') { } if ($route === '/login') { + if (Auth::pending2faUserId() > 0) { + header('Location: ' . $base . '/two-factor'); + exit; + } require __DIR__ . '/../views/login.php'; exit; } diff --git a/examples/crash_reporter/backend/src/Auth.php b/examples/crash_reporter/backend/src/Auth.php index 4e5cfc1..9de0440 100644 --- a/examples/crash_reporter/backend/src/Auth.php +++ b/examples/crash_reporter/backend/src/Auth.php @@ -101,14 +101,15 @@ final class Auth { public static function completeTotpLogin(string $code): bool { $uid = (int) ($_SESSION['pending_2fa_user_id'] ?? 0); $exp = (int) ($_SESSION['pending_2fa_exp'] ?? 0); - unset($_SESSION['pending_2fa_user_id'], $_SESSION['pending_2fa_exp']); if ($uid <= 0 || $exp < time()) { + self::clearPending2fa(); return false; } $secret = AuthFactors::getTotpSecret($uid); if ($secret === null || !AuthTotp::verify($secret, $code)) { return false; } + self::clearPending2fa(); $stmt = Database::pdo()->prepare('SELECT * FROM users WHERE id = ? LIMIT 1'); $stmt->execute([$uid]); $row = $stmt->fetch(); diff --git a/examples/crash_reporter/backend/views/two_factor_challenge.php b/examples/crash_reporter/backend/views/two_factor_challenge.php index 5a9dd0a..84ccb41 100644 --- a/examples/crash_reporter/backend/views/two_factor_challenge.php +++ b/examples/crash_reporter/backend/views/two_factor_challenge.php @@ -17,6 +17,15 @@ $bp = Auth::basePath(); +