1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:38:53 +03:00

be sync - preparing for sfu integration, login hopefully fixed

This commit is contained in:
Anton Afanasyeu
2026-06-08 22:09:02 +02:00
parent 1ddb51312c
commit 1e08efb9a4
41 changed files with 1360 additions and 401 deletions

View File

@@ -80,6 +80,12 @@ return [
'max_attempts' => 8,
'lockout_window_minutes' => 15,
],
// SFU / WebRTC relay (post-alpha) — hidden until owner spec; see examples/sfu/README.md
'sfu' => [
'enabled' => false,
'signaling_url' => '', // e.g. wss://apps.f0xx.org/sfu/signal
'janus_url' => '', // lab only — port 8089 is a separate stack (INFRA.md)
],
'mail' => [
'transport' => 'smtp', // smtp | sendmail
// Alpha: use apex if apps.f0xx.org has CNAME (no MX on subdomain) — see tmp/20260608_help_request.md

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../src/bootstrap.php';
header('Content-Type: application/json; charset=utf-8');
$sfu = cfg('sfu', []);
$enabled = (bool) ($sfu['enabled'] ?? false);
echo json_encode([
'ok' => true,
'sfu' => [
'enabled' => $enabled,
'status' => $enabled ? 'preview' : 'disabled',
'signaling_url' => $enabled ? (string) ($sfu['signaling_url'] ?? '') : '',
'janus_url' => $enabled ? (string) ($sfu['janus_url'] ?? '') : '',
],
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

View File

@@ -40,6 +40,17 @@ a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell { display: flex; min-height: calc(100vh - 40px); flex: 1 0 auto; }
.shell.single .main-pane { max-width: 1100px; margin: 0 auto; padding: 24px; }
.shell.shell--graphs-full .main-pane {
max-width: none;
width: 100%;
padding: 20px 24px 32px;
}
.shell.shell--graphs-full .graph-detail-body {
max-width: none;
}
.shell.shell--graphs-full .graph-detail-table {
max-width: none;
}
.nav-pane {
width: 56px;
background: var(--surface);

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env php
<?php
/**
* File infra validation findings as tickets (tag 20260608-validation).
*
* Usage:
* php scripts/ingest_validation_findings_20260608.php [--url=BASE] [--dry-run]
*/
declare(strict_types=1);
$opts = getopt('', ['url::', 'dry-run']);
$base = rtrim($opts['url'] ?? 'https://apps.f0xx.org/app/androidcast_project/crashes', '/');
$dryRun = isset($opts['dry-run']);
$uploadUrl = $base . '/api/ticket_upload.php';
function val_tags(string $kind): array {
$base = [
['id' => 'ticket', 'label' => 'ticket', 'bg' => '#6366f1'],
['id' => '20260608-validation', 'label' => '20260608-validation', 'bg' => '#f59e0b'],
['id' => 'infra', 'label' => 'infra', 'bg' => '#0ea5e9'],
];
if ($kind === 'issue') {
$base[] = ['id' => 'issue', 'label' => 'issue', 'bg' => '#94a3b8'];
}
return $base;
}
$tasks = [
[
'key' => 'SVC-OTA',
'kind' => 'ticket',
'text' => 'ISSUE SVC-OTA: OTA channel HTTP 400 on apps.f0xx.org — wire /v0/ota/ on apps vhost or align device ota.base_url. See docs/20260608_BE_SERVICES_and_infra.md',
],
[
'key' => 'SVC-URL',
'kind' => 'ticket',
'text' => 'ISSUE SVC-URL: Legacy f0xx.org crash upload URL 404 — production base should be apps.f0xx.org. Update settings.json / dev defaults.',
],
[
'key' => 'SVC-DIAG',
'kind' => 'issue',
'text' => 'ISSUE SVC-DIAG: diag.php returns 403 at public edge — acceptable; devices use upload.php + heartbeat.php. Documented in BE services map.',
],
[
'key' => 'SFU-SPEC',
'kind' => 'issue',
'text' => 'PLANNING SFU-SPEC: Janus/mediasoup relay post-alpha — branch feature/sfu-relay (hidden). Await owner spec. Gray section in docs/20260608_BE_SERVICES_and_infra.md',
],
[
'key' => 'graphs-5.1',
'kind' => 'ticket',
'status' => 'closed',
'text' => 'CLOSED 5.1: Graph brick drill-down — full overlay, axes, legend, hover implemented in graphs.js (2026-06-08).',
],
[
'key' => 'graphs-5.2',
'kind' => 'ticket',
'status' => 'closed',
'text' => 'CLOSED 5.2: Graph columns 1/2/4, clickable bricks, full-width shell--graphs-full CSS (2026-06-08).',
],
[
'key' => 'browser-js',
'kind' => 'ticket',
'status' => 'closed',
'text' => 'CLOSED: Browser SyntaxError on console JS — deployed assets pass node --check; validate_be_services.sh added.',
],
];
$device = ['manufacturer' => 'validation', 'model' => 'infra', 'sdk_int' => 34, 'release' => '14'];
$app = ['package' => 'com.foxx.androidcast', 'version_name' => 'validation', 'version_code' => 1];
$now = (int) round(microtime(true) * 1000);
foreach ($tasks as $task) {
$text = trim($task['text']);
$key = $task['key'];
$status = $task['status'] ?? 'open';
$tags = val_tags($task['kind']);
if ($status === 'closed') {
$tags[] = ['id' => 'closed', 'label' => 'closed', 'bg' => '#6b7280'];
} else {
$tags[] = ['id' => 'open', 'label' => 'open', 'bg' => '#22c55e'];
}
$payload = [
'schema_version' => 1,
'ingest_kind' => 'ticket',
'ticket_id' => 'validation-20260608-' . preg_replace('/[^a-z0-9.-]+/i', '-', $key),
'title' => mb_substr($text, 0, 256),
'brief' => mb_substr($text, 0, 512),
'body' => $text,
'opened_at_epoch_ms' => $now,
'rating' => $status === 'closed' ? 1 : 3,
'owner' => 'admin',
'tags' => $tags,
'device' => $device,
'app' => $app,
];
if ($dryRun) {
echo "DRY {$key}\n";
continue;
}
$ch = curl_init($uploadUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_TIMEOUT => 30,
]);
$resp = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300) {
echo "OK {$key} HTTP {$code}\n";
} else {
echo "FAIL {$key} HTTP {$code} {$resp}\n";
}
}

View File

@@ -34,10 +34,16 @@ ra_ra_post() {
local device_id="$2"
local random="${3:-}"
local app_version="${4:-cli-sim}"
local caps='["wireguard","files_app_home"]'
local mode="${RA_TUNNEL_MODE:-wireguard}"
local caps
if [[ "$mode" == "rssh" ]]; then
caps='["rssh","shell"]'
else
caps='["wireguard","files_app_home"]'
fi
local body
body=$(printf '{"heartbeat":{"type":"ra","status":"%s","device_id":"%s","random":"%s","app_version":"%s","tunnel_mode":"wireguard","capabilities":%s}}' \
"$status" "$device_id" "$random" "$app_version" "$caps")
body=$(printf '{"heartbeat":{"type":"ra","status":"%s","device_id":"%s","random":"%s","app_version":"%s","tunnel_mode":"%s","capabilities":%s}}' \
"$status" "$device_id" "$random" "$app_version" "$mode" "$caps")
local url body_tmp code
url="$(ra_heartbeat_url)"
body_tmp="$(mktemp)"

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# RSSH control-plane smoke: whitelist → open session → connect payload with bastion fields.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
# shellcheck source=ra_lib.sh
source "$ROOT/scripts/ra_lib.sh"
BASE="$(ra_base)"
DEVICE_ID="${RA_DEVICE_ID:-rssh-smoke-$(date +%s)}"
COOKIE_JAR="$(mktemp)"
trap 'rm -f "$COOKIE_JAR"' EXIT
ra_admin_login "$COOKIE_JAR"
body=$(printf '{"heartbeat":{"type":"ra","status":"enable","device_id":"%s","random":"r0","tunnel_mode":"rssh","capabilities":["rssh","shell"]}}' "$DEVICE_ID")
code=$(curl -sS -o /tmp/rssh-hb.json -w '%{http_code}' -X POST -H 'Content-Type: application/json' -d "$body" "$(ra_heartbeat_url)")
[[ "$code" == "200" ]] || { echo "FAIL heartbeat enable HTTP $code"; cat /tmp/rssh-hb.json; exit 1; }
grep -q '"action":"wait"' /tmp/rssh-hb.json && echo OK wait
ra_admin_post "$COOKIE_JAR" whitelist "$(printf '{"device_id":"%s","whitelisted":true}' "$DEVICE_ID")" >/dev/null
ra_admin_post "$COOKIE_JAR" open_session "$(printf '{"device_id":"%s"}' "$DEVICE_ID")" >/dev/null
body=$(printf '{"heartbeat":{"type":"ra","status":"enable","device_id":"%s","random":"r1","tunnel_mode":"rssh","capabilities":["rssh","shell"]}}' "$DEVICE_ID")
curl -sf -X POST -H 'Content-Type: application/json' -d "$body" "$(ra_heartbeat_url)" > /tmp/rssh-connect.json
grep -q '"action":"connect"' /tmp/rssh-connect.json && echo OK connect
grep -q 'bastion_host' /tmp/rssh-connect.json && echo OK bastion fields
grep -q 'remote_port' /tmp/rssh-connect.json && echo OK remote_port
echo "All RSSH API checks passed."

View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# Reachability smoke for mobile-app + console services (public or CRASHES_BASE).
# Exit 0 when all required checks pass; prints FAIL lines for ticket triage.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PUBLIC="${PUBLIC_BASE:-https://apps.f0xx.org}"
CRASHES="${CRASHES_BASE:-${PUBLIC}/app/androidcast_project/crashes}"
GRAPH="${GRAPH_BASE:-${PUBLIC}/app/androidcast_project/graphs}"
HUB="${HUB_BASE:-${PUBLIC}/app/androidcast_project/}"
BUILD="${BUILD_BASE:-${PUBLIC}/app/androidcast_project/build/}"
OTA="${OTA_CHANNEL:-${PUBLIC}/v0/ota/channel/stable.json}"
fail=0
ok() { echo "OK $*"; }
bad() { echo "FAIL $*"; fail=1; }
note() { echo "WARN $*"; }
http_code() {
curl -sS -o /dev/null -w '%{http_code}' --max-time "${CURL_TIMEOUT:-15}" "$1" 2>/dev/null || echo "000"
}
js_syntax() {
local url="$1"
local tmp
tmp="$(mktemp --suffix=.js)"
if ! curl -sSLf --max-time "${CURL_TIMEOUT:-15}" "$url" -o "$tmp" 2>/dev/null; then
rm -f "$tmp"
echo "fetch"
return 1
fi
if node --check "$tmp" 2>/dev/null; then
rm -f "$tmp"
echo "ok"
return 0
fi
rm -f "$tmp"
echo "syntax"
return 1
}
echo "== Android Cast service reachability =="
echo "PUBLIC=$PUBLIC"
echo
# Hub
code="$(http_code "$HUB")"
[[ "$code" =~ ^(200|301|302)$ ]] && ok "hub $HUB HTTP $code" || bad "hub $HUB HTTP $code"
# Crashes console (login redirect OK)
code="$(http_code "$CRASHES/")"
[[ "$code" =~ ^(200|302)$ ]] && ok "crashes $CRASHES/ HTTP $code" || bad "crashes $CRASHES/ HTTP $code"
# Graphs
code="$(http_code "$GRAPH/")"
[[ "$code" =~ ^(200|302)$ ]] && ok "graphs $GRAPH/ HTTP $code" || bad "graphs $GRAPH/ HTTP $code"
# Builder
code="$(http_code "$BUILD")"
[[ "$code" =~ ^(200|302)$ ]] && ok "build $BUILD HTTP $code" || bad "build $BUILD HTTP $code"
# OTA channel (may 404 until wired — warn only)
code="$(http_code "$OTA")"
if [[ "$code" == "200" ]]; then
ok "ota $OTA HTTP $code"
elif [[ "$code" == "404" ]]; then
note "ota $OTA HTTP 404 — not wired on apps vhost yet"
else
bad "ota $OTA HTTP $code"
fi
# Device APIs (no session) — 403 on public edge is OK if upload/heartbeat work
code="$(http_code "$CRASHES/api/diag.php")"
if [[ "$code" == "200" ]]; then
ok "diag API HTTP $code"
elif [[ "$code" == "403" ]]; then
note "diag API HTTP 403 (edge block; use upload/heartbeat for device path)"
else
bad "diag API HTTP $code"
fi
code="$(http_code "$CRASHES/api/heartbeat.php")"
[[ "$code" =~ ^(200|400|405)$ ]] && ok "heartbeat API reachable HTTP $code" || bad "heartbeat API HTTP $code"
# JS assets
for pair in \
"crashes-app:${CRASHES}/assets/js/app.js" \
"graphs-js:${GRAPH}/assets/js/graphs.js" \
"tickets-js:${CRASHES}/assets/js/tickets.js" \
"i18n-js:${CRASHES}/assets/js/i18n.js"; do
name="${pair%%:*}"
url="${pair#*:}"
st="$(js_syntax "$url" || true)"
[[ "$st" == "ok" ]] && ok "js $name syntax" || bad "js $name ($st) $url"
done
# Authenticated API smoke (optional)
if [[ "${RUN_API_SMOKE:-1}" == "1" ]] && command -v bash >/dev/null 2>&1; then
if CRASHES_BASE="$CRASHES" bash "$ROOT/scripts/test_remote_access_api.sh" >/dev/null 2>&1; then
ok "remote_access API smoke"
else
bad "remote_access API smoke"
fi
if CRASHES_BASE="$CRASHES" bash "$ROOT/scripts/test_rbac_api.sh" >/dev/null 2>&1; then
ok "rbac API smoke"
else
bad "rbac API smoke"
fi
fi
echo
if [[ "$fail" -eq 0 ]]; then
echo "All required service checks passed."
else
echo "Some checks failed — file tickets via ingest or Issues dashboard."
fi
exit "$fail"

View File

@@ -3,20 +3,28 @@ declare(strict_types=1);
/** Migration 008 — email registration + 2FA tables (SQLite dev + MariaDB ALTER). */
final class AuthEmailSchema {
private static bool $ensured = false;
private function __construct() {
}
public static function ensure(PDO $pdo): void {
if (self::$ensured) {
return;
}
if (!Database::tableExists($pdo, 'users')) {
self::$ensured = true;
return;
}
self::ensureUserColumns($pdo);
self::ensureAuthTokens($pdo);
self::ensureAuthFactors($pdo);
self::ensureAuthAttempts($pdo);
self::$ensured = true;
}
private static function ensureUserColumns(PDO $pdo): void {
$existing = Database::columnNames($pdo, 'users');
$cols = [
'email_normalized' => Database::isMysql() ? 'VARCHAR(254) NULL' : 'TEXT NULL',
'email_verified_at' => Database::isMysql() ? 'TIMESTAMP NULL' : 'TEXT NULL',
@@ -26,14 +34,11 @@ final class AuthEmailSchema {
'recovery_email_normalized' => Database::isMysql() ? 'VARCHAR(254) NULL' : 'TEXT NULL',
];
foreach ($cols as $name => $ddl) {
try {
$pdo->exec('ALTER TABLE users ADD COLUMN ' . $name . ' ' . $ddl);
} catch (PDOException $e) {
$msg = strtolower($e->getMessage());
if (!str_contains($msg, 'duplicate') && !str_contains($msg, 'already exists')) {
throw $e;
}
if (in_array($name, $existing, true)) {
continue;
}
$pdo->exec('ALTER TABLE users ADD COLUMN ' . $name . ' ' . $ddl);
$existing[] = $name;
}
}

View File

@@ -51,7 +51,7 @@
<?= Rbac::can('remote_access_admin') ? ' data-can-ra-admin="1"' : '' ?>
<?= Rbac::isRoot() ? ' data-can-rbac-root="1"' : '' ?>>
<header class="top-menu" hidden aria-hidden="true"></header>
<div class="shell">
<div class="shell<?= ($view ?? '') === 'graphs' ? ' shell--graphs-full' : '' ?>">
<nav class="nav-pane" id="nav-pane" aria-label="Console navigation">
<button type="button" class="nav-handle" id="nav-handle" data-i18n-aria="nav.toggle" data-i18n-title="nav.toggle" aria-label="Toggle navigation" title="Navigation">
<span class="nav-icon nav-icon--menu" aria-hidden="true"></span>