1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 07:39:15 +03:00

docs, obfuscation

This commit is contained in:
Anton Afanasyeu
2026-06-19 22:41:27 +02:00
parent 55cdcb49bf
commit b9f94fe005
57 changed files with 3229 additions and 105 deletions

View File

@@ -19,6 +19,7 @@ import com.foxx.androidcast.commercial.CommercialFeatures;
import com.foxx.androidcast.crash.CrashReporter; import com.foxx.androidcast.crash.CrashReporter;
import com.foxx.androidcast.dev.DevAdbWifiKeeper; import com.foxx.androidcast.dev.DevAdbWifiKeeper;
import com.foxx.androidcast.display.WiredDisplayMonitor; import com.foxx.androidcast.display.WiredDisplayMonitor;
import com.foxx.androidcast.entitlements.EntitlementCoordinator;
import com.foxx.androidcast.media.CodecPriorityCatalog; import com.foxx.androidcast.media.CodecPriorityCatalog;
import com.foxx.androidcast.network.CastTransportFactory; import com.foxx.androidcast.network.CastTransportFactory;
import com.foxx.androidcast.receiver.av.ReceiverAvRuntime; import com.foxx.androidcast.receiver.av.ReceiverAvRuntime;
@@ -45,6 +46,7 @@ public class AndroidCastApplication extends Application {
CastTransportFactory.init(this); CastTransportFactory.init(this);
WiredDisplayMonitor.getInstance(this).ensureRegistered(); WiredDisplayMonitor.getInstance(this).ensureRegistered();
CommercialFeatures.refreshFromPreferences(this); CommercialFeatures.refreshFromPreferences(this);
EntitlementCoordinator.getInstance(this).bootstrap();
RemoteAccessCoordinator.onBootIfNeeded(this); RemoteAccessCoordinator.onBootIfNeeded(this);
DevAdbWifiKeeper.ensureStarted(this); DevAdbWifiKeeper.ensureStarted(this);
CastTrayNotifier.ensureTrayService(this); CastTrayNotifier.ensureTrayService(this);

View File

@@ -72,6 +72,8 @@ public final class AppPreferences {
private static final String KEY_DEV_REMOTE_ACCESS_VPN_ROUTE_SCOPE = "dev_remote_access_vpn_route_scope"; private static final String KEY_DEV_REMOTE_ACCESS_VPN_ROUTE_SCOPE = "dev_remote_access_vpn_route_scope";
private static final String KEY_DEV_REMOTE_ACCESS_VPN_APP_SCOPE = "dev_remote_access_vpn_app_scope"; private static final String KEY_DEV_REMOTE_ACCESS_VPN_APP_SCOPE = "dev_remote_access_vpn_app_scope";
private static final String KEY_DEV_ADB_WIFI_KEEPER = "dev_adb_wifi_keeper"; private static final String KEY_DEV_ADB_WIFI_KEEPER = "dev_adb_wifi_keeper";
private static final String KEY_ENTITLEMENT_ACCOUNT_KEY = "entitlement_account_key";
private static final String KEY_ENTITLEMENT_LAST_VERIFIED_WALL_MS = "entitlement_last_verified_wall_ms";
/** Minimum interval between automatic OTA checks on app launch. */ /** Minimum interval between automatic OTA checks on app launch. */
public static final long OTA_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000L; public static final long OTA_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000L;
@@ -692,6 +694,23 @@ public final class AppPreferences {
} }
} }
public static String getEntitlementAccountKey(Context context) {
return prefs(context).getString(KEY_ENTITLEMENT_ACCOUNT_KEY, "");
}
public static void setEntitlementAccountKey(Context context, String keyHex) {
prefs(context).edit().putString(KEY_ENTITLEMENT_ACCOUNT_KEY,
keyHex != null ? keyHex : "").apply();
}
public static long getEntitlementLastVerifiedWallMs(Context context) {
return prefs(context).getLong(KEY_ENTITLEMENT_LAST_VERIFIED_WALL_MS, 0L);
}
public static void setEntitlementLastVerifiedWallMs(Context context, long wallMs) {
prefs(context).edit().putLong(KEY_ENTITLEMENT_LAST_VERIFIED_WALL_MS, wallMs).apply();
}
private static SharedPreferences prefs(Context context) { private static SharedPreferences prefs(Context context) {
if (context == null) { if (context == null) {
throw new IllegalStateException("context is null"); throw new IllegalStateException("context is null");

View File

@@ -0,0 +1,181 @@
package com.foxx.androidcast.entitlements;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.foxx.androidcast.BuildConfig;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
/**
* Process-wide paid-feature bitmask: loads native lib on demand, reads local obfuscated file, refreshes from BE.
*/
public final class EntitlementCoordinator {
private static final String TAG = "EntitlementCoord";
private static final long DEV_ACCOUNT_KEY = 0x0123456789ABCDEFL;
private static volatile EntitlementCoordinator instance;
private final Context appContext;
private final ExecutorService syncExecutor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "entitlement-sync");
t.setDaemon(true);
return t;
});
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final AtomicLong flags = new AtomicLong(0L);
private volatile boolean nativeCapable;
public static EntitlementCoordinator getInstance(Context context) {
if (instance == null) {
synchronized (EntitlementCoordinator.class) {
if (instance == null) {
instance = new EntitlementCoordinator(context.getApplicationContext());
}
}
}
return instance;
}
private EntitlementCoordinator(Context appContext) {
this.appContext = appContext;
}
private volatile boolean bootstrapDone;
/** Call once from {@link com.foxx.androidcast.AndroidCastApplication}. */
public void bootstrap() {
if (bootstrapDone) {
return;
}
bootstrapDone = true;
reloadFromLocal();
scheduleBackendSync();
}
public long getFlags() {
return flags.get();
}
public boolean hasFeature(long featureBit) {
return EntitlementFlags.hasFeature(flags.get(), featureBit);
}
public boolean hasAnyPaidFeature() {
return EntitlementFlags.hasAnyPaidBit(flags.get());
}
public boolean isNativeLibraryAvailable() {
return nativeCapable;
}
/** Re-read local file after manual refresh or sync completion. */
public void reloadFromLocal() {
if (isDevUnlockAll()) {
flags.set(EntitlementFlags.DEV_ALL_PAYLOAD);
EntitlementNativeBridge.acquire();
nativeCapable = EntitlementNativeBridge.isAvailable();
if (nativeCapable) {
try {
writeDevBlobIfNeeded();
} finally {
EntitlementNativeBridge.release();
}
} else {
EntitlementNativeBridge.release();
}
return;
}
EntitlementNativeBridge.acquire();
nativeCapable = EntitlementNativeBridge.isAvailable();
if (!nativeCapable) {
EntitlementNativeBridge.release();
flags.set(0L);
return;
}
try {
long now = System.currentTimeMillis();
if (EntitlementPolicy.isClockRollback(now, EntitlementStore.getLastVerifiedWallMs(appContext),
EntitlementPolicy.CLOCK_ROLLBACK_TOLERANCE_MS)) {
Log.w(TAG, "wall clock rollback detected — clearing entitlements");
resetToDefaults();
return;
}
File file = EntitlementStore.entitlementFile(appContext);
if (!file.isFile()) {
flags.set(0L);
return;
}
if (EntitlementPolicy.isFileStale(file.lastModified(), now, EntitlementPolicy.FILE_TTL_MS)) {
Log.i(TAG, "local entitlement file expired");
resetToDefaults();
return;
}
long key = EntitlementStore.getAccountKey(appContext);
if (key == 0L) {
flags.set(0L);
return;
}
long[] out = new long[1];
int rv = EntitlementNativeBridge.readFrom(file.getAbsolutePath(), key, out);
if (rv != 0) {
Log.w(TAG, "readFrom failed: " + rv);
flags.set(0L);
return;
}
flags.set(out[0]);
EntitlementStore.setLastVerifiedWallMs(appContext, now);
} finally {
EntitlementNativeBridge.release();
}
}
public void scheduleBackendSync() {
syncExecutor.execute(this::runBackendSync);
}
private void runBackendSync() {
EntitlementSyncClient.SyncPayload payload = EntitlementSyncClient.fetch(appContext);
if (payload == null) {
return;
}
if (!EntitlementSyncClient.persistPayload(appContext, payload)) {
return;
}
mainHandler.post(this::reloadFromLocal);
}
private void resetToDefaults() {
EntitlementStore.clearLocal(appContext);
flags.set(0L);
}
private boolean isDevUnlockAll() {
return BuildConfig.DEBUG;
}
/** Debug: materialize an all-features blob so Linux ↔ device file tests work without BE. */
private void writeDevBlobIfNeeded() {
File file = EntitlementStore.entitlementFile(appContext);
if (file.isFile() && EntitlementStore.getAccountKey(appContext) == DEV_ACCOUNT_KEY) {
return;
}
EntitlementStore.setAccountKey(appContext, DEV_ACCOUNT_KEY);
int w = EntitlementNativeBridge.writeTo(file.getAbsolutePath(),
EntitlementFlags.DEV_ALL_PAYLOAD, DEV_ACCOUNT_KEY);
if (w != 0) {
Log.w(TAG, "dev writeTo failed: " + w);
}
}
}

View File

@@ -0,0 +1,17 @@
package com.foxx.androidcast.entitlements;
/** Bitmask constants for paid / licensed app capabilities (40-bit payload). */
public final class EntitlementFlags {
private EntitlementFlags() {}
/** All encodable payload bits — dev builds unlock everything. */
public static final long DEV_ALL_PAYLOAD = 0xFFFFFFFFFFL;
public static boolean hasFeature(long flags, long featureBit) {
return featureBit != 0L && (flags & featureBit) == featureBit;
}
public static boolean hasAnyPaidBit(long flags) {
return flags != 0L;
}
}

View File

@@ -0,0 +1,105 @@
package com.foxx.androidcast.entitlements;
import android.util.Log;
/**
* Loads {@code libandroidcast_entitlements.so} on demand. If load fails, callers must assume zero paid
* flags (unless dev unlock-all is enabled at the Java layer).
*/
public final class EntitlementNativeBridge {
private static final String TAG = "EntitlementNative";
static final String LIB = "androidcast_entitlements";
private static final Object LOCK = new Object();
private static int refCount = 0;
private static boolean loaded = false;
private static boolean loadFailed = false;
private EntitlementNativeBridge() {}
public static void acquire() {
synchronized (LOCK) {
if (loadFailed) {
return;
}
if (!loaded) {
try {
System.loadLibrary(LIB);
loaded = true;
} catch (UnsatisfiedLinkError e) {
loadFailed = true;
Log.w(TAG, "Native entitlements library unavailable: " + e.getMessage());
return;
}
}
refCount++;
}
}
public static void release() {
synchronized (LOCK) {
if (refCount <= 0) {
return;
}
refCount--;
if (refCount == 0) {
loaded = false;
}
}
}
public static boolean isAvailable() {
synchronized (LOCK) {
return loaded && !loadFailed;
}
}
public static boolean ensureLoaded() {
acquire();
boolean ok = isAvailable();
if (!ok) {
release();
}
return ok;
}
static void requireLoaded() {
if (!isAvailable()) {
throw new IllegalStateException("libandroidcast_entitlements is not loaded");
}
}
public static int readFrom(String absolutePath, long key, long[] outVal) {
requireLoaded();
if (outVal == null || outVal.length < 1) {
return -1;
}
return nativeReadFrom(absolutePath, key, outVal);
}
public static int writeTo(String absolutePath, long val, long key) {
requireLoaded();
return nativeWriteTo(absolutePath, val, key);
}
public static int decode(long encodedVal, long key, long[] outVal) {
requireLoaded();
if (outVal == null || outVal.length < 1) {
return -1;
}
return nativeDecode(encodedVal, key, outVal);
}
public static int encode(long val, long key, long[] outEncoded) {
requireLoaded();
if (outEncoded == null || outEncoded.length < 1) {
return -1;
}
return nativeEncode(val, key, outEncoded);
}
private static native int nativeReadFrom(String path, long key, long[] outVal);
private static native int nativeWriteTo(String path, long val, long key);
private static native int nativeDecode(long encodedVal, long key, long[] outVal);
private static native int nativeEncode(long val, long key, long[] outEncoded);
}

View File

@@ -0,0 +1,53 @@
package com.foxx.androidcast.entitlements;
/** Pure policy helpers (unit-tested without native I/O). */
final class EntitlementPolicy {
/** Local entitlement blob older than this is discarded (offline abuse mitigation). */
static final long FILE_TTL_MS = 3L * 24L * 60L * 60L * 1000L;
/** Wall clock moved backwards beyond this vs last successful verify → treat as tamper. */
static final long CLOCK_ROLLBACK_TOLERANCE_MS = 60_000L;
private EntitlementPolicy() {}
static boolean isFileStale(long fileLastModifiedMs, long nowMs, long ttlMs) {
if (fileLastModifiedMs <= 0L) {
return true;
}
long age = nowMs - fileLastModifiedMs;
return age < 0L || age > ttlMs;
}
static boolean isClockRollback(long nowMs, long lastVerifiedWallMs, long toleranceMs) {
return lastVerifiedWallMs > 0L && nowMs + toleranceMs < lastVerifiedWallMs;
}
static long parseKeyHex(String hex) {
if (hex == null) {
return 0L;
}
String trimmed = hex.trim();
if (trimmed.isEmpty()) {
return 0L;
}
if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) {
trimmed = trimmed.substring(2);
}
if (trimmed.matches("[0-9]+")) {
try {
return Long.parseLong(trimmed, 10);
} catch (NumberFormatException e) {
return 0L;
}
}
try {
return Long.parseUnsignedLong(trimmed, 16);
} catch (NumberFormatException e) {
return 0L;
}
}
static String formatKeyHex(long key) {
return String.format("%016x", key);
}
}

View File

@@ -0,0 +1,47 @@
package com.foxx.androidcast.entitlements;
import android.content.Context;
import com.foxx.androidcast.AppPreferences;
import java.io.File;
/** Paths and persisted key / anti-tamper timestamps for the local entitlement blob. */
final class EntitlementStore {
private static final String FILE_NAME = "account_flags.obf";
private EntitlementStore() {}
static File entitlementFile(Context context) {
File dir = new File(context.getFilesDir(), "entitlements");
if (!dir.exists() && !dir.mkdirs()) {
return new File(context.getFilesDir(), FILE_NAME);
}
return new File(dir, FILE_NAME);
}
static long getAccountKey(Context context) {
return EntitlementPolicy.parseKeyHex(AppPreferences.getEntitlementAccountKey(context));
}
static void setAccountKey(Context context, long key) {
AppPreferences.setEntitlementAccountKey(context, EntitlementPolicy.formatKeyHex(key));
}
static long getLastVerifiedWallMs(Context context) {
return AppPreferences.getEntitlementLastVerifiedWallMs(context);
}
static void setLastVerifiedWallMs(Context context, long wallMs) {
AppPreferences.setEntitlementLastVerifiedWallMs(context, wallMs);
}
static void clearLocal(Context context) {
File f = entitlementFile(context);
if (f.exists() && !f.delete()) {
f.deleteOnExit();
}
AppPreferences.setEntitlementAccountKey(context, "");
AppPreferences.setEntitlementLastVerifiedWallMs(context, 0L);
}
}

View File

@@ -0,0 +1,151 @@
package com.foxx.androidcast.entitlements;
import android.content.Context;
import android.util.Log;
import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.network.BackendEndpoints;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* Fetches per-account entitlement key + obfuscated blob from BE (RBAC-backed endpoint stub).
* Returns {@code null} when backend has no payload or network fails — local file remains authoritative.
*/
final class EntitlementSyncClient {
private static final String TAG = "EntitlementSync";
static final class SyncPayload {
final long accountKey;
final byte[] fileBytes;
final String etag;
SyncPayload(long accountKey, byte[] fileBytes, String etag) {
this.accountKey = accountKey;
this.fileBytes = fileBytes;
this.etag = etag != null ? etag : "";
}
}
private EntitlementSyncClient() {}
static SyncPayload fetch(Context context) {
String user = AppPreferences.getUsername(context);
if (user == null || user.trim().isEmpty()) {
return null;
}
HttpURLConnection conn = null;
try {
String q = "user=" + URLEncoder.encode(user.trim(), StandardCharsets.UTF_8.name());
String pin = AppPreferences.getPin(context);
if (pin != null && !pin.isEmpty()) {
q += "&pin=" + URLEncoder.encode(pin, StandardCharsets.UTF_8.name());
}
URL url = new URL(BackendEndpoints.ENTITLEMENTS_FETCH_URL + "?" + q);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(12_000);
conn.setReadTimeout(20_000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return null;
}
if (code != HttpURLConnection.HTTP_OK) {
Log.w(TAG, "fetch HTTP " + code);
return null;
}
String body = readUtf8(conn.getInputStream());
JSONObject json = new JSONObject(body);
String keyHex = json.optString("key", "");
long key = EntitlementPolicy.parseKeyHex(keyHex);
if (key == 0L) {
Log.w(TAG, "fetch missing key");
return null;
}
String fileUrl = json.optString("file_url", "");
String etag = json.optString("etag", "");
byte[] bytes;
if (!fileUrl.isEmpty()) {
bytes = downloadBytes(fileUrl);
} else if (json.has("file_base64")) {
bytes = android.util.Base64.decode(json.getString("file_base64"), android.util.Base64.DEFAULT);
} else {
Log.w(TAG, "fetch missing file payload");
return null;
}
if (bytes == null || bytes.length < 16) {
return null;
}
return new SyncPayload(key, bytes, etag);
} catch (Exception e) {
Log.w(TAG, "fetch failed: " + e.getMessage());
return null;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
static boolean persistPayload(Context context, SyncPayload payload) {
if (payload == null || payload.fileBytes == null) {
return false;
}
File dest = EntitlementStore.entitlementFile(context);
File parent = dest.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
return false;
}
try (FileOutputStream fos = new FileOutputStream(dest, false)) {
fos.write(payload.fileBytes);
fos.flush();
} catch (Exception e) {
Log.w(TAG, "persist failed: " + e.getMessage());
return false;
}
EntitlementStore.setAccountKey(context, payload.accountKey);
return true;
}
private static byte[] downloadBytes(String fileUrl) throws Exception {
HttpURLConnection c = (HttpURLConnection) new URL(fileUrl).openConnection();
c.setConnectTimeout(12_000);
c.setReadTimeout(30_000);
try {
if (c.getResponseCode() != HttpURLConnection.HTTP_OK) {
return null;
}
return readBytes(c.getInputStream());
} finally {
c.disconnect();
}
}
private static String readUtf8(InputStream in) throws Exception {
byte[] raw = readBytes(in);
return new String(raw, StandardCharsets.UTF_8);
}
private static byte[] readBytes(InputStream in) throws Exception {
try (BufferedInputStream bin = new BufferedInputStream(in);
ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
byte[] buf = new byte[4096];
int n;
while ((n = bin.read(buf)) >= 0) {
bout.write(buf, 0, n);
}
return bout.toByteArray();
}
}
}

View File

@@ -0,0 +1,33 @@
package com.foxx.androidcast.entitlements;
import android.content.Context;
/**
* App-wide gate for paid capabilities decoded from the per-account obfuscated blob.
* Until feature-specific bits are defined, {@link #hasAnyPaidFeature} is the primary check.
*/
public final class PaidEntitlements {
private PaidEntitlements() {}
public static long getFlags(Context context) {
return EntitlementCoordinator.getInstance(context).getFlags();
}
public static boolean hasAnyPaidFeature(Context context) {
return EntitlementCoordinator.getInstance(context).hasAnyPaidFeature();
}
public static boolean hasFeature(Context context, long featureBit) {
return EntitlementCoordinator.getInstance(context).hasFeature(featureBit);
}
public static boolean isNativeGateAvailable(Context context) {
return EntitlementCoordinator.getInstance(context).isNativeLibraryAvailable();
}
public static void refresh(Context context) {
EntitlementCoordinator coord = EntitlementCoordinator.getInstance(context);
coord.reloadFromLocal();
coord.scheduleBackendSync();
}
}

View File

@@ -10,6 +10,10 @@ public final class BackendEndpoints {
public static final String HEARTBEAT_URL = CRASHES_BASE + "/api/heartbeat.php"; public static final String HEARTBEAT_URL = CRASHES_BASE + "/api/heartbeat.php";
public static final String UPLOAD_URL = CRASHES_BASE + "/api/upload.php"; public static final String UPLOAD_URL = CRASHES_BASE + "/api/upload.php";
public static final String GRAPH_UPLOAD_URL = GRAPHS_BASE + "/api/graph_upload.php"; public static final String GRAPH_UPLOAD_URL = GRAPHS_BASE + "/api/graph_upload.php";
public static final String ENTITLEMENTS_BASE =
"https://" + APPS_HOST + "/app/androidcast_project/entitlements";
/** GET — returns JSON {@code {key, file_url|file_base64, etag}} for RBAC-authenticated user. */
public static final String ENTITLEMENTS_FETCH_URL = ENTITLEMENTS_BASE + "/api/fetch.php";
private BackendEndpoints() {} private BackendEndpoints() {}

View File

@@ -0,0 +1,42 @@
package com.foxx.androidcast.entitlements;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class EntitlementPolicyTest {
@Test
public void fileStaleAfterTtl() {
long now = 500_000_000_000L;
long fresh = now - EntitlementPolicy.FILE_TTL_MS + 60_000L;
long stale = now - EntitlementPolicy.FILE_TTL_MS - 60_000L;
assertFalse(EntitlementPolicy.isFileStale(fresh, now, EntitlementPolicy.FILE_TTL_MS));
assertTrue(EntitlementPolicy.isFileStale(stale, now, EntitlementPolicy.FILE_TTL_MS));
}
@Test
public void clockRollbackDetected() {
long lastOk = 5_000_000L;
long nowOk = lastOk + 1_000L;
long nowRollback = lastOk - EntitlementPolicy.CLOCK_ROLLBACK_TOLERANCE_MS - 1;
assertFalse(EntitlementPolicy.isClockRollback(nowOk, lastOk,
EntitlementPolicy.CLOCK_ROLLBACK_TOLERANCE_MS));
assertTrue(EntitlementPolicy.isClockRollback(nowRollback, lastOk,
EntitlementPolicy.CLOCK_ROLLBACK_TOLERANCE_MS));
}
@Test
public void parseKeyHex() {
assertEquals(0xcafebabedeadbeefL, EntitlementPolicy.parseKeyHex("cafebabedeadbeef"));
assertEquals(42L, EntitlementPolicy.parseKeyHex("42"));
assertEquals(0L, EntitlementPolicy.parseKeyHex(""));
assertEquals(0L, EntitlementPolicy.parseKeyHex(null));
}
@Test
public void devAllPayloadHasBits() {
assertTrue(EntitlementFlags.hasAnyPaidBit(EntitlementFlags.DEV_ALL_PAYLOAD));
}
}

View File

@@ -10,6 +10,7 @@ public class BackendEndpointsTest {
public void canonicalUrlsUseAppsHost() { public void canonicalUrlsUseAppsHost() {
assertTrue(BackendEndpoints.HEARTBEAT_URL.startsWith("https://apps.f0xx.org/")); assertTrue(BackendEndpoints.HEARTBEAT_URL.startsWith("https://apps.f0xx.org/"));
assertTrue(BackendEndpoints.UPLOAD_URL.contains("/api/upload.php")); assertTrue(BackendEndpoints.UPLOAD_URL.contains("/api/upload.php"));
assertTrue(BackendEndpoints.ENTITLEMENTS_FETCH_URL.contains("/entitlements/api/fetch.php"));
} }
@Test @Test

View File

@@ -483,7 +483,7 @@ endobj
endobj endobj
81 0 obj 81 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220445+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220445+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174830+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174830+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 On-Demand Remote Access \(Reverse Tunnel\) Proposals) /Trapped /False
>> >>
endobj endobj
@@ -1052,7 +1052,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<3f94d39cd85223d5686ff0349cc56520><3f94d39cd85223d5686ff0349cc56520>] [<559db9fd7b62d2e91af17ade78c5196e><559db9fd7b62d2e91af17ade78c5196e>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 81 0 R /Info 81 0 R

View File

@@ -265,7 +265,7 @@ endobj
endobj endobj
42 0 obj 42 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220445+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220445+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174830+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174830+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 Email agent config & 2FA / mobile auth flow) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 Email agent config & 2FA / mobile auth flow) /Trapped /False
>> >>
endobj endobj
@@ -537,7 +537,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<8e9582b9d629fb33039bda9127c34432><8e9582b9d629fb33039bda9127c34432>] [<004fa69baafe806860fd5caf900f27cc><004fa69baafe806860fd5caf900f27cc>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 42 0 R /Info 42 0 R

View File

@@ -208,7 +208,7 @@ endobj
endobj endobj
33 0 obj 33 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220445+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220445+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174830+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174830+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha priorities \(2026-06-08 renumber\)) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 alpha priorities \(2026-06-08 renumber\)) /Trapped /False
>> >>
endobj endobj
@@ -418,7 +418,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<cbeebf3abfc4c12ed8adf1ecbcb8d5a4><cbeebf3abfc4c12ed8adf1ecbcb8d5a4>] [<d83210b5b8072cc987bb1c318f31c89f><d83210b5b8072cc987bb1c318f31c89f>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 33 0 R /Info 33 0 R

View File

@@ -203,7 +203,7 @@ endobj
endobj endobj
31 0 obj 31 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174830+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174830+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (BE services & infrastructure \204 validation map) /Trapped /False /Subject (\(unspecified\)) /Title (BE services & infrastructure \204 validation map) /Trapped /False
>> >>
endobj endobj
@@ -385,7 +385,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<33842309a5332be4e933481fb5cc3564><33842309a5332be4e933481fb5cc3564>] [<75fe7d8232594dda144e97ffd8f367f5><75fe7d8232594dda144e97ffd8f367f5>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 31 0 R /Info 31 0 R

View File

@@ -137,7 +137,7 @@ endobj
endobj endobj
22 0 obj 22 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Stream dump \204 developer debug recording \(FR 0.1\)) /Trapped /False /Subject (\(unspecified\)) /Title (Stream dump \204 developer debug recording \(FR 0.1\)) /Trapped /False
>> >>
endobj endobj
@@ -277,7 +277,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<9df46fa4be55aef5251ba708945ef660><9df46fa4be55aef5251ba708945ef660>] [<20faf14480cf6a5151269a1112e8dd23><20faf14480cf6a5151269a1112e8dd23>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R /Info 22 0 R

View File

@@ -275,7 +275,7 @@ endobj
endobj endobj
47 0 obj 47 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 exposed services catalog) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 exposed services catalog) /Trapped /False
>> >>
endobj endobj
@@ -589,7 +589,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<3bea3360dfff53d8101b105e2ed9c80c><3bea3360dfff53d8101b105e2ed9c80c>] [<ab9efcddb9075779887ee065ecd85d8c><ab9efcddb9075779887ee065ecd85d8c>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 47 0 R /Info 47 0 R

View File

@@ -183,7 +183,7 @@ endobj
endobj endobj
30 0 obj 30 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 alpha release & QA) /Trapped /False
>> >>
endobj endobj
@@ -377,7 +377,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<ef2f91cbe4ea30aef08962c29d279824><ef2f91cbe4ea30aef08962c29d279824>] [<22fbb11b27f5f2572e8185d6911a1116><22fbb11b27f5f2572e8185d6911a1116>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 30 0 R /Info 30 0 R

View File

@@ -122,7 +122,7 @@ endobj
endobj endobj
20 0 obj 20 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False /Subject (\(unspecified\)) /Title (Alpha soak checklist \(LAN demo\)) /Trapped /False
>> >>
endobj endobj
@@ -240,7 +240,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<933980664bcbdbdfe6a12d9cb1786fa3><933980664bcbdbdfe6a12d9cb1786fa3>] [<d13b107bc5a7f93c8642ebc1592ff911><d13b107bc5a7f93c8642ebc1592ff911>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R

View File

@@ -244,7 +244,7 @@ endobj
endobj endobj
40 0 obj 40 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False /Subject (\(unspecified\)) /Title (AV quality enhancement \204 Q&A session notes) /Trapped /False
>> >>
endobj endobj
@@ -512,7 +512,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<ad9c835cc704152d28b5b3f7ebff6df3><ad9c835cc704152d28b5b3f7ebff6df3>] [<885b3d86c2d7fc8825759aeb11eaff9c><885b3d86c2d7fc8825759aeb11eaff9c>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 40 0 R /Info 40 0 R

View File

@@ -137,7 +137,7 @@ endobj
endobj endobj
22 0 obj 22 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast build ecosystem) /Trapped /False
>> >>
endobj endobj
@@ -278,7 +278,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<6cfcadb37be859dd22566ebb5a6a8025><6cfcadb37be859dd22566ebb5a6a8025>] [<e642f50f6998474676e62cda30c69ab5><e642f50f6998474676e62cda30c69ab5>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R /Info 22 0 R

View File

@@ -97,7 +97,7 @@ endobj
endobj endobj
15 0 obj 15 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False /Subject (\(unspecified\)) /Title (Commercial features \(developer-gated\)) /Trapped /False
>> >>
endobj endobj
@@ -192,7 +192,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<c8d6b37b537060a0dc93294feeb6bfd7><c8d6b37b537060a0dc93294feeb6bfd7>] [<550d28e222cb18370f0095c31b168c50><550d28e222cb18370f0095c31b168c50>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 15 0 R /Info 15 0 R

View File

@@ -102,7 +102,7 @@ endobj
endobj endobj
16 0 obj 16 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False /Subject (\(unspecified\)) /Title (Anonymous crash reporter) /Trapped /False
>> >>
endobj endobj
@@ -198,7 +198,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<50b171b4584914c2210e49fce78dc2be><50b171b4584914c2210e49fce78dc2be>] [<a6b6c31fc1c3334769ccc6ba2abbc535><a6b6c31fc1c3334769ccc6ba2abbc535>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R /Info 16 0 R

View File

@@ -279,7 +279,7 @@ endobj
endobj endobj
46 0 obj 46 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (URL shortener service \204 design review \(DR\)) /Trapped /False /Subject (\(unspecified\)) /Title (URL shortener service \204 design review \(DR\)) /Trapped /False
>> >>
endobj endobj
@@ -553,7 +553,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<3a7c0ee4bbf3482849eeadc12de9b9c8><3a7c0ee4bbf3482849eeadc12de9b9c8>] [<83c7ff4a46a2644aace09ed6f7e081cb><83c7ff4a46a2644aace09ed6f7e081cb>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 46 0 R /Info 46 0 R

View File

@@ -498,7 +498,7 @@ endobj
endobj endobj
79 0 obj 79 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220446+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220446+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174831+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174831+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Platform scaling and multi-environment architecture \204 design review \(DR\)) /Trapped /False /Subject (\(unspecified\)) /Title (Platform scaling and multi-environment architecture \204 design review \(DR\)) /Trapped /False
>> >>
endobj endobj
@@ -933,7 +933,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<cc552ef08a2fe35d84129cecaf25d843><cc552ef08a2fe35d84129cecaf25d843>] [<7d99cda1a76db27f24dc235d81baf7e5><7d99cda1a76db27f24dc235d81baf7e5>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 79 0 R /Info 79 0 R

View File

@@ -324,7 +324,7 @@ endobj
endobj endobj
51 0 obj 51 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220453+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220453+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174832+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174832+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (RSSH routed egress \(dev-only\) \204 design review \(DR\)) /Trapped /False /Subject (\(unspecified\)) /Title (RSSH routed egress \(dev-only\) \204 design review \(DR\)) /Trapped /False
>> >>
endobj endobj
@@ -663,7 +663,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<c1d94c835c02f8ed54a6427788bba047><c1d94c835c02f8ed54a6427788bba047>] [<ed6e2cbfecdbe9d49d63d210ed176bb8><ed6e2cbfecdbe9d49d63d210ed176bb8>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 51 0 R /Info 51 0 R

View File

@@ -319,7 +319,7 @@ endobj
endobj endobj
51 0 obj 51 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220459+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220459+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174832+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174832+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Codec2 / ultra-low-bandwidth voice \204 design review \(DR\)) /Trapped /False /Subject (\(unspecified\)) /Title (Codec2 / ultra-low-bandwidth voice \204 design review \(DR\)) /Trapped /False
>> >>
endobj endobj
@@ -657,7 +657,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<cb8942224039a3f8bf18c924200f4ad4><cb8942224039a3f8bf18c924200f4ad4>] [<c52c2dec229f7f4121897008cb63143d><c52c2dec229f7f4121897008cb63143d>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 51 0 R /Info 51 0 R

View File

@@ -0,0 +1,681 @@
# Repository and microservice reorganization — design review (DR)
<!-- doc-meta:start -->
| Field | Value |
|---|---|
| Author | Anton Afanasyeu |
| Revision | R0 |
| Creation date | 2026-06-18 |
| Last modification date | 2026-06-18 |
| Co-authored | Cursor Agent (project assistant) |
| Severity | high |
| State | approved for planning |
| Document type | DR |
| Pre-requisite to | Gitea org `ac` repo creation; post-alpha full MS split (alpha may stay on monolith until Step 8+) |
<!-- doc-meta:end -->
\newpage
\newpage
---
**Document type:** DR (Design Review)
**Source draft:** [docs/drafts/20260618_repos_reorganizing.txt](../drafts/20260618_repos_reorganizing.txt)
**PDF:** [20260618_repos_reorganizing.pdf](20260618_repos_reorganizing.pdf) · Regenerate: `bash scripts/build-all-docs-pdf.sh`
**Status:** PO-approved (2026-06-18) — **implementation phased**; monolith remains deployable until each step completes
**Scope:** Split `git://f0xx.org/android_cast` monolith into Gitea org **`ac`** — full microservice end-state, VM-first cloud path
**Related:** [INFRA.md](../INFRA.md) · [BUILD_DEPLOY.md](../BUILD_DEPLOY.md) · [specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md) · [orchestration/sim/cluster0/ARCHITECTURE.md](../../orchestration/sim/cluster0/ARCHITECTURE.md) · [examples/crash_reporter/backend/scripts/gitea/README.md](../../examples/crash_reporter/backend/scripts/gitea/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. PO decisions (locked)](#3-po-decisions-locked)
- [4. Current state](#4-current-state)
- [5. Target architecture](#5-target-architecture)
- [6. Gitea repo catalog](#6-gitea-repo-catalog)
- [7. Public URL map](#7-public-url-map)
- [8. Dependency graphs](#8-dependency-graphs)
- [9. Monolith conversion steps](#9-monolith-conversion-steps)
- [10. Microservice vs monolith comparison](#10-microservice-vs-monolith-comparison)
- [11. Infrastructure and cloud](#11-infrastructure-and-cloud)
- [12. ac-workspace and OTA versioning](#12-ac-workspace-and-ota-versioning)
- [13. Risks and mitigations](#13-risks-and-mitigations)
- [14. Changelog](#14-changelog)
<!-- /toc -->
---
## 1. Executive summary
**Question:** How should the androidcast monolith (`git://f0xx.org/android_cast`) be split into independent repos and deployable microservices?
**Short answer:** Create Gitea org **`ac`** with **platform libraries**, **microservice APIs**, **thin backend UIs**, **client repos**, and **`ac-deploy`** (submodules **`ac-scripts`**). End-state is **full microservice** architecture. Delivery is **strictly ordered****[§9 Monolith conversion steps](#9-monolith-conversion-steps)** — with **`ac-ms-identity` as the gate** before any domain service split.
**DR decision (R0):**
| Item | Decision |
|------|----------|
| **Gitea org** | **`ac`** — `git://f0xx.org/ac/<repo>` |
| **Architecture** | Full microservice end-state; phased migration from monolith |
| **URL prefix** | **`/app/androidcast_project/`** (full path, locked) |
| **Path-per-service** | `/issues/`, `/tickets/`, `/access/`, etc. — replace `/crashes/?view=` |
| **Scripts** | **`ac-scripts`** separate; **`ac-deploy`** consumes it (submodule or PATH) |
| **Workspace** | Optional **`ac-workspace`** — selective clone only; **not** build-all-in-one |
| **Identity gate** | **Step 6** (`ac-ms-identity`) before domain MS extraction |
| **Cloud** | VM lift-and-shift first; K8s optional later |
| **Alpha** | Monolith may run in prod until Step 8+; no forced big-bang |
---
## 2. Problem statement
The monolith bundles Android app, PHP backend (crashes, tickets, graphs, RBAC, remote access), builder, hub, orchestration, docs, and one submodule (url-shortener). This blocks:
- Independent CI/CD and rollback per service
- Clear ownership and OpenAPI contracts
- Mapping repos to VM roles ([scaling SPEC §6.3](../specs/20100612_1_scaling.md))
- Product URLs such as [issues](https://apps.f0xx.org/app/androidcast_project/crashes/?view=reports) vs [tickets](https://apps.f0xx.org/app/androidcast_project/crashes/?view=tickets) under distinct paths
Todays coupling: **one MariaDB**, **one session cookie** (`ac_crash_sess`, path `/app/androidcast_project`), **shared Auth.php** across consoles ([BUILD_DEPLOY.md](../BUILD_DEPLOY.md)).
---
## 3. PO decisions (locked)
| # | Topic | Decision |
|---|-------|----------|
| 1 | Gitea org | **`ac`** |
| 2 | Architecture | **Full microservice** (phased delivery) |
| 3 | ac-scripts | Separate repo; **ac-deploy utilizes ac-scripts** |
| 4 | Public URLs | Path-per-service under **`/app/androidcast_project/`** |
| 5 | ac-workspace | Optional manifest; **not** unified build; supports multi-repo OTA |
| 6 | Cloud | VM lift-and-shift; no K8s requirement at R0 |
| 7 | Identity gate | **Approved** — identity MS before other MS splits |
---
## 4. Current state
### 4.1 Monolith map
| Monolith path | Future repo(s) | Notes |
|---------------|----------------|-------|
| `app/`, `ndk/`, `gradle/` | ac-mobile-android | third-party submodules |
| `desktop/session-studio/` | ac-session-studio | Low coupling |
| `docs/` | ac-docs | |
| `scripts/` | ac-scripts | |
| `orchestration/` | ac-deploy | cluster0, docker |
| `examples/crash_reporter/backend/` | ac-ms-* + ac-be-* | **32 PHP classes**, 24 APIs |
| `examples/build_console/` | ac-ms-build, ac-be-builder | Same DB `users` |
| `examples/app_hub/` | ac-be-hub | Loads `/crashes/assets/` today |
| `backend/url-shortener/` | ac-ms-url-shortener | Existing submodule |
### 4.2 What is not a git repo
- **Gitea** (infra on BE; scripts in ac-deploy)
- **MariaDB**, **Janus** (:8089)
- `examples/gentoo-portage-experimental/` (exclude or separate lab repo)
---
## 5. Target architecture
```text
Platform (Composer / static — not runtime MS)
ac-platform-php, ac-platform-db, ac-platform-web, ac-scripts, ac-docs
Infrastructure
ac-deploy submodules ac-scripts; docker, cluster0, nginx
ac-platform-edge nginx BFF: routes + session cookie domain
ac-workspace optional clone manifest (no unified build)
Clients
ac-mobile-android, ac-mobile-ios (future), ac-session-studio
Microservices (OpenAPI)
ac-ms-identity → ac-ms-rbac → ac-ms-devices
ac-ms-issues, ac-ms-tickets, ac-ms-graphs, ac-ms-remote-access
ac-ms-url-shortener, ac-ms-build, ac-ms-ota, ac-ms-notifications (later)
Backend UI (thin → MS APIs)
ac-be-hub, ac-be-issues, ac-be-tickets, ac-be-graphs,
ac-be-remote-access, ac-be-access, ac-be-builder, ac-be-auth
```
**Consolidations vs initial PO sketch:**
| Initial name | R0 decision |
|--------------|-------------|
| ac-be-console (monolith) | Dissolved into ac-be-issues … ac-be-access |
| ac-ms-tracker | **ac-ms-issues** + **ac-ms-tickets** |
| ac-ms-vpn-rssh + ac-ms-vpn-wireguard | **ac-ms-remote-access** (single control plane) |
| ac-ms-analytics | **ac-ms-graphs** (+ BI later) |
| ac-ms-orchestration | **ac-ms-build** + **ac-deploy** |
| ac-ms-vcs | Not a repo — Gitea infra |
---
## 6. Gitea repo catalog
Canonical: **`git://f0xx.org/ac/<repo>`**
HTTPS (Gitea UI): **`https://apps.f0xx.org/app/androidcast_project/git/ac/<repo>.git`**
Legacy: **`git://f0xx.org/android_cast`** → read-only mirror until retired.
| ID | Repo | Git URL | Role |
|----|------|---------|------|
| W0 | ac-workspace | `git://f0xx.org/ac/ac-workspace` | Optional submodule manifest |
| P0 | ac-platform-php | `git://f0xx.org/ac/ac-platform-php` | Composer: auth client, PDO, HTTP |
| P1 | ac-platform-db | `git://f0xx.org/ac/ac-platform-db` | SQL migrations |
| P2 | ac-platform-web | `git://f0xx.org/ac/ac-platform-web` | Shared CSS/JS/theme |
| P3 | ac-platform-edge | `git://f0xx.org/ac/ac-platform-edge` | nginx routes + proxy |
| P4 | ac-scripts | `git://f0xx.org/ac/ac-scripts` | CI, OTA, native codecs, PDF |
| P5 | ac-docs | `git://f0xx.org/ac/ac-docs` | Documentation |
| D0 | ac-deploy | `git://f0xx.org/ac/ac-deploy` | docker, cluster0; **uses P4** |
| C0 | ac-mobile-android | `git://f0xx.org/ac/ac-mobile-android` | Android + ndk + third-party |
| C1 | ac-mobile-ios | `git://f0xx.org/ac/ac-mobile-ios` | Future iOS |
| C2 | ac-session-studio | `git://f0xx.org/ac/ac-session-studio` | Desktop analyzer |
| S0 | ac-ms-template | `git://f0xx.org/ac/ac-ms-template` | New MS cookiecutter |
| S1 | ac-ms-identity | `git://f0xx.org/ac/ac-ms-identity` | Users, login, 2FA, sessions |
| S2 | ac-ms-rbac | `git://f0xx.org/ac/ac-ms-rbac` | Companies, privileges |
| S3 | ac-ms-devices | `git://f0xx.org/ac/ac-ms-devices` | Device registry |
| S4 | ac-ms-issues | `git://f0xx.org/ac/ac-ms-issues` | Crash/report ingest |
| S5 | ac-ms-tickets | `git://f0xx.org/ac/ac-ms-tickets` | Ticket workflow |
| S6 | ac-ms-graphs | `git://f0xx.org/ac/ac-ms-graphs` | Graph ingest/query |
| S7 | ac-ms-remote-access | `git://f0xx.org/ac/ac-ms-remote-access` | WG + RSSH API |
| S8 | ac-ms-url-shortener | `git://f0xx.org/ac/ac-ms-url-shortener` | Short links API |
| S9 | ac-ms-build | `git://f0xx.org/ac/ac-ms-build` | Build worker API |
| S10 | ac-ms-ota | `git://f0xx.org/ac/ac-ms-ota` | Channel manifests |
| S11 | ac-ms-notifications | `git://f0xx.org/ac/ac-ms-notifications` | SMTP (post-alpha) |
| B0 | ac-be-hub | `git://f0xx.org/ac/ac-be-hub` | Landing |
| B1 | ac-be-issues | `git://f0xx.org/ac/ac-be-issues` | Issues UI |
| B2 | ac-be-tickets | `git://f0xx.org/ac/ac-be-tickets` | Tickets UI |
| B3 | ac-be-graphs | `git://f0xx.org/ac/ac-be-graphs` | Graphs UI |
| B4 | ac-be-remote-access | `git://f0xx.org/ac/ac-be-remote-access` | RA admin UI |
| B5 | ac-be-access | `git://f0xx.org/ac/ac-be-access` | RBAC admin UI |
| B6 | ac-be-builder | `git://f0xx.org/ac/ac-be-builder` | Builder UI |
| B7 | ac-be-auth | `git://f0xx.org/ac/ac-be-auth` | Login/register shell |
| F1 | ac-ms-sfu-signaling | `git://f0xx.org/ac/ac-ms-sfu-signaling` | Future F1 |
| F2 | ac-ms-media-transcode | `git://f0xx.org/ac/ac-ms-media-transcode` | Future VOD |
Third-party codecs remain **submodules of C0** (upstream URLs); Gitea mirrors under `ac/`.
---
## 7. Public URL map
**Base prefix (locked):** `https://apps.f0xx.org/app/androidcast_project`
| Today | Target URL |
|-------|------------|
| `…/crashes/?view=reports` | `…/issues/` |
| `…/crashes/?view=report&id=N` | `…/issues/N` |
| `…/crashes/?view=tickets` | `…/tickets/` |
| `…/crashes/?view=ticket&id=N` | `…/tickets/N` |
| `…/crashes/?view=rbac` | `…/access/` |
| `…/crashes/?view=remote_access` | `…/remote-access/` |
| `…/crashes/?view=short_links` | `…/short-links/` |
| `…/crashes/api/upload.php` | `…/issues/api/upload` |
| `…/graphs/` | `…/graphs/` (unchanged) |
| `…/build/` | `…/build/` (unchanged) |
| `…/` (hub) | `…/` (unchanged) |
| `…/git/` | `…/git/` (Gitea) |
| `/v0/ota/` | `/v0/ota/` (unchanged) |
**Cookie path:** remain `/app/androidcast_project` at **ac-platform-edge** until SSO tokens replace shared PHP session.
**Migration:** 301 redirects from old `?view=` URLs for ≥ one release; update `BackendEndpoints.java`, `CrashSettings.java`.
---
## 8. Dependency graphs
### 8.1 End-to-end — clients, edge, services
```mermaid
flowchart TB
subgraph clients [Clients]
AND[ac-mobile-android]
IOS[ac-mobile-ios]
HUB[ac-be-hub]
SS[ac-session-studio]
end
subgraph edge [Edge]
EDGE[ac-platform-edge]
end
subgraph core [Core MS — order matters]
ID[ac-ms-identity]
RB[ac-ms-rbac]
DV[ac-ms-devices]
end
subgraph domain [Domain MS]
IS[ac-ms-issues]
TK[ac-ms-tickets]
GR[ac-ms-graphs]
RA[ac-ms-remote-access]
UL[ac-ms-url-shortener]
BL[ac-ms-build]
OT[ac-ms-ota]
end
AND --> EDGE
IOS --> EDGE
HUB --> EDGE
SS -.->|offline files| AND
EDGE --> ID
EDGE --> IS & TK & GR & RA & UL & BL & OT & RB
ID --> RB
RB --> DV
IS --> DV & RB
TK --> RB
TK -.-> IS
GR --> ID
RA --> RB & DV
RA -.-> IS
UL --> RB
BL --> ID
OT --> BL
OT --> AND & IOS
```
### 8.2 Backend UI → microservice (requires)
```mermaid
flowchart LR
B7[ac-be-auth] --> S1[ac-ms-identity]
B5[ac-be-access] --> S2[ac-ms-rbac]
B1[ac-be-issues] --> S4[ac-ms-issues]
B2[ac-be-tickets] --> S5[ac-ms-tickets]
B3[ac-be-graphs] --> S6[ac-ms-graphs]
B4[ac-be-remote-access] --> S7[ac-ms-remote-access]
B6[ac-be-builder] --> S9[ac-ms-build]
B0[ac-be-hub] --> S1
B0 --> P2[ac-platform-web]
S4 --> S3[ac-ms-devices]
S4 --> S2
S5 --> S2
S7 --> S2 & S3
```
Every **B*** and **S*** also **requires** **P0** (ac-platform-php) and **P1** (ac-platform-db) via Composer — omitted from diagram for clarity.
### 8.3 Platform and deploy layer
```mermaid
flowchart TB
D0[ac-deploy] --> P4[ac-scripts]
D0 --> P3[ac-platform-edge]
D0 --> P1[ac-platform-db]
P3 -->|routes| S1 & S4 & S5 & S6 & S7 & S8 & S9 & S10
P2[ac-platform-web] -->|static| B0 & B1 & B2
S1 & S2 & S3 & S4 & S5 --> P0[ac-platform-php]
S6 & S7 & S8 & S9 & S10 --> P0
B0 & B1 & B2 & B3 & B4 & B5 & B6 & B7 --> P0
P1 --> DB[(MariaDB / managed DB)]
S11[ac-ms-notifications] -.->|mail| S1
```
### 8.4 Service dependency matrix
| Service | Requires (hard) | Requires (soft) | Required by |
|---------|-----------------|-----------------|-------------|
| ac-platform-db | — | — | All MS, migration jobs |
| ac-platform-php | ac-platform-db | — | All MS, all BE UI |
| ac-platform-web | — | — | ac-be-hub, all BE UI |
| ac-platform-edge | ac-deploy nginx templates | all MS upstream addrs | All public HTTP |
| ac-ms-identity | platform-db, platform-php | ac-ms-notifications | All MS, all BE UI, ac-ms-build |
| ac-ms-rbac | ac-ms-identity | — | devices, issues, tickets, RA, url-shortener |
| ac-ms-devices | ac-ms-rbac | ac-ms-identity | ac-ms-issues, ac-ms-remote-access |
| ac-ms-issues | ac-ms-rbac, ac-ms-devices | — | ac-be-issues, mobile upload |
| ac-ms-tickets | ac-ms-rbac | ac-ms-issues | ac-be-tickets |
| ac-ms-graphs | ac-ms-identity | — | ac-be-graphs, mobile |
| ac-ms-remote-access | ac-ms-rbac, ac-ms-devices | ac-ms-issues | ac-be-remote-access, mobile |
| ac-ms-url-shortener | ac-ms-rbac | — | hub/admin UI |
| ac-ms-build | ac-ms-identity | Gitea infra | ac-be-builder, ac-ms-ota |
| ac-ms-ota | ac-ms-build | C0, C1 SHAs | mobile OTA clients |
| ac-mobile-android | ac-ms-issues, ac-ms-graphs, ac-ms-remote-access APIs | ac-ms-ota | — |
### 8.5 Repo creation waves (Gitea)
```mermaid
flowchart LR
W1[Wave 1: docs scripts deploy session-studio url-shortener] --> W2[Wave 2: platform-php platform-db platform-web]
W2 --> W3[Wave 3: ms-identity]
W3 --> W4[Wave 4: ms-rbac ms-devices]
W4 --> W5[Wave 5: ms-issues ms-tickets + be UI]
W5 --> W6[Wave 6: ms-graphs ms-remote-access ms-build + UI]
W6 --> W7[Wave 7: platform-edge mobile-android ms-ota]
```
---
## 9. Monolith conversion steps
Ordered steps for converting the monolith. **Do not skip gates.** Each step lists repos created/moved, monolith paths affected, and verification.
### Step 1 — Gitea org and mirrors (no code move)
**Goal:** Org **`ac`** exists; legacy mirror preserved.
| Action | Detail |
|--------|--------|
| Create org | `ac` on f0xx.org Gitea |
| Mirror | `android_cast` → read-only; plan `ac-workspace` or retire later |
| Update docs | Pointer in INFRA.md, AGENTS.md |
**Verify:** `git ls-remote git://f0xx.org/ac/ac-docs` (after Step 2) ; legacy push still works.
**Requires:** nothing
**Enables:** all following steps
---
### Step 2 — Extract zero-coupling repos
**Goal:** Repos with no runtime dependency on PHP monolith.
| Repo | From monolith | CI |
|------|---------------|-----|
| ac-docs | `docs/` | PDF build |
| ac-scripts | `scripts/` | lint/smoke |
| ac-session-studio | `desktop/session-studio/` | unit tests |
| ac-ms-url-shortener | `backend/url-shortener/` | PHPUnit (change remote URL only if already split) |
| ac-deploy | `orchestration/` + nginx seeds | docker compose smoke |
**ac-deploy** submodules **ac-scripts** at pinned SHA.
**Verify:** Each repo builds independently; monolith submodule paths updated OR mirror dual-push during transition.
**Requires:** Step 1
**Enables:** Steps 34
---
### Step 3 — Platform libraries (foundation)
**Goal:** Shared code leaves monolith as Composer packages — **no MS split yet**.
| Repo | Extract from |
|------|--------------|
| ac-platform-db | `sql/`, migrations from crash_reporter + url-shortener |
| ac-platform-php | Auth*, Database, Rbac helpers, bootstrap patterns, `shared_session.php` |
| ac-platform-web | `crashes/assets/`, shared theme referenced by hub |
Monolith **requires** packages via Composer; behavior unchanged.
**Verify:** Monolith tests green; single login still works across hub/crashes/build.
**Requires:** Step 2
**Enables:** Step 4 (identity)
---
### Step 4 — Microservice template and OpenAPI baseline
**Goal:** Cookiecutter + contract layout for all MS.
| Repo | Action |
|------|--------|
| ac-ms-template | Copy url-shortener + platform-php wiring |
| ac-docs | Add `openapi/` stubs per service |
**Verify:** Scaffold new empty MS from template; deploy locally via ac-deploy.
**Requires:** Step 3
**Enables:** Step 5
---
### Step 5 — Edge proxy (routing shell)
**Goal:** **ac-platform-edge** nginx config routes paths; still proxies to **monolith** PHP until MS exist.
| Action | Detail |
|--------|--------|
| Deploy edge config | Map `/issues/` → monolith `?view=reports` internally (temporary) |
| Cookie | Single path `/app/androidcast_project` |
**Verify:** `curl -I` new paths return 200 via rewrite; old URLs still work.
**Requires:** Step 2 (ac-deploy)
**Enables:** Step 10 URL cutover
---
### Step 6 — Identity microservice (GATE)
**Goal:** **ac-ms-identity** owns users, login, register, 2FA, session issuance.
| Repo | From monolith |
|------|---------------|
| ac-ms-identity | Auth*, UserRepository, AuthTotp*, AuthRegistration*, … |
| ac-be-auth | login/register views (thin) |
Monolith/builder **call identity** for auth (HTTP or shared session bridge during transition).
**Verify:** Login at `/app/androidcast_project/` + builder; sessions valid; `./scripts/test_rbac_api.sh` still passes against bridged auth.
**Requires:** Steps 3, 4
**Enables:** Steps 7, 9, 11 — **nothing else splits before this**
---
### Step 7 — RBAC and devices microservices
**Goal:** Company scope and device registry are independent APIs.
| Repo | From monolith |
|------|---------------|
| ac-ms-rbac | Rbac*, RbacAdmin*, companies API |
| ac-ms-devices | DeviceRepository |
**Verify:** Upload still registers device; RBAC panel works via APIs.
**Requires:** Step 6
**Enables:** Steps 8, 9
---
### Step 8 — Issues and tickets (domain core)
**Goal:** Split POs primary URL surfaces.
| Repo | From monolith |
|------|---------------|
| ac-ms-issues | upload, Report*, Tag*, reports API |
| ac-ms-tickets | Ticket*, attachments |
| ac-be-issues | reports views + JS |
| ac-be-tickets | tickets views + JS |
**Edge** routes `/issues/` → S4/B1, `/tickets/` → S5/B2.
**Verify:** Mobile upload to new URL; web issue/ticket lists; E2E ticket workflow.
**Requires:** Step 7
**Enables:** Step 9 (RA links to issues)
---
### Step 9 — Graphs and remote access
| Repo | From monolith |
|------|---------------|
| ac-ms-graphs | GraphRepository, graph_* API |
| ac-ms-remote-access | RemoteAccess*, WireGuard*, Rssh* |
| ac-be-graphs | graphs UI |
| ac-be-remote-access | remote_access view |
**Verify:** `BackendEndpoints` graph URL; RA E2E (`ra_e2e_cli.sh`); mobile RA API.
**Requires:** Steps 7, 8 (RA → devices, issue links)
**Enables:** Step 11
---
### Step 10 — Hub, access UI, URL migration
| Repo | Action |
|------|--------|
| ac-be-hub | Extract app_hub; use **ac-platform-web** assets |
| ac-be-access | RBAC UI → calls ac-ms-rbac |
| ac-platform-edge | Enable **301** from `/crashes/?view=*` to new paths |
**Verify:** Hub cards link to `/issues/`, `/tickets/`; no broken CSS.
**Requires:** Steps 5, 8, 9
**Enables:** Step 11
---
### Step 11 — Build and OTA pipeline
| Repo | From monolith |
|------|---------------|
| ac-ms-build | BuildRunner, build APIs |
| ac-be-builder | build_console UI |
| ac-ms-ota | OTA manifest assembly |
| ac-mobile-android | `app/`, `ndk/`, gradle (extract from monolith) |
**Verify:** Builder triggers APK; OTA channel publishes; manifest lists android (+ ios when C1 exists).
**Requires:** Step 6 (identity for builder users)
**Enables:** Step 12
---
### Step 12 — Optional workspace and monolith retirement
| Action | Detail |
|--------|--------|
| ac-workspace | Publish optional `.gitmodules` manifest |
| Retire | Stop commits to `android_cast` monolith; archive mirror |
| Gitea migrate | Update scripts for org `ac` ([gitea README](../../examples/crash_reporter/backend/scripts/gitea/README.md)) |
**Verify:** Fresh clone via ac-workspace or individual repos; prod deploy from ac-deploy only.
**Requires:** Steps 111 complete for services in scope
**Enables:** cloud VM scaling, future F1/F2 MS
---
### Step 13 — Notifications and cloud hardening (post-alpha)
| Repo | Action |
|------|--------|
| ac-ms-notifications | Extract AuthMailer; wire 2.x mail when unblocked |
| ac-deploy | Managed DB DSN templates; cluster0 → cloud VM roles |
**Requires:** Step 6; mail infra (developer)
**Enables:** full 2FA mail E2E
---
### Conversion step summary (quick reference)
| Step | Name | Gate |
|------|------|------|
| 1 | Gitea org `ac` | — |
| 2 | docs, scripts, deploy, session-studio, url-shortener | — |
| 3 | platform-php, platform-db, platform-web | — |
| 4 | ms-template, OpenAPI | — |
| 5 | platform-edge (shell) | — |
| 6 | **ms-identity**, be-auth | **GATE** |
| 7 | ms-rbac, ms-devices | after 6 |
| 8 | ms-issues, ms-tickets, be-issues, be-tickets | after 7 |
| 9 | ms-graphs, ms-remote-access, be UI | after 78 |
| 10 | be-hub, be-access, URL 301 | after 89 |
| 11 | ms-build, ms-ota, mobile-android | after 6 |
| 12 | ac-workspace, retire monolith | after 111 |
| 13 | notifications, cloud | post-alpha |
---
## 10. Microservice vs monolith comparison
| Criterion | Full microservice (PO choice) | Modular monolith |
|-----------|------------------------------|------------------|
| Independent deploy | Yes — per MS/UI repo | Single PHP deploy |
| Failure isolation | Stronger | Shared FPM pool |
| Auth complexity | Needs identity MS + edge early | Works today |
| URL clarity | `/issues/`, `/tickets/` | `?view=` parameters |
| Ops load | More nginx, repos, logs | One sync path ([INFRA.md](../INFRA.md)) |
| Alpha risk | High if before Step 6 | Lower |
| Cloud mapping | 1:1 to VM roles | Lift entire tree |
| Rollback | Revert one repo SHA | Revert one commit |
PO choice is approved with **Step 6 gate** enforced in [§9](#9-monolith-conversion-steps).
---
## 11. Infrastructure and cloud
| Stage | Pattern |
|-------|---------|
| **Prod today** | FE TLS → BE :80 nginx + PHP-FPM + MariaDB |
| **cluster0 lab** | cast0103; NFS config only; GTID DB ([ARCHITECTURE.md](../../orchestration/sim/cluster0/ARCHITECTURE.md)) |
| **Cloud phase A** | 46 VMs: be-web, be-data, be-ops, be-build ([scaling §6.3](../specs/20100612_1_scaling.md)) |
| **Cloud phase B** | Managed DB; DSN in secrets (not in git) |
| **Cloud phase C** | K8s optional — not required at R0 |
**Inter-service calls:** HTTP on private VLAN / localhost initially; OpenAPI in ac-docs. No service mesh at R0.
**ac-deploy** ships Ansible/docker-compose per VM role; submodules **ac-scripts**.
---
## 12. ac-workspace and OTA versioning
**ac-workspace** — optional; lists repo SHAs for a release train. **Not** a unified Gradle/PHP build.
**ac-ms-ota** manifest (same product version, per-platform SHAs):
```json
{
"channel": "staging",
"version": "00.02.00.00",
"artifacts": {
"android": { "repo": "ac-mobile-android", "git_sha": "abc123" },
"ios": { "repo": "ac-mobile-ios", "git_sha": "def456" }
}
}
```
---
## 13. Risks and mitigations
| Risk | Mitigation |
|------|------------|
| Auth split breaks all consoles | Step 6 gate; session bridge during transition |
| Hub broken CSS | ac-platform-web before ac-be-hub (Step 3 before 10) |
| Mobile upload URL change | Parallel endpoints + `BackendEndpoints` normalization |
| DB schema drift | ac-platform-db single migration stream |
| Too many repos to bump | Composer semver for P0; not @common submodule diamond |
| Alpha blocked | Monolith stays prod until Step 8+ complete per service |
| PO BE learning curve | ac-ms-template, ac-deploy, OpenAPI, localhost HTTP |
---
## 14. Changelog
| Rev | Date | Change |
|-----|------|--------|
| R0 | 2026-06-18 | Initial DR from draft; PO locked org `ac`, full MS, URL prefix, identity gate; §9 conversion steps; dependency graphs |

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -42,7 +42,7 @@ endobj
endobj endobj
7 0 obj 7 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220511+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220511+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174842+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174842+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Grafana vs Others GraphVis Pivot) /Trapped /False /Subject (\(unspecified\)) /Title (Grafana vs Others GraphVis Pivot) /Trapped /False
>> >>
endobj endobj
@@ -81,7 +81,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<10a588f5438fd4e86531a2b28eee10c1><10a588f5438fd4e86531a2b28eee10c1>] [<bc6184b32d1f12a1bd28db177f8e8b1d><bc6184b32d1f12a1bd28db177f8e8b1d>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 7 0 R /Info 7 0 R

View File

@@ -183,7 +183,7 @@ endobj
endobj endobj
30 0 obj 30 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220500+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220500+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174833+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174833+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 production infrastructure) /Trapped /False
>> >>
endobj endobj
@@ -376,7 +376,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<535797484fc3ea7f6c0c9067cdb5b8d8><535797484fc3ea7f6c0c9067cdb5b8d8>] [<62e6c98798332c0d70f94c87e8f28c95><62e6c98798332c0d70f94c87e8f28c95>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 30 0 R /Info 30 0 R

View File

@@ -142,7 +142,7 @@ endobj
endobj endobj
23 0 obj 23 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220500+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220500+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174833+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174833+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False /Subject (\(unspecified\)) /Title (Crash console \204 open ingest API \(design\)) /Trapped /False
>> >>
endobj endobj
@@ -289,7 +289,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<f785af38a27e85b78bc868cfd8895ae6><f785af38a27e85b78bc868cfd8895ae6>] [<40b0d4915320fbf245576f9c86c8a968><40b0d4915320fbf245576f9c86c8a968>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R /Info 23 0 R

View File

@@ -152,7 +152,7 @@ endobj
endobj endobj
24 0 obj 24 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174833+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174833+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Open tasks \204 dependency graph) /Trapped /False /Subject (\(unspecified\)) /Title (Open tasks \204 dependency graph) /Trapped /False
>> >>
endobj endobj
@@ -294,7 +294,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<6fade7d28070a04635a01aac9213cf6d><6fade7d28070a04635a01aac9213cf6d>] [<4cca413c1249fe7757d7ab6f2c356e7a><4cca413c1249fe7757d7ab6f2c356e7a>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 24 0 R /Info 24 0 R

View File

@@ -163,7 +163,7 @@ endobj
endobj endobj
27 0 obj 27 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Opus / Speex / stream protection \204 validation checklist \(task 3\)) /Trapped /False /Subject (\(unspecified\)) /Title (Opus / Speex / stream protection \204 validation checklist \(task 3\)) /Trapped /False
>> >>
endobj endobj
@@ -333,7 +333,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<09eae2d38beac43ddd775c1a746bf63e><09eae2d38beac43ddd775c1a746bf63e>] [<57e4e0aca5ee192ac24df0f4d343b97f><57e4e0aca5ee192ac24df0f4d343b97f>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 27 0 R /Info 27 0 R

View File

@@ -142,7 +142,7 @@ endobj
endobj endobj
23 0 obj 23 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False /Subject (\(unspecified\)) /Title (Over-the-air \(OTA\) updates \204 deployment schema **v0**) /Trapped /False
>> >>
endobj endobj
@@ -290,7 +290,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<82cd7398c6c125d2dd7bfdcdc3eec6fa><82cd7398c6c125d2dd7bfdcdc3eec6fa>] [<676f27593046572b8cd4c389e7e4f7aa><676f27593046572b8cd4c389e7e4f7aa>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 23 0 R /Info 23 0 R

View File

@@ -127,7 +127,7 @@ endobj
endobj endobj
20 0 obj 20 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False /Subject (\(unspecified\)) /Title (Pro / OSS split \(cast-core + cast-pro AAR\)) /Trapped /False
>> >>
endobj endobj
@@ -259,7 +259,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<25f000128476a42bea3f6be0b0c8670f><25f000128476a42bea3f6be0b0c8670f>] [<bf3391a6afce2847368495673b246a93><bf3391a6afce2847368495673b246a93>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R /Info 20 0 R

View File

@@ -89,6 +89,7 @@ Click a link to open a guide. Section links jump within the document (GitHub, Cu
| [20100612_1_scaling.md](DRs/20100612_1_scaling.md) | | | [20100612_1_scaling.md](DRs/20100612_1_scaling.md) | |
| [20260616_rssh_routed_egress.md](DRs/20260616_rssh_routed_egress.md) | RSSH routed egress (postponed). | | [20260616_rssh_routed_egress.md](DRs/20260616_rssh_routed_egress.md) | RSSH routed egress (postponed). |
| [20260617_codec2_ulbw_voice.md](DRs/20260617_codec2_ulbw_voice.md) | Codec2 ULBW voice-only cast + SFU negotiation hooks. | | [20260617_codec2_ulbw_voice.md](DRs/20260617_codec2_ulbw_voice.md) | Codec2 ULBW voice-only cast + SFU negotiation hooks. |
| [20260618_repos_reorganizing.md](DRs/20260618_repos_reorganizing.md) | Monolith → Gitea org `ac` full microservice split + migration steps. |
--- ---

View File

@@ -153,7 +153,7 @@ endobj
endobj endobj
25 0 obj 25 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 documentation index) /Trapped /False
>> >>
endobj endobj
@@ -253,17 +253,17 @@ GarnQ4`>s,'\rL!2j,Y2Gf7VIg8H)\3`Pd>,m?5I#W;ibrdKEq;-TkXak$(>48Fnu%%uk//)XH<EDN5A
endobj endobj
43 0 obj 43 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3437 /Filter [ /ASCII85Decode /FlateDecode ] /Length 3497
>> >>
stream stream
Gb!l!D0)I3')nJ0d@[RRG&;iLX7mQ)8B*S[lkjBt[OP8k_6V;q#*p&Q\R0h<#hUD4%-7+sX0,@4*k>kTS(.DD=p.7k,PhAq%D^X::+lnb$M,PT=cd'@%HdTLcP,k1;cX$$=%!o_9bTp/,ie?TN'AT<>(j\="-i?uT3frn]Zd?!`%1Unpc^5q2qtPZ9g6Jm&"*C6\_Q0$Ng$L'qEt@PdQL'OW)H9M5`eI8W9GF<$6@4kf0'H0B9L,#*!qj+'o#,.R)/>0>a,2QF!U]sF+ep,);]h.^S8m@Q-jR,=/Mb?d?U]6p/tO^Nj#OAI9)ZAj"!cCT9\Y\%oTZn#Kc2>#Dq<XK1%lj6)+#A+H__3O@Xd^8B>c]'![K&8&lXKp$nZ#gjMd:mn#*k?SC:o].T/P'XU!PdW^7C!_F/cQrcc&HBb6gaAsgMp]@'??DHf%TLpL[W1r!]E@]d[SG;3jgAAYJ\YDG4WjQ5+p/t9*_ilQJHY.^_B9m79ee^E*_^J1u5I$D=,%7fkTl>5DP@1n.)^ea\lS`1o^i<YG,k1<fqpd,fEk:mj#0(c_n/3^LKS-"l$q0Mt!Gt@\N?JYhq85r%LjXtQQEH!95C@4DIWs?ik6B*`IWFG%(TcOr[):$Zdb%.)0K;LXckMK^XW?&)8NL*<>P@Qsd/YBTB`QsIY,>V8HTE*AB&i^>[+_Kb147D\<5K<NN_XJ^,\8\ke-fb>I=]I`Jt,[afI'X&'Z^BQl:?.hUBK94Oi7D.T)ocU""7su3U+-.*)tO:S)XnfK/rfe4uY&4\#AZtbLgN?P9*uT9Lr9uoiq\?-Q9eOn\%B*$Qr$31ddh9bBpT6gNr2mc*N:uQQs(W8oNOfq91[5m#1b1WI83Hh-[S=]0"'V)5*a%a-\k+=Cdm,P)-Z.HE"9D?::k7g.n3aN/"&L&lF8V'-?RS$s.7B\hsN?8KC$#!Ua\3T%G:CNs@eUTkiI8p1ljAa@./-lB$gK?5E@Z5;S-Yl=uL[YdZDUO2^_N\R`3iGtsA;q_oJ0SH4SV$ARM[3Ih9;3glTu7WU&Y^!s?iM=J!EDV[<TlS))mg:2h^FFJbPB'p0O@lBD.W7<6Mo4+P9<9gR?2uE%"\/rtG\*m-be$?uWgNIn/bRR*Vnhs)ZhgblG-ZBsD4)gNBq=Ka-_tX]8Fg/Y0^.g:D]?K%#M!d^UpB&q4<71seeUuo\q"b'Pd.DYTHTQA^o0?^X"'%>$1Pl-V`O#)FD@GGXo6d^=)[8li?t@(t5C@0COGl6\T_$:MNB''B)qV4AlbVP3D4)J8/0:sa'Vght8Dk3%`Y?j?[ITem7eG!Qk'Ub`OIV`fa_Yk?g1f*.M\1WYb9kFe\Ah:LL(&Lj/3%7!+p:i*%f/"a1oEp>F[f&+j10S`4o*r0/aXB?H?]#,@Q\gL`c:Ju/s9R]d3aWDDo'jN]U9P>kOd#pLX-&bW'588N5Y$9?$ag6KJUYuL#.e9;EEj+X-msAg`#lEZ,:[CR*A+9'!8WL@Ro959\Yu<45Y+4al*GEAdJuC=0])R!XI&RlQn[$DG].YA`_2[ApeqcCR7\@/3]'27HWa2+N4Zn&]Dk2E.oGq%,oVc4g45PjFJdqh>P=eJ9`$M1Rsj8LbO`1nd$qtiC6A$os-R\8=`&uA89uTLi:ubVYg5Qq2r]_bqSr;+*95m(B[3<cWN!<.#nY;=VbLjp=s9Q88!mFX#Du=YEk0;IJTe2RQ^F6$pF&)58'Q7Gg[&VQXYSWI(3T>(6QX(SCe_@9W%QMD,!ar?hQEH*#HW-/_'"%/BX,S.q7i_KD,8_Zn)HdjA/p`ohB2hTN78ZUX#eFdsN[bHEE,m4$gZ0n=>n"949=\YeUXZ-#QoASMtca3M+4V&_1&PIb2r>S),%59P5$n0\YXGm]3seNJ;>,&H<I8J"sm]fjP3MHR\H2SLNh5B:b0gE7LbQg)8;^baZ`0[#K,Xk:Ct-1Mqo<\hj3N`U#'qiWX-)Galdp9mRc_.\4CL?(m:VXuj720?BaDr-AcCk+rKO.^$H-Eqlb1PAYDR`&*r#(H9@@Pq(Tk+/;d*.]"PbA&Non,t`/MKU&DNGaspuSLfd5s#u/,O62@qY!U@Qk?lKEATq"Z#8[moD46^Na8iBiG7LXAVN/:6qgCG+nVZ_[N+GLe?!$IgesZKuib!#!)qMYJK^69N$LbgMWVIKPB&Gd30uL"(2+%D!.H>pkZb-qs*5sbGYq,Qedp"@[D^P1C0t$B"HR#M$DWf4NdQd9:5>D?PC9g1amH<j$6r;g@lDJNCD%&W#_9,@/5!c8u/ac?gRrVm[hN-J?iNOM'V+/=173]*._I6$q%FmTS+''XmXc[R)OYj%,_I5l2CVo[O7?Y%s$&96eP%u<_<[:j).j%e;Z"=G//4mL*\OYYsMPSk*_1rasX]^8e/!<f:"s6kk\Rl:7X@Q76)LPYL8Bi'#=HUOj2>FC$NG?HMm;/KH9VLi)U"0XGmd-Urbb4=(+OuDnmHo`+L=igth#O;NP`CV'`Jm\'b5H_^k4hs&-"JbD1lZ1\6hkPN.ZC1@F@hflHH51a#[L_Sl+)*93W@l%P0&a3Z0)mPe(/66bgj%@g3r@L2GUks.4PrB1BXm.4,\MKiKqEKUZ,$?Z2K[/rTkFP*`]*dmj);'G5hK8-kVd)o<T"JX#Kc<]^eRgSjT7trM,rBTf0.hf?"X)?^Nb_Kq%5+3GM<Xs7N<JC(=F1KG2n1Z90g,'m*#J%o3buC-otFFckRJ5g`Il*k!arm;Y0"\,P9VP-h7(HH*kjCO\/^Ft^Zq'G%g^X2i+?%PII!1N*;)l$<>dT<5=->_Rh.%^!6"48*:EIh_\7A!)5..'HKI4&1sH*J[e?(,fC9=>0VQ'q/]^JV"IJN6p?BX$gC3Na,$HD;B!9)_RLNmEC'-ZF;!S^NL?8k?I&<%GY<_I>=?;0F[lGr-Aa"c/2n3C_`gRERh9'_]M+V=DjbP%O1;[6rqS0Gp<,)!;9J+%]2E>8V2Q)mpKb&OsjuV4e*J58?X@^9RUP,]U@ci#<1"iM'a<RU8&6]$dhU",Q>YGRk]CJPb`<]jprX1KkPIIfRg;!"=c:B&\uI^$OQSDf(39%nh54mLhI]kD5e^C?CId)B%95J3^7m3#g_rA(=m/p=7X8l((r+VSn34>pnk<'#eJWE0:VEaGSQp;FL([=q\>*A67@;J^D4prr'DHrKShEd$&%CfmCSKeg\:M:N$o23,KU]@7JcVX]@4Dl\?`o1AT!+5:3U-%k'j\oVB;q,0Dr<h.?r*F2DuOPDSi7RIFP7.le+UuNT[Vn`Pbat!oqP%E>uG",4EY(dC09H*ZOO+j/eJ-*.>m2jjUEM!6+9*S:C>qX(I4,%3sHD#dXA:kL<gV(#i4(XU:K13)T>=qXF*I8:i0%U8dYUEsHu3SsZ0W1%1]MMPLPHa$PM1JjP9bMtor\]r]f[^r_'+^['pJ~>endstream Gb!#^>BAN>&q801kbhA$0$'L/Q+Y4@G1`14!@MN\>c!V&!87^8eHO_)lVERP#t0_i3N:`,@PY;I4l&9mp"(ue5f=1)s#OA7b6;-U'-Iu).j2,?_r;HU`OinBOk1sl78S+qPX3bT6'MR/^5$0FOVE3U!fJX,b$VlmVJ1CB8js$;nD<Xs=9_O+AUYVIJAkm"pl0("_p:W4Wal&s!L_@\7=qI:8Wc9"#-+Wd+D;*&a9;BgkMp?@_M:)^.%r-_M$+uSeSj(fs5_KsqHM8OBnnI6IckWHV7pSa>l1,W?r5T6e^Z98CJoT,Wn=.\=f_!oP.U?[lQ\s>/eK@]q?R_I9*C)T+$lWCM'.rH,=nU_&L=/9Jjer<_*NJL[4^uJlbfWBM4W4)q/qR#Fm)`)V4aY]97HqF#p`Zt0J>rA1<NK)Z:,9+hSK)i=&_<jW`5^E.]T%#T-rBq*PS'9IJc:T*>S9EXas)bXHJGH:T?sfRJGC'btVt;s((N6?l)5>(KjTn,%8*"?uUJ['>M:m>MELokdEe$^ig\57I]gGgW]n^jC74s%#jH7i<'dH'$^sja$A^G*mjcUg?@R)>d(mO*`6k5:?"utjTSH)b\s]=jP!uVKMQd$FT")pNdb*B;&n;p)u(4/,'8P+m5?k=P:5j;lof^j;!@tBFEE%N)7S'jL,GN]-F;*/ABp[4SLaV8cQK,5>+(q?m3lQi#1O)K2h$=KBD\[&@USZPCf);cPc=iE)4fb7gIE%Q+F*H[)"f$Caj,o*eaB*AhUrp9\/<HCf?qs8><#&'f3a5T,G0))H>,^a_S,7hf\3qYltt'SVR$8sfT$WPH%(1&5%Ar+4e[5RU&@l!];B\YID2):`n7l6+&B!hoCDmXRGI6f?I&TRNAcO8hSM&$mNC4<Vp[2_G-d*_76m?1dooUU'LX-;Y^"%(rJY^7Q?cRWE9u6NJi2Jff&2BJ_YpFRYmj(d,b\:sq*R4(^8Ua^GSF_Op\^=Mj?[":Ul2pS)`i)40$b9MM9Bs\aOi_Y#BpA?mb7+7X&T[rCh4u#>`%TB;EL:XbpUNoB"#,!0]*"A%(F4mL][H83#h"-pCOYkNh]V/i$aVN`_i_CEF5i[HfK:VpsZfA,L-%32#5SDc<T%*PU<:VD0uNLDf"<>SF=7!WoX,.Z7k5_c>.od.)(`RSfQt]3MO,Ik.*[V%Ou20Most&5J"Np*1G_ZrbQ1aHQ>ZD^7u!%>a#H'CS$M5$_mrfM<f.CS\U9f,U5sjo7E;Y"3I2n%iS3_@rjOJ)7PULl_*3om?ndW=P.!80qsiEN8Ob,`Y@E?^%SHn7eG9]AiMT.8C#HDA@=1)c)bZ``UF!0AK)m#Y+ZkQ+Ml^_9?rrK4![ac2TF6R<i8N`[:W)kUP3i=48IIO/?OpTrNPoH6X4dR`d.&(09T[fd5IgrVe##n:*0#[F+!Q*_hT9c<0,]E,4R"9=7.krE+40te@X-t8o$V31Gc1n08?lqX<?@BB=!](lo'2LZ?GY"LmtrX^"sp\jBr@pZA=.^9k^_Hho9g)\.MN=>;Wh;=Lg;G)1B&e29Vg:3hM/C13$2m&7Yls%-$@ng`RjI!F2+>jH\b,*iPg!_k6l;R$SDViR7)/%^R!-%j-oD-UJMa<n.a([3f1CWVOM"IacLqmJA!&9-/YQ@IciM(h-5nH4Yk7]k2dCNWsB'&n6$-SSIaeCQ=UI[kQs:k8$hdl)/-*G0mR*(jS4>*qA%'PHOAeA.osQ@E_%h,3p@HCs?uIe,-[5T:4nlXfof*Y)GeJWmO8g/r;4C43!j([-t2qGr^C$J'>3tDc60l[An$`Zt<=*jr52hWFm3XmJG*0c[H1`)"qDL$U!<PiM=HG3PJ]KE%NDtB\!C9]nQmKo[^*U3h'6+d4G=+n(kfre9:tip_T%k*s2_G%SJe-NIgQa9[RafF?QFRlRLB5er43QN[G4`158oVc)oB<g5J,$=7j`HfhGq29@sns[lY*JBfui'>*GA[5qQ/DHZcg#T01$\Fp(\%`XIjL[_6']o3Ib6gZ6qEBf7]K`l>BJ.IDI(@8PB%,qGQSjFHJA9"%BGQ)<lZ>`r-Y/.@[@4pBp>lA_RIK34(eT?Y@1C1Cu`/M9rT36KF,*o(/iCc"#0D.f'(pL.RV/%32+QIO9m[bC35Qe&(8n]2Ueo0l"Rf6XH-d=\J-o-83c?pE<f_l=>$_ZBC>`X*r'H/"B>Ml@+p>O>dq?a.ls:TPLF3k&GiNMM<>SL_lOJ)J)Vh?c6nA;.h`p[%%5TYOKGT"7')A+6?#o2HtV$cLK>imEBKO.>HjT>r"2YZZd\;A*EE&.r(Y@5A_i"4\V.D\aO&/W$]7Lbu(V_Iarh97uEP66p=m&N1#Z,aUpO/4?l+<o,Aq@`]S%R>^h@k)kHY$^"cd#&b8$[H%eR/4<+P)n]=0jH;HHZOf\SDt%?^+sh0sAg%d[hVcX`1ak@oE:AQ^EeS]+KN`DLHLQPbEeAE%JlHcBF>P5b'B7(8AI%;i;`_u8'R)mkaQN0Nampsb9?:WdBH#8.MsuK,<+-o?QV4o[b?J;^5jIXE/([AA:]5\L8L=[2Fk85EV+7GC10;M[e+S4im(mE5/4DuO@V-aKA0mp^7=8#NF&^dA.5p&q\t@eFTqaRr8l$)(8/qQ<;%s<enbH;n_Y)#7&?l"oWMe7Ek5;0Q:T-^`aK\[`rsSG\3Ol'6RY0n$q0to:5rE=I&jE7+Q0"D=<'cj]p&"od>Wg@G[]\'W'$-(IlX>]LHDIME\)+iK;SoQFDk#87f%]ppb'1e\KZq##;/_'MaXs0XK?QfMB1L$ZhtPG`B)@2OV]`=Z\h[fn:q8][nGJLnM)RSR]O["ui]?;"gMGQo=uA.F02Xf?UEZ9^r%:FuCDimN@2*R[0M#Z][+IFXP3NjKWO)Eu'4ctC;hXT$M.[Y"XDYMQj0]&b=\c23%^cr<n]BJiQm.#WNB0c+_#<Z^2b<W6"5eGspV>_]oV.)dej8Y7q[o;?pg/@og<j<a$5kl>s'hS/IJ9A2du(Tq+61-I4?)-(4)RU`-32)0H9Pn%Vajji`:IC&3an,6RqrW*GPlG$jqhIYmS,o(B+8M>SP%I04SLaLpc"O6Y@BXe5U[?N"W>BaIR/f;BG*JS:l)7g&%!T)QS88-bB;**fRjg%S5Z.lE)@"jF"ILn_m,XB^UP#t@H+*7;>@SA[X[$sYH/+2Wk,0\acU0[i3QQ^o;0NXi!Y0N)WZ[+=UM_u0.tN/+KmbS92FRZ+s3bbTp&g%%j)^K:st@ddqOI_f.4.X*o2UmY<ZX9.g1i$8%"u^?ZnPq%>l7+m)AlR,o[B@o@XY6"scf2")9iY?8#0OpL/5R^oschCX`SO5'[K`e+2;P_?1%.W=+aS)e=Dr^(m;"\J:e#pFNic.HFP?ReT\uE#2Q;K%ck=2T[b-[][ZF(qDrH#`OtLq^1?$PiHbkN,>GRA&Fl-K.90N$.2^3rB:eSe4.C3=/LA@>epS9FMqUf#RWl)j9'\2@!L1a*dIEDKPUJ>~>endstream
endobj endobj
44 0 obj 44 0 obj
<< <<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1402 /Filter [ /ASCII85Decode /FlateDecode ] /Length 1487
>> >>
stream stream
GatUsCJWtM'SaBKY;jsD=.NVfj4rTtBN(k,d#b'INeCqs`3`\$3+G&+iSUgZg<a8c#ZMOlqNCI2of6q##^#l.c0oHPE0`b=KE)mOg]Oi=$^(f!aep5Qct9+cCQ<&n(:YF6FS!G6qrG->Lt[3$RLR@GOJd9r7g>K^Jbs%c#<%@o=dhL815E6:p_QL#L#am%k$!6-KlUGCDF8:0Z5Z^D)c&a7^=U`tC1%+V>NE@LM#@#L7mT+oOs&1R3SYO-$Q2V5!pXMle%>d<mI!+-LH)/>E/"70g_(=_S8\LL#]>!uL?6]!(c?:0V#unB*%FZ<*deE[(e7MSGj0u8R4.,6#nDI&Gp,`4J]2^@Q01:#8,ed7f,R_o+H5>@9<6%Ci4(8f<4$Vh0Sa8E".<TdW3d@SbL(o)-Yl(]k_>io\fl.Y-A^X]a"j8:7OuX1--*Z)X]"lFnhIGEJj>c9K_*I17V&WP$.fW)n'@nihs$AfIAjHa+rThg3mr-U+`-/,/MUU/-:g^1b<:b1d#LF4#(nLT&1=$4;R[-1b$jp[fO<:pbis.BEhF-riC>KcX.Ud-6LK*pr^?![C*B?tSI%i`Z"5bYH?TYY*,I'Vl/cIf%EACC'dh0coO7*A*<>GY%4$n0f_;"4<U'ST]1fbHd@iN?lX8J7`FnsTGWEbG5>GAl2'QUnPK/8'2sJ+e*c)Pi%DJG0PB$.6?i)emicmT!C74u=+QXe!1U84$Gj,AP!0)W%i2O)JRirS,H"u8`H1rCedip"gl(FGqg#VK=1c'/^#<m6Kktf3"N2?+Ks54iqj3USF"e8UNn&H.d59V]^a;-%J=;JPBSfsn$B6"a@h?C?7dOocAr!4`Z_q<PSqS%=Ir`l#V*Mn>qq.=pl`cc1CBOgGY8Uk"ms'A729;TapK)?>RA]'?aOr)e1'I?SA'6`)\_7!ZshR@PHBe[7>7K.A?+CBgq3e*A@>Z,70pIFd[TXOMg4l&3QN,<I_K6tb*=#*t?=is/LJ$prpj0BO@S<"=GRS:K(e9=U;iW<5q,TY[o2U*8]hAP2"+A)CHZ$I'"pI064B=%,=Lke8UR=:Jj7seFEU[F,^&J1(=*?N'O5H\E_.LJl33V_dR<>$f*.pA1C:Qu&V3ec0Ce()jHAS??q]AT5kGaV/:q'2AD8>_"L-Yg2hk3W]QFq)A8;k4(CMt2Edc&(_M0C$1KTOs_"@kJgh7-]rt$:lH]AR'j^q<#r#X8tf5FJ\tc;)Fn0^T<h%mn8+6ZO+[K+4Oj,(U0\KD&)gjA40J[.M[Qr@ER#"dK=@86b;P](Y!Oq^$8HGBP@Oo=A]C]T<#s_($%?P)*\lne#LGqM"^D,RS7p@Rie)7+lk]Ro(p;S559usD#RMl+gk$G6_<UplMhIe)X<n=F``)KJ5?1I2CFqW~>endstream Gatn&D/\Dn&BE]&;iJ)p'78>c395pEk<dmsV'Si&pK":.V29b.+jSf=\%PReP8$p'9@\,E+UH_\kEgJhHQ]!AMq"B`$K*ammMQ%75VrRPC*l>Yn=l[r7qAsSZo$OFF;#YGeZ<*7`;=LWYG]A;#^=oR`E`nsJnS0Ql.0)(e\E"_\&<9:Nr9?cXYRCD$jUBrZ%!W:70YN>(FOSgKA4\R@6/L5/eX6p0A5a5NtD7!d2s+R288dH3Nf7Z%qKZ>Tq<::0tdY1PPYg<78fG4:aTr)$HY&IOEO0:E1]CO>?UEd):5PkB!CBOc=rHu1\;#XPV#RsqOqmH">mcI"4*SBOV]hq84=^=YV)8)a]`ZUoL#B-n\(ujqP/cf0BPqX^nFI:VDV%)cmfBuJ#I.WdGV>IKlJf?_b98RA1GV:.1(L@H*18tJjo,MlCRTtT$]&,5KK4m"VWY_MkjT?j>&@Z#HeuTH<<KspbnPrh_"3.5[06QloA3a1.+!:h:<%ZTc5Wj]f<rX.KqK99U7H"ld[HsNt%^h/$?,Ie!*G7M/9sK2!Dbuc.i'mksF,mfldlYIp''VFq(#+\kP`NMq;DUb68]>'hgI>dpNU/."o;nfYVuDe2Aama#Pt*LV^T2LI's5ad3Mh!u0>g^9+ZSEaHgfjAp2KU9kLi?gqm&W`nghStWo'(c2bhPelGOdL-7(3c5q$A.lI-U4*IMj'VoXK1Sc$Z`'T?!.SM$/b:m>8Ih&GBm:P'$nk-@J;-q=;2q4fE:QE%8kiKnkQ_J!]nrqjF?GGp`n,IlH=8e%>W#4I#$A-$MNk5FTjDKMgON?)?mU%IX\-io'A%ME`9ZO@5Oej%;q@$?l5>tL:VT.XK/^6sEt9o%M*DGZ<$(M3p]g[VUsZ=I-fnNT_ih:ni#*T3$^TX&F4hb`/D&Akh/k-Rr<k0O=n&0\$)rFa@pAi\n(l(6GnnRccBV@R1:;Ys\XLpug?0j^FZ7^96G6#gA.$;i.m.Vn;oa.!^*,.[Mi<Kg/JnoZ[HDs"f80^,X0^`pf2[&00)>DW7C_8P>OS.l;c)<$q;ggYIl"OdWWP%0.]jCIcdG4]2Yq5%kL\O%BQ-.mij81991++TYSJ#tek=R\XY&Acn^oqJpG@[=H%/__`;(r[5JcT+1`jY(cQ>rs,15qP<p[>Ej)GK$V:sZ7@J9(qpL1j0%Kc5AkdbmPKp<lUSep$,U;4'8k'L:XdE/G#B\K7bnBNf%9TTD7fMk3p)753uC"f#fhq6[@&@/\./7=k^pCc&?<Lr^"qQ;n^$d$AE<0'\,IS$gPW(Jm!U^!=Zc0A0-!PsY(<IK?o1`nooXI/G%97C'N0dfg/ejTr]0N!\8*r*tr%'+E6.)dr+&]lQf6+6$/-g">D<-Ld%nkgu`dDQ!l+to5JcE"r9;=#RgWB=$?e]PoSM"^Dl)Q(-Wbn&&R$#sd+H$s.:*k\]0Rm6U.UfPghM!MQY=IM[7>>o`^oV;?e"s!RHrh&0D~>endstream
endobj endobj
xref xref
0 45 0 45
@@ -311,11 +311,11 @@ xref
0000007001 00000 n 0000007001 00000 n
0000007229 00000 n 0000007229 00000 n
0000007642 00000 n 0000007642 00000 n
0000011171 00000 n 0000011231 00000 n
trailer trailer
<< <<
/ID /ID
[<941c3b462cefa7769fe8013038040d71><941c3b462cefa7769fe8013038040d71>] [<4606f0426670fb09488d895e0a695b1c><4606f0426670fb09488d895e0a695b1c>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 25 0 R /Info 25 0 R
@@ -323,5 +323,5 @@ trailer
/Size 45 /Size 45
>> >>
startxref startxref
12665 12810
%%EOF %%EOF

View File

@@ -183,7 +183,7 @@ endobj
endobj endobj
30 0 obj 30 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False /Subject (\(unspecified\)) /Title (Remote access \204 implementation notes \(v1 WireGuard\)) /Trapped /False
>> >>
endobj endobj
@@ -383,7 +383,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<853c39306aecb77ed422e218a356e197><853c39306aecb77ed422e218a356e197>] [<d904f4741c13412ecfac86be95b5e1e2><d904f4741c13412ecfac86be95b5e1e2>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 30 0 R /Info 30 0 R

View File

@@ -137,7 +137,7 @@ endobj
endobj endobj
22 0 obj 22 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False /Subject (\(unspecified\)) /Title (Remote access \204 validation \(Linux CLI + mobile parity\)) /Trapped /False
>> >>
endobj endobj
@@ -277,7 +277,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<5d07b3d520dc1b2274d0b97bcaaecff2><5d07b3d520dc1b2274d0b97bcaaecff2>] [<edb858d19a7dc7c6b966c01613958242><edb858d19a7dc7c6b966c01613958242>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 22 0 R /Info 22 0 R

View File

@@ -198,7 +198,7 @@ endobj
endobj endobj
32 0 obj 32 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False /Subject (\(unspecified\)) /Title (Android Cast \204 implementation roadmap) /Trapped /False
>> >>
endobj endobj
@@ -407,7 +407,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<0da68f69d69dfa4b1bad783189324a7b><0da68f69d69dfa4b1bad783189324a7b>] [<8f27e090a4e0d9efbfb958e71fb6f0a8><8f27e090a4e0d9efbfb958e71fb6f0a8>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 32 0 R /Info 32 0 R

View File

@@ -132,7 +132,7 @@ endobj
endobj endobj
21 0 obj 21 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False /Subject (\(unspecified\)) /Title (Tickets, Gitea, and alpha smoke delegation) /Trapped /False
>> >>
endobj endobj
@@ -266,7 +266,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<7488f6bb6c67f7874dbd93366608ef58><7488f6bb6c67f7874dbd93366608ef58>] [<933f0253ea9da04f7117c56c29f33651><933f0253ea9da04f7117c56c29f33651>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 21 0 R /Info 21 0 R

View File

@@ -112,7 +112,7 @@ endobj
endobj endobj
18 0 obj 18 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False /Subject (\(unspecified\)) /Title (USB-C / HDMI and USB-tether cast \(roadmap D & E\)) /Trapped /False
>> >>
endobj endobj
@@ -216,7 +216,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<fce2b2bd44cc97dec62511ff4c212ee7><fce2b2bd44cc97dec62511ff4c212ee7>] [<04785d34764bfbca56a9a6cb6c28e5e7><04785d34764bfbca56a9a6cb6c28e5e7>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 18 0 R /Info 18 0 R

View File

@@ -102,7 +102,7 @@ endobj
endobj endobj
16 0 obj 16 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (VPN / Remote Access demo \204 gap report \(2026-06-13\)) /Trapped /False /Subject (\(unspecified\)) /Title (VPN / Remote Access demo \204 gap report \(2026-06-13\)) /Trapped /False
>> >>
endobj endobj
@@ -204,7 +204,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<fc38ff8f318a67e77a875d191d72edee><fc38ff8f318a67e77a875d191d72edee>] [<0d0dcfb02c2ef4688845f129827f2125><0d0dcfb02c2ef4688845f129827f2125>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 16 0 R /Info 16 0 R

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -5,74 +5,206 @@ state: pre-implementation
author: Anton Afanasyeu author: Anton Afanasyeu
date: 2026-06-18 date: 2026-06-18
[AI comment]: Updated 2026-06-18 — PO chose org **`ac`**, **full microservice** end-state, separate **ac-scripts**, URL path migration (§L), optional **ac-workspace** (not build-all-in-one), VM-first cloud (§N). See §G master repo table.
The problem: The problem:
The project looks like monolith. For better and more atomic support we need to reorganize the whole project repo structure The project looks like monolith. For better and more atomic support we need to reorganize the whole project repo structure
[AI comment]: Agree on the problem, but **atomic** should mean: independent release cadence, CI, deploy, and rollback — not “one git repo per PHP class.” Todays real coupling is **one MariaDB**, **one PHP session** (`ac_crash_sess`, [BUILD_DEPLOY.md](../BUILD_DEPLOY.md)), and **one nginx vhost tree** on BE. Split repos before extracting shared platform code will multiply broken deploys.
──────────────────────────────────────────────────────────────────────────────
A. Current codebase map (monolith → draft names)
──────────────────────────────────────────────────────────────────────────────
| Draft name / intent | Actual path today | Coupling notes |
|---------------------|-------------------|----------------|
| ac-mobile | `app/`, `ndk/`, `gradle/`, `build.gradle`, `settings.gradle` | WireGuard tunnel module; `third-party/*` submodules |
| ac-session-studio | `desktop/session-studio/` | Standalone; low coupling — **easy first extract** |
| ac-docs | `docs/` (+ `docs/_pdf_build/`) | No runtime deps — **easy extract** |
| ac-scripts | `scripts/` (pdf, native codecs, OTA, CI helpers) | Used by mobile + orchestration; keep with **ac-deploy** or meta until split clear |
| ac-be-console (crashes/issues/tickets/graphs/rbac/ra) | `examples/crash_reporter/backend/` | **32 PHP classes**, 24 `public/api/*.php`, shared `Auth.php`, `Database.php`, `Rbac.php` — **one modular monolith** |
| ac-be-builder | `examples/build_console/backend/` | Separate app; shares auth pattern via `examples/platform/shared_session.php` + same DB users table |
| ac-be-landing | `examples/app_hub/` | Static hub + deploy nginx snippets |
| ac-be-ota (artifacts) | `examples/ota/v0/...` | Files on disk + nginx; logic also in builder + `scripts/generate-ota-v0.sh` |
| ac-ms-url-shortener | `backend/url-shortener/` | **Only live submodule today** — `git://f0xx.org/androidcast_project/url-shortener` |
| ac-deploy / orchestration | `orchestration/` (docker, cluster0, nginx, runtime) | Local stack + **cast0103** lab ([orchestration/sim/cluster0/ARCHITECTURE.md](../../orchestration/sim/cluster0/ARCHITECTURE.md)) |
| ac-ms-vpn / WG / RSSH | Mobile: `app/.../remoteaccess/`; BE: `RemoteAccessRepository.php`, `WireGuard*`, `Rssh*` provisioners | **Split by plane**: client in mobile, control API in BE console — not two deployables yet |
| ac-ms-vcs (gitea) | `examples/crash_reporter/backend/scripts/gitea/` | **Ops scripts only** — Gitea is external service on BE, not app source |
| ac-ms-tracker / analytics | `Ticket*`, `Graph*` repos in crash_reporter | Same DB + session — **logical modules**, not separate services today |
| SFU stub | `examples/sfu/`, `api/sfu_health.php` | Post-alpha; stay in console until F1 SPEC |
| Experimental | `examples/gentoo-portage-experimental/` (nested `.git`) | **Exclude** from meta umbrella or own repo — do not submodule into product meta |
| third-party codecs | `third-party/libvpx|opus|speex|wireguard-android` | Stay **submodules of ac-mobile** (upstream URLs unchanged); Gitea mirrors per existing migrate script |
[AI comment]: **Correction for draft “how” section:** `be-console` is **not** `./examples/build_console` — that is **builder**. Primary operator console is **`examples/crash_reporter/backend`** (crashes + tickets + graphs + RBAC + remote access APIs). Builder is separate product surface under `/build/`.
──────────────────────────────────────────────────────────────────────────────
B. Infrastructure & cloud fit (combine lab + prod + future cloud)
──────────────────────────────────────────────────────────────────────────────
Principles (from [specs/20100612_1_scaling.md](../specs/20100612_1_scaling.md), [INFRA.md](../INFRA.md), cluster0):
1. **Deploy unit = VM role or container image**, not git repo count. Phase A prod: `be-web-1` runs nginx + PHP-FPM for hub + console + graphs UI; `be-data-1` MariaDB; `be-ops-1` url-shortener + Gitea; `be-build-1` builder.
2. **cluster0 pattern** (NFS config only, app on local disk, GTID MariaDB) maps to cloud as:
- NFS `/shared/cluster/` → **ConfigMaps / object storage** for nginx + SQL seeds + scripts
- Per-node `/var/www/...` → **container image** or **git pull on VM** per service
- Managed DB → **connection string in secret** (PO: “pay for DB service”) — must **not** live in `ms-common` git
3. **PHP session + RBAC**: Shared cookie path `/app/androidcast_project` across hub/crashes/build ([BUILD_DEPLOY.md](../BUILD_DEPLOY.md)). Until true SSO/service tokens exist, **splitting BE UI repos is fine**; splitting **API auth** into N repos without `ac-platform-php` Composer package is painful.
4. **OpenAPI**: PO wants MS OpenAPI-compatible — today APIs are PHP files, not generated spec. Recommend **`ac-platform-openapi`** (or `ac-docs/openapi/`) as **contract-first** artifacts; implement in console first, extract services later.
5. **CI**: `build.config.yml` + CircleCI shape assume **monorepo checkout**. Meta-repo needs **per-repo pipelines** + `ac-deploy` triggering builds with `git_ref` across submodules (builder already does this for `android_cast`).
6. **URL / paths:** Under **discussion** — PO wants path-per-service (§M), not frozen `?view=` under `/crashes/`. nginx + mobile `BackendEndpoints` must migrate with redirects.
[AI comment]: Shared cookie path may become `/app/androidcast_project/` at **edge** only while backends move to `/issues/`, `/tickets/`, etc.
──────────────────────────────────────────────────────────────────────────────
C. Target structure — **full microservice** (PO decision 2026-06-18)
──────────────────────────────────────────────────────────────────────────────
PO chose **full microservice** end-state (§K comparison). Delivery remains **phased** (§D).
```text
Platform (libraries — Composer, not runtime MS)
ac-platform-php, ac-platform-db, ac-platform-web, ac-scripts, ac-docs
Infrastructure
ac-deploy submodules ac-scripts; cluster0/docker/nginx
ac-workspace optional clone manifest only (§N) — NOT unified build
Clients
ac-mobile-android (+ future ac-mobile-ios)
ac-session-studio
Microservices (OpenAPI APIs / workers)
ac-ms-identity, ac-ms-rbac, ac-ms-devices, ac-ms-issues, ac-ms-tickets,
ac-ms-graphs, ac-ms-remote-access, ac-ms-url-shortener, ac-ms-build,
ac-ms-ota, ac-ms-notifications (later)
Backend UI (thin)
ac-be-hub, ac-be-issues, ac-be-tickets, ac-be-graphs, ac-be-remote-access,
ac-be-access, ac-be-builder, ac-be-auth (optional)
Edge
ac-platform-edge nginx BFF — recommended for cookie + URL migration (§M)
```
[AI]: Collapse POs **ms-vpn-rssh** + **ms-vpn-wireguard** → **ac-ms-remote-access**. Drop **ac-ms-vcs** as code repo (Gitea = infra). Drop **ac-ms-tracker** as separate MS — split into **ac-ms-issues** + **ac-ms-tickets** (different URLs PO wants).
**Risk mitigation:** Composer semver for platform libs (avoid @common submodule diamond in 20+ repos).
──────────────────────────────────────────────────────────────────────────────
D. Phased migration (safe order — no big-bang)
──────────────────────────────────────────────────────────────────────────────
| Phase | Action | Alpha blocker? | Rollback |
|-------|--------|----------------|----------|
| **P0** | Create Gitea org **`ac`**; mirror legacy → **ac-workspace** (optional) | No | Keep `android_cast` mirror |
| **P1** | Extract **ac-docs**, **ac-session-studio** submodules; meta points SHAs | No | Revert submodule |
| **P2** | Extract **ac-ms-url-shortener** path to `ac/ac-ms-url-shortener` (already submodule — change URL only) | No | Old URL works via mirror |
| **P3** | **ac-platform-php** + **ac-platform-web** + **ac-ms-identity** first (everything depends on auth) | **Yes** for MS split | Shim monolith |
| **P4** | Extract **ac-ms-issues**, **ac-ms-tickets**, **ac-ms-rbac**, **ac-ms-devices** + matching **ac-be-*** UI repos | Medium | Edge proxy to monolith |
| **P5** | **ac-mobile-android**, **ac-deploy** (+ submodule **ac-scripts**), **ac-ms-build**, **ac-ms-ota** | Medium | Per-repo CI |
| **P6** | **ac-platform-edge** nginx routes (§M); retire `/crashes/?view=` | Medium | 301 redirects |
| **P7** | cluster0 + cloud VMs (§O); optional **ac-ms-graphs**, **ac-ms-remote-access** | No | Per scaling SPEC |
[AI comment]: Do **not** start P4 until P3 — otherwise Auth/Database duplicates diverge in days.
**ms-common sensitive data:** Split into:
- `ac-platform-php` — code (session helpers, RBAC classes, DB adapter interfaces)
- **Runtime config** — `config.php` / env / K8s secrets per deploy (never in git)
- Optional `ac-platform-db` — **SQL migrations only** (shared schema), consumed by console + shortener + builder
──────────────────────────────────────────────────────────────────────────────
E. The what (original table — annotated)
──────────────────────────────────────────────────────────────────────────────
The what:
Current project structure Current project structure
Project | Placement | Repo | State | Description Project | Placement | Repo | State | Description
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
AndroidCast all in one | repo root | git://f0xx.org/android_cast | monolithic | all-in-one project, needs to be splitted AndroidCast all in one | repo root | git://f0xx.org/android_cast | monolithic | all-in-one project, needs to be splitted
[AI]: → becomes **ac-meta** umbrella; keep **read-only mirror** at old URL ≥ 1 release
libvpx | third-party/libvpx | https://chromium.googlesource.com/webm/libvpx | 3rdparty exported submodule | VPx video codecs libvpx | third-party/libvpx | https://chromium.googlesource.com/webm/libvpx | 3rdparty exported submodule | VPx video codecs
[AI]: stays upstream URL in .gitmodules; Gitea mirror `AndroidCast/libvpx` unchanged
opus | third-party/opus | https://github.com/xiph/opus | 3rdparty exported submodule | Opus voice codec opus | third-party/opus | https://github.com/xiph/opus | 3rdparty exported submodule | Opus voice codec
speex | third-party/speex | https://github.com/xiph/speex/ | 3rdparty exported submodule | Speex voice codec speex | third-party/speex | https://github.com/xiph/speex/ | 3rdparty exported submodule | Speex voice codec
wireguard | third-party/wireguard-android | https://git.zx2c4.com/wireguard-android | 3rdparty exported submodule | wireguard for android wireguard | third-party/wireguard-android | https://git.zx2c4.com/wireguard-android | 3rdparty exported submodule | wireguard for android
url-shortener | backend/url-shortener | git://f0xx.org/androidcast_project/url-shortener | part of AndroidCast backend | url shortener standalone backend url-shortener | backend/url-shortener | git://f0xx.org/androidcast_project/url-shortener | part of AndroidCast backend | url shortener standalone backend
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ [AI]: migrate to **git://f0xx.org/ac/ac-ms-url-shortener**; alias old URL
──────────────────────────────────────────────────────────────────────────────
F. The how (original target — annotated + fixes)
──────────────────────────────────────────────────────────────────────────────
The how:
Next hope project structure Next hope project structure
AndroidCast project - all in one | git://f0xx.org/ac AndroidCast project - all in one | git://f0xx.org/ac/ac-meta
|-- AndroidCast - common | @common |-- AndroidCast - common | @ac-platform-php [AI: Composer, not submodule diamond]
|-- AndroidCast - mobile | @mobile |-- AndroidCast - mobile | @ac-mobile
|-- AndroidCast - BE | @backend |-- AndroidCast - BE | @ac-be-console (+ builder, hub as siblings)
... TBD ... TBD
AndroidCast project | Placement (@ - submodule) | Description | Repo AndroidCast project | Placement (@ - submodule) | Description | Repo
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
AndroidCast - common | @common | project all common | git://f0xx.org/ac/ac-common AndroidCast - common | @common | project all common | git://f0xx.org/ac/ac-common
[AI]: **Rename → ac-platform-php** (Composer). Reserve **ac-common** for non-PHP only: LICENSE templates, OpenAPI, semver constants — or drop repo and fold into ac-docs.
|-- AndroidCast - docs | @docs | project docs, BE, MS, mobile, web, everything | git://f0xx.org/ac/ac-docs |-- AndroidCast - docs | @docs | project docs, BE, MS, mobile, web, everything | git://f0xx.org/ac/ac-docs
|-- AndroidCast - scripts | @scripts | common project scripts | git://f0xx.org/ac/ac-scripts |-- AndroidCast - scripts | @scripts | common project scripts | git://f0xx.org/ac/ac-scripts
[AI]: merge ac-scripts into **ac-deploy** unless mobile-only scripts dominate
AndroidCast - mobile | @mobile | mobile app sources | git://f0xx.org/ac/ac-mobile AndroidCast - mobile | @mobile | mobile app sources | git://f0xx.org/ac/ac-mobile
|-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common |-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common
[AI]: mobile should NOT submodule ac-common PHP — only third-party natives
AndroidCast - Session Studio | @session-studio | offline analytic tools | git://f0xx.org/ac/ac-session-studio AndroidCast - Session Studio | @session-studio | offline analytic tools | git://f0xx.org/ac/ac-session-studio
|-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common |-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common
[AI]: session-studio has no PHP dep — drop @common edge
AndroidCast - BE - MS | @backend | backend microservices common base (UI) | git://f0xx.org/ac/ac-be-microservice AndroidCast - BE - MS | @backend | backend microservices common base (UI) | git://f0xx.org/ac/ac-be-microservice
[AI]: **Overloaded name.** Split: **ac-be-console** (main UI+API), **ac-platform-php** (lib). “ac-be-microservice” as umbrella meta for BE apps optional.
|-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common |-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common
AndroidCast - MS | @microservices | microservice commons | git://f0xx.org/ac/ac-microservice AndroidCast - MS | @microservices | microservice commons | git://f0xx.org/ac/ac-microservice
[AI]: defer **ac-microservice** meta-repo until ≥3 independently deployed APIs exist (today: 1 = url-shortener)
|-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common |-- AndroidCast - common | @common | | git://f0xx.org/ac/ac-common
... TBD ... TBD
AndroidCast BE microservices and their derivatives (OpenAPI providers, any REST API services for web and mobile apps) AndroidCast BE microservices and their derivatives (OpenAPI providers, any REST API services for web and mobile apps)
AndroidCast - MS - template <- AndroidCast - MS (@ means suibmodule) |git://f0xx.org/ac/ac-microservice/ac-ms-template AndroidCast - MS - template <- AndroidCast - MS (@ means suibmodule) |git://f0xx.org/ac/ac-microservice/ac-ms-template
[AI]: good template for **future** services; seed from url-shortener layout (public/, src/, sql/, tests/)
| -- AndroidCast - common | @common | project all common | git://f0xx.org/ac/ac-common | -- AndroidCast - common | @common | project all common | git://f0xx.org/ac/ac-common
| -- AndroidCast - MS - common | @ms-common | microservices common | git://f0xx.org/ac/ac-microservice/ac-ms-common | -- AndroidCast - MS - common | @ms-common | microservices common | git://f0xx.org/ac/ac-microservice/ac-ms-common
[AI]: **ms-common = ac-platform-php + openapi + migration runner** — NOT DB passwords. Fix typo **@ms-commmon** in remote-access row below.
|-- AndroidCast - MS - builder | @ms-builder | builder microservice | git://f0xx.org/ac/ac-microservice/ac-ms-builder |-- AndroidCast - MS - builder | @ms-builder | builder microservice | git://f0xx.org/ac/ac-microservice/ac-ms-builder
[AI]: today = **ac-be-builder** (UI+worker in one); split worker when be-build-1 is isolated CI agent only
|-- AndroidCast - MS - VPN | @ms-vpn | VPN microservice | git://f0xx.org/ac/ac-microservice/ac-ms-vpn |-- AndroidCast - MS - VPN | @ms-vpn | VPN microservice | git://f0xx.org/ac/ac-microservice/ac-ms-vpn
[AI]: **Phase 6+** — API surface = `remote_access.php` + provisioners; client stays in ac-mobile
|-- AndroidCast - MS - VPN - RSSH | @ms-vpn-rssh | VPN RSSH microservice | git://f0xx.org/ac/ac-microservice/ac-ms-vpn-rssh |-- AndroidCast - MS - VPN - RSSH | @ms-vpn-rssh | VPN RSSH microservice | git://f0xx.org/ac/ac-microservice/ac-ms-vpn-rssh
|-- AndroidCast - MS - VPN - WireGuard | @ms-vpn-wireguard | VPN WG microservice | git://f0xx.org/ac/ac-microservice/ac-ms-vpn-wireguard |-- AndroidCast - MS - VPN - WireGuard | @ms-vpn-wireguard | VPN WG microservice | git://f0xx.org/ac/ac-microservice/ac-ms-vpn-wireguard
[AI]: package by directory inside console first: `src/RemoteAccess/Rssh/`, `src/RemoteAccess/WireGuard/`
|-- AndroidCast - MS - VCS | @ms-vcs | VCS microservice (gitea) | git://f0xx.org/ac/ac-microservice/ac-ms-vcs |-- AndroidCast - MS - VCS | @ms-vcs | VCS microservice (gitea) | git://f0xx.org/ac/ac-microservice/ac-ms-vcs
[AI]: **Not a code repo** — Gitea is infra; keep **ac-deploy** scripts + docs for Gitea ops
|-- AndroidCast - MS - URL shortener | @ms-url-shortener | URL shortener microservice | git://f0xx.org/ac/ac-microservice/ac-ms-url-shortener |-- AndroidCast - MS - URL shortener | @ms-url-shortener | URL shortener microservice | git://f0xx.org/ac/ac-microservice/ac-ms-url-shortener
[AI]: **P2** — flatten path to `git://f0xx.org/ac/ac-ms-url-shortener` (no nested ac-microservice parent)
|-- AndroidCast - MS - analytics | @ms-analytics | analytics microservice | git://f0xx.org/ac/ac-microservice/ac-ms-analytics |-- AndroidCast - MS - analytics | @ms-analytics | analytics microservice | git://f0xx.org/ac/ac-microservice/ac-ms-analytics
[AI]: graphs ingest + GA config — module in console until read load warrants split
|-- AndroidCast - MS - orchestration | @ms-orchestration | orchestration microservice | git://f0xx.org/ac/ac-microservice/ac-ms-orchestration |-- AndroidCast - MS - orchestration | @ms-orchestration | orchestration microservice | git://f0xx.org/ac/ac-microservice/ac-ms-orchestration
[AI]: today = **ac-deploy** + builder API; name collision with folder `orchestration/`
|-- AndroidCast - MS - tracker | @ms-tracker | issues & tickets tracker microservice | git://f0xx.org/ac/ac-microservice/ac-ms-tracker |-- AndroidCast - MS - tracker | @ms-tracker | issues & tickets tracker microservice | git://f0xx.org/ac/ac-microservice/ac-ms-tracker
|-- AndroidCast - MS - RBAC | @ms-rbac | RBAC microservice | git://f0xx.org/ac/ac-microservice/ac-ms-rbac |-- AndroidCast - MS - RBAC | @ms-rbac | RBAC microservice | git://f0xx.org/ac/ac-microservice/ac-ms-rbac
... TBD ... TBD
Sub-project of AndroidCast BE/MS, mostly Web UI stuff, but also should be OpenAPI-compatible and provide services to the mobile app (@ means suibmodule) Sub-project of AndroidCast BE/MS, mostly Web UI stuff, but also should be OpenAPI-compatible and provide services to the mobile app (@ means suibmodule)
AndroidCast - BE - common <- AndroidCast - BE - MS | git://f0xx.org/ac/ac-backend AndroidCast - BE - common <- AndroidCast - BE - MS | git://f0xx.org/ac/ac-backend
[AI]: prefer **per-app repos** (ac-be-console, ac-be-builder, …) over deep `ac-backend/*` tree — flatter Gitea permissions
|-- AndroidCast - BE - vcs (UI) | @be-vcs | VCS frontend, former gitea, backend UI | git://f0xx.org/ac/ac-backend/ac-be-vcs |-- AndroidCast - BE - vcs (UI) | @be-vcs | VCS frontend, former gitea, backend UI | git://f0xx.org/ac/ac-backend/ac-be-vcs
[AI]: Gitea **is** the UI — link from hub only; optional thin PHP wrapper if needed
|-- AndroidCast - MS - vcs | @ms-vcs | VCS microservice (gitea) | git://f0xx.org/ac/ac-microservice/ac-ms-vcs |-- AndroidCast - MS - vcs | @ms-vcs | VCS microservice (gitea) | git://f0xx.org/ac/ac-microservice/ac-ms-vcs
|-- AndroidCast - BE - console (UI) | @be-console | git://f0xx.org/ac/ac-backend/ac-be-console | primary console, ./examples/build_console ? check if it is |-- AndroidCast - BE - console (UI) | @be-console | git://f0xx.org/ac/ac-backend/ac-be-console | primary console, ./examples/build_console ? check if it is
[AI]: **FIX:** primary console = **ac-be-console** ← `examples/crash_reporter/backend`. Builder = **ac-be-builder** ← `examples/build_console`.
|-- AndroidCast - MS - common | @ms-common | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons |-- AndroidCast - MS - common | @ms-common | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons
|-- AndroidCast - BE - landing (UI) | @be-landing | git://f0xx.org/ac/ac-backend/ac-be-landing | landing page(s), now in ./examples/app_hub |-- AndroidCast - BE - landing (UI) | @be-landing | git://f0xx.org/ac/ac-backend/ac-be-landing | landing page(s), now in ./examples/app_hub
|-- AndroidCast - MS - common | @ms-common | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons |-- AndroidCast - MS - common | @ms-common | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons
|-- AndroidCast - BE - issues (UI) | @be-issues | git://f0xx.org/ac/ac-backend/ac-be-issues | issues tracker UI, now is in ./examples somewhere |-- AndroidCast - BE - issues (UI) | @be-issues | git://f0xx.org/ac/ac-backend/ac-be-issues | issues tracker UI, now is in ./examples somewhere
[AI]: **same PHP app** as tickets — `?view=` routes in crash_reporter views, not separate deployable
|-- AndroidCast - MS - tracker | @ms-tracker | git://f0xx.org/ac/ac-microservice/ac-ms-tracker | issues tracker microservice shared base, now is in ./examples somewhere |-- AndroidCast - MS - tracker | @ms-tracker | git://f0xx.org/ac/ac-microservice/ac-ms-tracker | issues tracker microservice shared base, now is in ./examples somewhere
|-- AndroidCast - BE - tickets (UI) | @be-tickets | git://f0xx.org/ac/ac-backend/ac-be-tickets | tickets UI, now is in ./examples somewhere |-- AndroidCast - BE - tickets (UI) | @be-tickets | git://f0xx.org/ac/ac-backend/ac-be-tickets | tickets UI, now is in ./examples somewhere
|-- AndroidCast - MS - tracker | @ms-tracker | git://f0xx.org/ac/ac-microservice/ac-ms-tracker | issues tracker microservice shared base, now is in ./examples somewhere |-- AndroidCast - MS - tracker | @ms-tracker | git://f0xx.org/ac/ac-microservice/ac-ms-tracker | issues tracker microservice shared base, now is in ./examples somewhere
@@ -80,19 +212,345 @@ AndroidCast - BE - common <- AndroidCast - BE - MS | git://f0xx.org
|-- AndroidCast - MS - analytics | @ms-analytics | git://f0xx.org/ac/ac-microservice/ac-ms-analytics | analytics base microservice |-- AndroidCast - MS - analytics | @ms-analytics | git://f0xx.org/ac/ac-microservice/ac-ms-analytics | analytics base microservice
|-- AndroidCast - BE - analytics (UI) | @be-analytics | git://f0xx.org/ac/ac-backend/ac-be-analytics | analytics and statistics web UI |-- AndroidCast - BE - analytics (UI) | @be-analytics | git://f0xx.org/ac/ac-backend/ac-be-analytics | analytics and statistics web UI
|-- AndroidCast - MS - analytics | @ms-analytics | git://f0xx.org/ac/ac-microservice/ac-ms-analytics | analytics base microservice |-- AndroidCast - MS - analytics | @ms-analytics | git://f0xx.org/ac/ac-microservice/ac-ms-analytics | analytics base microservice
[AI]: graphs + analytics UI = views inside **ac-be-console** today (`GraphRepository`, `AnalyticsHead`)
|-- AndroidCast - BE - remote access (UI) | @be-remote-access | git://f0xx.org/ac/ac-backend/ac-be-remote-access | remote access (now for vpn, somewhere in ./examples now) UI |-- AndroidCast - BE - remote access (UI) | @be-remote-access | git://f0xx.org/ac/ac-backend/ac-be-remote-access | remote access (now for vpn, somewhere in ./examples now) UI
|-- AndroidCast - MS - common | @ms-commmon | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons |-- AndroidCast - MS - common | @ms-commmon | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons
[AI]: typo **commmon** → common; RA UI = admin views in console + mobile app
|-- AndroidCast - MS - VPN | @ms-vpn | git://f0xx.org/ac/ac-microservice/ac-ms-vpn | VPN MS as a dependency |-- AndroidCast - MS - VPN | @ms-vpn | git://f0xx.org/ac/ac-microservice/ac-ms-vpn | VPN MS as a dependency
|-- AndroidCast - MS - RBAC | @ms-rbac | @ms-rbac | git://f0xx.org/ac/ac-microservice/ac-ms-rbac | RBAC MS as a dependency |-- AndroidCast - MS - RBAC | @ms-rbac | @ms-rbac | git://f0xx.org/ac/ac-microservice/ac-ms-rbac | RBAC MS as a dependency
|-- AndroidCast - BE - URL shortener (UI) | @be-url-shortener | git://f0xx.org/ac/ac-backend/ac-be-url-shortener | URL shortener UI, now is in separate submodule |-- AndroidCast - BE - URL shortener (UI) | @be-url-shortener | git://f0xx.org/ac/ac-backend/ac-be-url-shortener | URL shortener UI, now is in separate submodule
[AI]: short-link **admin UI** is in crash console (`short_links.js`, hub tokens); **API** is submodule
|-- AndroidCast - MS - URL shortener | @ms-url-shortener | git://f0xx.org/ac/ac-microservice/ac-ms-url-shortener | URL shortener microservice, now is in separate submodule |-- AndroidCast - MS - URL shortener | @ms-url-shortener | git://f0xx.org/ac/ac-microservice/ac-ms-url-shortener | URL shortener microservice, now is in separate submodule
|-- AndroidCast - BE - builder (UI) | @be-builder | git://f0xx.org/ac/ac-backend/ac-be-builder | orchestration builder UI, now in ./orchestration |-- AndroidCast - BE - builder (UI) | @be-builder | git://f0xx.org/ac/ac-backend/ac-be-builder | orchestration builder UI, now in ./orchestration
[AI]: **FIX:** builder UI = `examples/build_console`; **orchestration/** = docker/cluster0 (**ac-deploy**)
|-- AndroidCast - MS - orchestration | @ms-orchestration | git://f0xx.org/ac/ac-microservice/ac-ms-orchestration | orchestration microservice, now in ./orchestration |-- AndroidCast - MS - orchestration | @ms-orchestration | git://f0xx.org/ac/ac-microservice/ac-ms-orchestration | orchestration microservice, now in ./orchestration
|-- AndroidCast - BE - RBAC (UI) | @be-rbac | git://f0xx.org/ac/ac-backend/ac-be-rbac | RBAC backend, now mixed within ./examples somewhere |-- AndroidCast - BE - RBAC (UI) | @be-rbac | git://f0xx.org/ac/ac-backend/ac-be-rbac | RBAC backend, now mixed within ./examples somewhere
|-- AndroidCast - MS - common | @ms-common | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons |-- AndroidCast - MS - common | @ms-common | git://f0xx.org/ac/ac-microservice/ac-ms-common | microservice commons
|-- AndroidCast - MS - RBAC | @ms-rbac | git://f0xx.org/ac/ac-microservice/ac-ms-rbac | RBAC microservice |-- AndroidCast - MS - RBAC | @ms-rbac | git://f0xx.org/ac/ac-microservice/ac-ms-rbac | RBAC microservice
|-- AndroidCast - BE - OTA | @be-ota | git://f0xx.org/ac/ac-backend/ac-be-ota | OTA BE UI |-- AndroidCast - BE - OTA | @be-ota | git://f0xx.org/ac/ac-backend/ac-be-ota | OTA BE UI
|-- AndroidCast - MS - OTA | @ms-ota | git://f0xx.org/ac/ac-microservice/ac-ms-ota | OTA microservice, now intermixed in examples/ota and other commons |-- AndroidCast - MS - OTA | @ms-ota | git://f0xx.org/ac/ac-microservice/ac-ms-ota | OTA microservice, now intermixed in examples/ota and other commons
[AI]: OTA = static tree + builder publish + nginx location; extract API when `5.1 OTA` nginx stable
... TBD ... TBD
ms-common (microservices common) needs a sharp eye, it may share project sensitive parts, like common stuff such as shared web sessions, DB access (or should be separated to @be-db-service/@ac-db-service sort of) ms-common (microservices common) needs a sharp eye, it may share project sensitive parts, like common stuff such as shared web sessions, DB access (or should be separated to @be-db-service/@ac-db-service sort of)
[AI]: **Agree.** Recommended split:
- **ac-platform-php** — session *code*, Auth, Rbac, PDO factory (reads DSN from env)
- **ac-platform-db** — SQL migrations + schema version table (shared by console, builder, shortener)
- **ac-deploy/secrets** — example only; prod DSN in vault / Alpine `config.php` outside git
- **No ac-db-service runtime** until you run DB proxy (PlanetScale, RDS, etc.) — app VMs use driver + secret
──────────────────────────────────────────────────────────────────────────────
G. FULL REPO CATALOG — microservice end-state (Gitea org **ac**)
──────────────────────────────────────────────────────────────────────────────
Canonical clone: `git://f0xx.org/ac/<repo>`. HTTPS UI: `https://apps.f0xx.org/app/androidcast_project/git/ac/<repo>.git` (path TBD with edge migration). Legacy `git://f0xx.org/android_cast` → read-only mirror until retired.
| ID | Repo | Git URL | Role | From (monolith) | VM / cloud slot |
|----|------|---------|------|-----------------|-----------------|
| **Workspace** |
| W0 | ac-workspace | `git://f0xx.org/ac/ac-workspace` | Optional submodule manifest; **no unified build** | `.gitmodules` | dev laptops |
| **Platform** |
| P0 | ac-platform-php | `git://f0xx.org/ac/ac-platform-php` | Composer: HTTP, auth client, PDO factory | Auth*, bootstrap | baked into PHP images |
| P1 | ac-platform-db | `git://f0xx.org/ac/ac-platform-db` | SQL migrations + schema version | sql/, migrations | be-data-1 job |
| P2 | ac-platform-web | `git://f0xx.org/ac/ac-platform-web` | Shared CSS/JS/theme | crashes/assets, hub links | CDN / edge static |
| P3 | ac-platform-edge | `git://f0xx.org/ac/ac-platform-edge` | nginx routes, cookie domain, proxy | fe/apps.conf | be-web-1 / FE |
| P4 | ac-scripts | `git://f0xx.org/ac/ac-scripts` | CI, OTA, native codec, pdf scripts | `scripts/` | CI runners |
| P5 | ac-docs | `git://f0xx.org/ac/ac-docs` | Documentation | `docs/` | — |
| **Deploy** |
| D0 | ac-deploy | `git://f0xx.org/ac/ac-deploy` | docker, cluster0, env templates; **uses ac-scripts** | `orchestration/` | all VMs |
| **Clients** |
| C0 | ac-mobile-android | `git://f0xx.org/ac/ac-mobile-android` | Android app + ndk + third-party | `app/`, `ndk/` | CI → OTA |
| C1 | ac-mobile-ios | `git://f0xx.org/ac/ac-mobile-ios` | Future iOS client | — | CI → OTA |
| C2 | ac-session-studio | `git://f0xx.org/ac/ac-session-studio` | Desktop analyzer | `desktop/session-studio/` | releases |
| **Microservices** |
| S0 | ac-ms-template | `git://f0xx.org/ac/ac-ms-template` | Cookiecutter for new PHP MS | url-shortener layout | — |
| S1 | ac-ms-identity | `git://f0xx.org/ac/ac-ms-identity` | Users, login, register, 2FA, sessions | Auth*, UserRepository | be-web-1 |
| S2 | ac-ms-rbac | `git://f0xx.org/ac/ac-ms-rbac` | Companies, memberships, privileges | Rbac*, RbacAdmin* | be-web-1 |
| S3 | ac-ms-devices | `git://f0xx.org/ac/ac-ms-devices` | Device registry | DeviceRepository | be-web-1 |
| S4 | ac-ms-issues | `git://f0xx.org/ac/ac-ms-issues` | Crash/report ingest + reports API | upload, Report*, Tag* | be-web-1 |
| S5 | ac-ms-tickets | `git://f0xx.org/ac/ac-ms-tickets` | Tickets workflow, comments, attachments | Ticket* | be-web-1 |
| S6 | ac-ms-graphs | `git://f0xx.org/ac/ac-ms-graphs` | Graph ingest + query | GraphRepository, graph_*.php | be-web-1 |
| S7 | ac-ms-remote-access | `git://f0xx.org/ac/ac-ms-remote-access` | WG + RSSH control API | RemoteAccess*, WireGuard*, Rssh* | be-ops-1 |
| S8 | ac-ms-url-shortener | `git://f0xx.org/ac/ac-ms-url-shortener` | Short link API | `backend/url-shortener/` | be-ops-1 |
| S9 | ac-ms-build | `git://f0xx.org/ac/ac-ms-build` | Build queue + worker API | BuildRunner, build_console APIs | be-build-1 |
| S10 | ac-ms-ota | `git://f0xx.org/ac/ac-ms-ota` | Channel manifests; multi-client SHAs | OTA scripts + v0 tree | be-web-1 / OBJ |
| S11 | ac-ms-notifications | `git://f0xx.org/ac/ac-ms-notifications` | SMTP / auth mail (post-alpha) | AuthMailer | be-ops-1 |
| **Backend UI** |
| B0 | ac-be-hub | `git://f0xx.org/ac/ac-be-hub` | Landing | `examples/app_hub/` | be-web-1 |
| B1 | ac-be-issues | `git://f0xx.org/ac/ac-be-issues` | Issues UI → S4 | views reports/* | be-web-1 |
| B2 | ac-be-tickets | `git://f0xx.org/ac/ac-be-tickets` | Tickets UI → S5 | views tickets/* | be-web-1 |
| B3 | ac-be-graphs | `git://f0xx.org/ac/ac-be-graphs` | Graphs dashboard UI | graphs views | be-web-1 |
| B4 | ac-be-remote-access | `git://f0xx.org/ac/ac-be-remote-access` | RA admin UI → S7 | view remote_access | be-web-1 |
| B5 | ac-be-access | `git://f0xx.org/ac/ac-be-access` | RBAC admin UI → S2 | view rbac | be-web-1 |
| B6 | ac-be-builder | `git://f0xx.org/ac/ac-be-builder` | Builder UI → S9 | `examples/build_console/` | be-build-1 |
| B7 | ac-be-auth | `git://f0xx.org/ac/ac-be-auth` | Login/register chrome → S1 | login.php shared | be-web-1 |
| **Third-party (submodules of C0 only)** |
| — | libvpx, opus, speex, wireguard-android | upstream URLs + Gitea mirrors under `ac/` | Native deps | `third-party/*` | mobile CI |
| **Future** |
| F1 | ac-ms-sfu-signaling | `git://f0xx.org/ac/ac-ms-sfu-signaling` | WebRTC signaling | examples/sfu | be-media-N |
| F2 | ac-ms-media-transcode | `git://f0xx.org/ac/ac-ms-media-transcode` | VOD/ffmpeg | V.2x draft | be-media-N |
**PO draft mapping fixes:**
- `ac-be-console` (monolith) → **dissolved** into B1B5 + S4S7
- `ac-ms-tracker` → **S4 + S5** (issues ≠ tickets — PO URL split confirms)
- `ac-ms-analytics` → **S6** (graphs) + later BI; GA config in **P2 platform-web**
- `ac-ms-orchestration` → **S9 + D0** (worker vs deploy)
- `ac-ms-vcs` → **not a repo**; document in **D0**
- `ac-be-url-shortener` UI → admin panel in **B5** or **B0** calling **S8** (tokens already in crashes console)
**ac-deploy ↔ ac-scripts:** D0 submodules P4 at pinned SHA; deploy-local scripts only for image entrypoints.
──────────────────────────────────────────────────────────────────────────────
H. ac-workspace sketch (optional — PO §5)
──────────────────────────────────────────────────────────────────────────────
**Purpose:** “I only work on mobile” or “I only work on tickets UI” — clone workspace, init selected submodules. **Not** for building everything in one Gradle/PHP tree.
```ini
# ac-workspace — example partial checkout
[submodule "mobile-android"]
path = clients/android
url = git://f0xx.org/ac/ac-mobile-android
[submodule "ms-issues"]
path = services/issues
url = git://f0xx.org/ac/ac-ms-issues
[submodule "be-issues"]
path = ui/issues
url = git://f0xx.org/ac/ac-be-issues
[submodule "deploy"]
path = deploy
url = git://f0xx.org/ac/ac-deploy
```
Developers without workspace clone individual repos directly.
──────────────────────────────────────────────────────────────────────────────
I. PO decisions (2026-06-18) — recorded
──────────────────────────────────────────────────────────────────────────────
| # | Question | Decision |
|---|----------|----------|
| 1 | Gitea org | **`ac`** |
| 2 | Architecture | **Full microservice** end-state (phased delivery) — §K |
| 3 | ac-scripts | **Separate repo**; **ac-deploy submodules/invokes it** |
| 4 | Public URLs | **Change desired** — path-per-service (§M); not frozen under `/crashes/?view=` |
| 5 | Meta repo | **`ac-workspace` optional** — selective clone only; **not** build-all-in-one; §N OTA |
| 6 | Cloud | **No K8s requirement** — start VM lift-and-shift (§O); K8s later optional |
──────────────────────────────────────────────────────────────────────────────
J. Microservice vs modular monolith — comparison (PO requested)
──────────────────────────────────────────────────────────────────────────────
| Criterion | Full microservice (PO choice) | Modular monolith (single ac-be-console) |
|-----------|------------------------------|----------------------------------------|
| **Independent deploy** | Deploy tickets without touching issues | One PHP deploy for any UI fix |
| **Team scaling** | Different owners per repo/service | Single codebase, simpler grep |
| **Failure isolation** | Ticket MS down ≠ crash ingest | Shared FPM pool — blast radius larger |
| **CI speed** | Small repos, faster per-service tests | One `./gradlew`-scale PHP test suite |
| **Auth/session** | Needs **ac-ms-identity** + edge cookie early | Already works (`ac_crash_sess`) |
| **DB** | Shared MariaDB OK initially; schema ownership per MS | One migration stream today |
| **Operational cost** | More nginx routes, more images, more logs | One BE tree to sync ([INFRA.md](../INFRA.md)) |
| **PO microservice learning curve** | Higher — mitigated by **ac-ms-template**, **D0** | Lower short-term |
| **Cloud path** | Maps 1:1 to VM roles then containers | Lift whole monolith first |
| **URL clarity** | Natural `/issues/`, `/tickets/` per service | Stuck on `?view=` without refactor |
| **Rollback** | Revert one service SHA | Revert one monolith commit |
| **Risk for alpha** | **Higher** if done before identity split | Lower — current prod pattern |
[AI]: PO choice is valid **if P3 (identity + platform) is non-negotiable first**. Without it, full MS split repeats the build-console/bootstrap.php class of failures across N repos.
──────────────────────────────────────────────────────────────────────────────
K. Service dependency graph (logical + structural + repo)
──────────────────────────────────────────────────────────────────────────────
```mermaid
flowchart TB
subgraph clients [Clients]
AND[ac-mobile-android]
IOS[ac-mobile-ios future]
HUB[ac-be-hub]
SS[ac-session-studio]
end
subgraph edge [Edge]
EDGE[ac-platform-edge]
end
subgraph core [Core platform MS]
ID[ac-ms-identity]
RB[ac-ms-rbac]
DV[ac-ms-devices]
end
subgraph domain [Domain MS]
IS[ac-ms-issues]
TK[ac-ms-tickets]
GR[ac-ms-graphs]
RA[ac-ms-remote-access]
UL[ac-ms-url-shortener]
BL[ac-ms-build]
OT[ac-ms-ota]
end
subgraph ui [Backend UI]
UI_I[ac-be-issues]
UI_T[ac-be-tickets]
UI_G[ac-be-graphs]
UI_R[ac-be-remote-access]
UI_A[ac-be-access]
UI_B[ac-be-builder]
end
AND -->|upload API| EDGE
AND -->|RA API| EDGE
AND -->|graph upload| EDGE
HUB --> EDGE
UI_I & UI_T & UI_G & UI_R & UI_A & UI_B --> EDGE
EDGE --> ID & IS & TK & GR & RA & UL & BL & OT & RB
ID --> RB
RB --> DV
IS --> DV
IS --> RB
TK --> RB
TK -.->|optional link| IS
GR --> ID
RA --> RB
RA --> DV
RA -.->|issue deep links| IS
BL --> ID
BL -->|git clone| GITEA[(Gitea infra)]
OT --> BL
OT --> AND
OT --> IOS
UL --> RB
```
**Dependency rules (enforce in DR/SPEC):**
| Service | Hard depends on | Soft / read depends on | Mobile / public API |
|---------|-----------------|------------------------|---------------------|
| ac-ms-identity | ac-platform-db (users schema) | ac-ms-notifications (mail) | register, login |
| ac-ms-rbac | identity (JWT/session validation) | — | — |
| ac-ms-devices | rbac (company scope) | identity | — |
| ac-ms-issues | devices, rbac | tags catalog | **upload.php** |
| ac-ms-tickets | rbac | issues (link ticket↔report) | — |
| ac-ms-graphs | identity | — | graph_upload |
| ac-ms-remote-access | rbac, devices | issues (device context URLs) | remote_access.php |
| ac-ms-url-shortener | rbac (API tokens) | — | s.f0xx.org |
| ac-ms-build | identity | gitea mirror | — |
| ac-ms-ota | build artifacts | mobile/ios SHAs | `/v0/ota/` |
| ac-be-hub | identity (session for nav) | **P2 platform-web** assets | — |
**Cross-cutting fixes vs PO draft:**
1. **Auth must be first MS** — every UI and API currently includes `bootstrap.php` → Auth.
2. **Devices before issues** — upload auto-registers device + company ([README](../examples/crash_reporter/backend/README.md) RBAC).
3. **Hub → crashes assets** — today `app_hub/index.php` loads `/crashes/assets/css/app.css`; extract **ac-platform-web** before splitting hub.
4. **Builder → same DB users** — build_console uses `androidcast_crashes.users`; cannot split S9 before S1.
5. **Short links admin** — UI in monolith, API in submodule; UI calls S8, not duplicate repo **ac-be-url-shortener**.
6. **Graphs** — already separate nginx location `/graphs/` ([apps.conf](../../examples/crash_reporter/backend/nginx/fe/apps.conf)) — good MS boundary.
**Suggested new intermediates (actors):**
| Service | Why |
|---------|-----|
| **ac-ms-identity** | Single login/register/2FA; avoids 8 copies of Auth.php |
| **ac-ms-devices** | Shared by issues, RA, graphs device keys |
| **ac-platform-edge** | One nginx config for POs URL map + SSO cookie |
| **ac-ms-ota** | PO multi-repo Android+iOS same version (§N) |
| **ac-ms-notifications** | Decouple SMTP from identity (task 2.x mail frozen) |
| **ac-ms-attachments** (later) | Ticket files → object store when > local disk |
──────────────────────────────────────────────────────────────────────────────
L. Public URL migration map (PO §4)
──────────────────────────────────────────────────────────────────────────────
Base prefix (discussion): keep **`/app/androidcast_project/`** for alpha compatibility, or shorten to **`/app/ac/`** later. Below uses POs intent under current prefix.
| Today | Target path | MS / UI | Notes |
|-------|-------------|---------|-------|
| `/crashes/?view=reports` | **`/issues/`** | B1 → S4 | “Issue tracker” / crash reports list |
| `/crashes/?view=report&id=N` | **`/issues/N`** or `/issues/report/N` | B1 | Detail |
| `/crashes/?view=tickets` | **`/tickets/`** | B2 → S5 | Tickets catalog |
| `/crashes/?view=ticket&id=N` | **`/tickets/N`** | B2 | Detail |
| `/crashes/?view=rbac` | **`/access/`** | B5 → S2 | RBAC admin |
| `/crashes/?view=remote_access` | **`/remote-access/`** | B4 → S7 | RA admin |
| `/crashes/?view=short_links` | **`/short-links/`** or hub panel | B0/B5 → S8 | API stays on s.f0xx.org |
| `/crashes/?view=home` | **`/`** hub or `/console/` | B0 | Retire crashes “home” |
| `/crashes/api/upload.php` | **`/issues/api/upload`** or `/api/v1/issues/upload` | S4 | Update `BackendEndpoints.java`, `CrashSettings.java` |
| `/crashes/api/*` (tickets) | **`/tickets/api/*`** | S5 | |
| `/graphs/` | **`/graphs/`** (keep) | B3 → S6 | Already separate vhost block |
| `/build/` | **`/build/`** (keep) | B6 → S9 | |
| `/` (hub) | **`/`** (keep) | B0 | |
| `/git/` | **`/git/`** (keep) | Gitea | Not a MS |
| `/v0/ota/` | **`/v0/ota/`** (keep) | S10 | |
**Migration mechanics:** ac-platform-edge 301 redirects old URLs ≥ 1 release; mobile app legacy URL rewrite already in `BackendEndpoints.normalizeUploadUrl()`.
**Naming note:** Today “reports” in code = **issues/crashes** in product language — align UI strings when moving to `/issues/`.
──────────────────────────────────────────────────────────────────────────────
M. ac-workspace + OTA multi-repo versioning (PO §5)
──────────────────────────────────────────────────────────────────────────────
**ac-workspace is NOT:**
- A single Gradle/Composer build root
- Required for CI (each repo has own pipeline)
**ac-workspace IS:**
- Optional manifest: “which repos at which SHA for a release train”
- Convenience for developers who want `clients/android` + `services/issues` side by side
**OTA same version, different client SHAs (Android + iOS):**
`ac-ms-ota` channel manifest example:
```json
{
"channel": "staging",
"version": "00.02.00.00",
"artifacts": {
"android": { "repo": "ac-mobile-android", "git_sha": "abc123", "apk_url": "..." },
"ios": { "repo": "ac-mobile-ios", "git_sha": "def456", "ipa_url": "..." }
}
}
```
Builder (S9) triggers per-repo builds; **S10** assembles manifest. Mobile apps read **`version`** for UX; **`git_sha`** per platform for support.
──────────────────────────────────────────────────────────────────────────────
N. Cloud & VM strategy (PO §6 — no K8s experience)
──────────────────────────────────────────────────────────────────────────────
**Recommendation: Phase 1 = VM lift-and-shift** (matches cluster0 lab, scaling SPEC §6.3, PO skill set).
| Stage | Where | Pattern |
|-------|-------|---------|
| **Now** | FE + BE Alpine | nginx + PHP-FPM + MariaDB ([INFRA.md](../INFRA.md)) |
| **Lab** | cast0103 | GTID MariaDB, NFS config only, apps local ([cluster0 ARCHITECTURE](../../orchestration/sim/cluster0/ARCHITECTURE.md)) |
| **Cloud phase A** | 46 VMs (be-web, be-data, be-ops, be-build) | Same as scaling table; **docker only on build** |
| **Cloud phase B** | Managed DB (RDS-class) | Apps use DSN secret; **ac-platform-db** migrations as job |
| **Cloud phase C** (optional) | K8s / compose swarm | **Not required** — introduce when MS count > ~8 and PO wants auto-scale |
**ac-deploy deliverables for cloud:** Terraform optional; minimum **Ansible + docker-compose fragments per VM role** + env templates. Each MS = one PHP-FPM pool or one container behind edge nginx.
**Inter-service calls (PO poor on microservices):** Start with **HTTP on localhost/private VLAN** (same BE host), not service mesh. Edge nginx routes public paths; internal `127.0.0.1:900x` or docker network. OpenAPI spec in **ac-docs/openapi/** is the contract.
──────────────────────────────────────────────────────────────────────────────
O. DR readiness checklist
──────────────────────────────────────────────────────────────────────────────
- [x] Map monolith → full MS catalog (§G)
- [x] PO decisions recorded (§I)
- [x] Comparison table (§J)
- [x] Dependency graph + fixes (§K)
- [x] URL migration map (§L)
- [x] Workspace / OTA model (§M)
- [x] Cloud recommendation (§N)
- [ ] PO confirms URL prefix (`/app/androidcast_project/` vs `/app/ac/`) — **locked: full prefix**
- [ ] PO confirms **ac-ms-identity** as P3 gate — **approved**
- [x] Write `docs/DRs/20260618_repos_reorganizing.md` + PDF
- [ ] Update gitea migrate scripts for org **`ac`**
- [ ] Migration runbook P0P7
[AI]: **Create Gitea repos from §G table in waves:** Wave 1 = P4,P5,D0,C2,S8; Wave 2 = S1,S2,S3; Wave 3 = S4,S5 + B1,B2; then edge + URL migration.

View File

@@ -305,7 +305,7 @@ endobj
endobj endobj
51 0 obj 51 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220502+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220502+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (URL shortener service \204 specification) /Trapped /False /Subject (\(unspecified\)) /Title (URL shortener service \204 specification) /Trapped /False
>> >>
endobj endobj
@@ -647,7 +647,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<db651936781475f5cdf0f7bf03422a2e><db651936781475f5cdf0f7bf03422a2e>] [<70460d5cbd1dab2ea3f548e883e3e39b><70460d5cbd1dab2ea3f548e883e3e39b>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 51 0 R /Info 51 0 R

View File

@@ -425,7 +425,7 @@ endobj
endobj endobj
67 0 obj 67 0 obj
<< <<
/Author (Android Cast project) /CreationDate (D:20260617220503+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260617220503+02'00') /Producer (ReportLab PDF Library - \(opensource\)) /Author (Android Cast project) /CreationDate (D:20260618174834+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260618174834+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Platform scaling and multi-environment architecture \204 specification) /Trapped /False /Subject (\(unspecified\)) /Title (Platform scaling and multi-environment architecture \204 specification) /Trapped /False
>> >>
endobj endobj
@@ -862,7 +862,7 @@ xref
trailer trailer
<< <<
/ID /ID
[<f0fbbf50d8dfd95b3b0f2c0df4aae3a2><f0fbbf50d8dfd95b3b0f2c0df4aae3a2>] [<09178fbdf1e2842e537b65a31d547b84><09178fbdf1e2842e537b65a31d547b84>]
% ReportLab generated PDF document -- digest (opensource) % ReportLab generated PDF document -- digest (opensource)
/Info 67 0 R /Info 67 0 R

View File

@@ -100,3 +100,19 @@ find_library(log-lib log)
find_library(android-lib android) find_library(android-lib android)
find_library(dl-lib dl) find_library(dl-lib dl)
target_link_libraries(androidcast_codecs ${log-lib} ${android-lib} ${dl-lib}) target_link_libraries(androidcast_codecs ${log-lib} ${android-lib} ${dl-lib})
# Per-account obfuscated entitlement flags (separate .so; safe to omit on load failure).
set(OBFUSCATE_ROOT ${CMAKE_SOURCE_DIR}/obfuscate)
add_library(androidcast_entitlements SHARED
${NDK_BRIDGE}/jni/entitlements_jni.c
${OBFUSCATE_ROOT}/obfuscate.c
${OBFUSCATE_ROOT}/obfuscate_file.c
)
target_include_directories(androidcast_entitlements PRIVATE
${OBFUSCATE_ROOT}
)
target_compile_options(androidcast_entitlements PRIVATE -std=c11 -Wall -Wextra)
target_link_libraries(androidcast_entitlements ${log-lib})

103
ndk/jni/entitlements_jni.c Normal file
View File

@@ -0,0 +1,103 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include "obfuscate.h"
#include "obfuscate_file.h"
static jint throw_illegal_state(JNIEnv *env, const char *msg)
{
jclass cls = (*env)->FindClass(env, "java/lang/IllegalStateException");
if (cls != NULL)
(*env)->ThrowNew(env, cls, msg);
return OBF_ERR_NULL;
}
JNIEXPORT jint JNICALL
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeReadFrom(
JNIEnv *env, jclass clazz, jstring path, jlong key, jlongArray out_val)
{
const char *cpath;
uint64_t decoded = 0;
int32_t rv;
jlong slot;
(void)clazz;
if (path == NULL || out_val == NULL)
return throw_illegal_state(env, "path or out_val is null");
cpath = (*env)->GetStringUTFChars(env, path, NULL);
if (cpath == NULL)
return OBF_ERR_IO;
rv = read_from(cpath, (uint64_t)key, &decoded);
(*env)->ReleaseStringUTFChars(env, path, cpath);
if (rv != OBF_OK)
return rv;
slot = (jlong)decoded;
(*env)->SetLongArrayRegion(env, out_val, 0, 1, &slot);
return OBF_OK;
}
JNIEXPORT jint JNICALL
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeWriteTo(
JNIEnv *env, jclass clazz, jstring path, jlong val, jlong key)
{
const char *cpath;
int32_t rv;
(void)clazz;
if (path == NULL)
return throw_illegal_state(env, "path is null");
cpath = (*env)->GetStringUTFChars(env, path, NULL);
if (cpath == NULL)
return OBF_ERR_IO;
rv = write_to(cpath, (uint64_t)val, (uint64_t)key);
(*env)->ReleaseStringUTFChars(env, path, cpath);
return rv;
}
JNIEXPORT jint JNICALL
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeDecode(
JNIEnv *env, jclass clazz, jlong encoded_val, jlong key, jlongArray out_val)
{
uint64_t decoded = 0;
int32_t rv;
jlong slot;
(void)clazz;
if (out_val == NULL)
return throw_illegal_state(env, "out_val is null");
rv = decode((uint64_t)encoded_val, (uint64_t)key, &decoded);
if (rv != OBF_OK)
return rv;
slot = (jlong)decoded;
(*env)->SetLongArrayRegion(env, out_val, 0, 1, &slot);
return OBF_OK;
}
JNIEXPORT jint JNICALL
Java_com_foxx_androidcast_entitlements_EntitlementNativeBridge_nativeEncode(
JNIEnv *env, jclass clazz, jlong val, jlong key, jlongArray out_encoded)
{
uint64_t encoded = 0;
int32_t rv;
jlong slot;
(void)clazz;
if (out_encoded == NULL)
return throw_illegal_state(env, "out_encoded is null");
rv = encode((uint64_t)val, (uint64_t)key, &encoded);
if (rv != OBF_OK)
return rv;
slot = (jlong)encoded;
(*env)->SetLongArrayRegion(env, out_encoded, 0, 1, &slot);
return OBF_OK;
}

8
ndk/obfuscate/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Entitlements obfuscation (C)
Shared by `libandroidcast_entitlements.so` and the archived spike under `reference-design/obfuscate_entitlements/`.
- `encode` / `decode` — 40-bit payload + 24-bit verification tag, keyed by per-account `uint64_t` **key** (not wall clock).
- `write_to` / `read_from` — file layout: 32-bit dummy header, N×uint64_t payloads, 32-bit dummy tail.
Reference demo: `tmp/obfuscate_test/` (standalone; not wired into the main app).

122
ndk/obfuscate/obfuscate.c Normal file
View File

@@ -0,0 +1,122 @@
#include "obfuscate.h"
#include <stddef.h>
static uint16_t rotl16(uint16_t x, unsigned int n)
{
n &= 15u;
return (uint16_t)((x << n) | (x >> (16u - n)));
}
static uint64_t rotl40(uint64_t x, unsigned int n)
{
n %= OBF_PAYLOAD_BITS;
x &= OBF_PAYLOAD_MASK;
if (n == 0u)
return x;
return ((x << n) | (x >> (OBF_PAYLOAD_BITS - n))) & OBF_PAYLOAD_MASK;
}
static uint64_t rotr40(uint64_t x, unsigned int n)
{
n %= OBF_PAYLOAD_BITS;
x &= OBF_PAYLOAD_MASK;
if (n == 0u)
return x;
return ((x >> n) | (x << (OBF_PAYLOAD_BITS - n))) & OBF_PAYLOAD_MASK;
}
/* Lightweight seed mixer — same output for same seed on any platform. */
static uint64_t derive_key(uint64_t seed)
{
uint64_t x = seed ^ 0xA5A5A5A5A5A5A5A5ULL;
x ^= x >> 33;
x *= 0xFF51AFD7ED558CCDULL;
x ^= x >> 33;
x *= 0xC4CEB9FE1A85EC53ULL;
x ^= x >> 33;
return x & OBF_PAYLOAD_MASK;
}
static unsigned int derive_rotation(uint64_t seed)
{
uint64_t mix = seed * 0x9E3779B97F4A7C15ULL;
return (unsigned int)((mix >> 59) + 1u); /* 1..8 */
}
/*
* 24-bit FEC-style tag: four 16-bit shards XOR-mixed with key and seed.
* Detects single-bit flips and wrong seed with high probability.
*/
static uint32_t compute_tag(uint64_t val, uint64_t seed)
{
uint64_t k = derive_key(seed);
uint64_t a = (val & 0xFFFFULL) ^ (k & 0xFFFFULL);
uint64_t b = ((val >> 16) & 0xFFFFULL) ^ ((k >> 16) & 0xFFFFULL);
uint64_t c = ((val >> 32) & 0xFFFFULL) ^ ((k >> 32) & 0xFFFFULL);
uint64_t d = ((val >> 48) & 0xFFFFULL) ^ ((k >> 48) & 0xFFFFULL);
uint64_t p1 = a ^ rotl16((uint16_t)b, 3) ^ c;
uint64_t p2 = b ^ rotl16((uint16_t)c, 7) ^ d;
uint64_t p3 = c ^ rotl16((uint16_t)d, 11) ^ a;
uint64_t mix = (p1 * 0x01000193ULL) ^ (p2 << 16) ^ (p3 << 8);
mix ^= seed;
mix *= 0x85EBCA77C2B2AE63ULL;
mix ^= mix >> 33;
return (uint32_t)(mix & 0xFFFFFFULL);
}
static uint64_t obfuscate(uint64_t val, uint64_t seed)
{
uint64_t key = derive_key(seed);
unsigned int rot = derive_rotation(seed);
return rotl40(val & OBF_PAYLOAD_MASK, rot) ^ key;
}
static uint64_t deobfuscate(uint64_t obscured, uint64_t seed)
{
uint64_t key = derive_key(seed);
unsigned int rot = derive_rotation(seed);
return rotr40(obscured ^ key, rot);
}
int32_t encode(uint64_t val, uint64_t seed, uint64_t *out)
{
uint64_t packed;
uint32_t tag;
if (out == NULL)
return OBF_ERR_NULL;
if (val > OBF_PAYLOAD_MASK)
return OBF_ERR_RANGE;
tag = compute_tag(val, seed);
packed = ((uint64_t)tag << OBF_PAYLOAD_BITS) | obfuscate(val, seed);
*out = packed;
return OBF_OK;
}
int32_t decode(uint64_t encoded_val, uint64_t seed, uint64_t *out)
{
uint32_t stored_tag;
uint32_t expected_tag;
uint64_t obscured;
uint64_t val;
if (out == NULL)
return OBF_ERR_NULL;
stored_tag = (uint32_t)(encoded_val >> OBF_PAYLOAD_BITS);
obscured = encoded_val & OBF_PAYLOAD_MASK;
val = deobfuscate(obscured, seed);
expected_tag = compute_tag(val, seed);
if (stored_tag != expected_tag)
return OBF_ERR_VERIFY;
*out = val;
return OBF_OK;
}

30
ndk/obfuscate/obfuscate.h Normal file
View File

@@ -0,0 +1,30 @@
#ifndef OBFUSCATE_H
#define OBFUSCATE_H
#include <stdint.h>
/* Payload uses lower 40 bits; upper 24 bits hold verification tag. */
#define OBF_PAYLOAD_BITS 40u
#define OBF_PAYLOAD_MASK ((1ULL << OBF_PAYLOAD_BITS) - 1ULL)
/* Return codes */
#define OBF_OK 0
#define OBF_ERR_NULL -1 /* out pointer is NULL */
#define OBF_ERR_RANGE -2 /* val exceeds encodable payload width */
#define OBF_ERR_VERIFY -3 /* tag mismatch (wrong seed or corrupted data) */
/*
* Encode uint64_t payload (e.g. flag bits) using seed-derived keystream.
* seed is typically unix time in whole seconds shared by encoder and decoder.
*
* On success writes single uint64_t to *out and returns OBF_OK.
*/
int32_t encode(uint64_t val, uint64_t seed, uint64_t *out);
/*
* Decode and verify. seed must match the value used at encode time.
* On success writes recovered payload to *out and returns OBF_OK.
*/
int32_t decode(uint64_t encoded_val, uint64_t seed, uint64_t *out);
#endif /* OBFUSCATE_H */

View File

@@ -0,0 +1,215 @@
#include "obfuscate_file.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
static uint32_t dummy_u32(uint64_t seed, uint32_t salt)
{
uint64_t x = seed ^ ((uint64_t)salt * 0x9E3779B97F4A7C15ULL);
x ^= x >> 33;
x *= 0xFF51AFD7ED558CCDULL;
x ^= x >> 33;
return (uint32_t)(x ^ (x >> 32));
}
static void put_u32_le(uint8_t *dst, uint32_t v)
{
dst[0] = (uint8_t)(v);
dst[1] = (uint8_t)(v >> 8);
dst[2] = (uint8_t)(v >> 16);
dst[3] = (uint8_t)(v >> 24);
}
static void put_u64_le(uint8_t *dst, uint64_t v)
{
unsigned int i;
for (i = 0; i < 8u; i++)
dst[i] = (uint8_t)(v >> (8u * i));
}
static uint32_t get_u32_le(const uint8_t *src)
{
return (uint32_t)src[0] | ((uint32_t)src[1] << 8) | ((uint32_t)src[2] << 16) |
((uint32_t)src[3] << 24);
}
static uint64_t get_u64_le(const uint8_t *src)
{
uint64_t v = 0;
unsigned int i;
for (i = 0; i < 8u; i++)
v |= (uint64_t)src[i] << (8u * i);
return v;
}
int32_t obf_file_payload_count(long file_size, size_t *count_out)
{
long payload_bytes;
if (file_size < 0)
return OBF_ERR_IO;
if ((unsigned long)file_size < OBF_FILE_OVERHEAD + sizeof(uint64_t))
return OBF_ERR_FORMAT;
payload_bytes = file_size - (long)OBF_FILE_OVERHEAD;
if (payload_bytes % (long)sizeof(uint64_t) != 0)
return OBF_ERR_FORMAT;
*count_out = (size_t)payload_bytes / sizeof(uint64_t);
return OBF_OK;
}
static int32_t write_encoded_file(const char *path, const uint64_t *encoded, size_t count,
uint64_t seed)
{
FILE *fp;
uint8_t hdr[OBF_FILE_HDR_SIZE];
uint8_t tail[OBF_FILE_TAIL_SIZE];
uint8_t block[sizeof(uint64_t)];
size_t i;
if (path == NULL || encoded == NULL || count == 0)
return OBF_ERR_NULL;
fp = fopen(path, "wb");
if (fp == NULL)
return OBF_ERR_IO;
put_u32_le(hdr, dummy_u32(seed, 0xA11CEu));
if (fwrite(hdr, 1, sizeof(hdr), fp) != sizeof(hdr)) {
fclose(fp);
return OBF_ERR_IO;
}
for (i = 0; i < count; i++) {
put_u64_le(block, encoded[i]);
if (fwrite(block, 1, sizeof(block), fp) != sizeof(block)) {
fclose(fp);
return OBF_ERR_IO;
}
}
put_u32_le(tail, dummy_u32(seed, 0x7A1Lu));
if (fwrite(tail, 1, sizeof(tail), fp) != sizeof(tail)) {
fclose(fp);
return OBF_ERR_IO;
}
if (fclose(fp) != 0)
return OBF_ERR_IO;
return OBF_OK;
}
int32_t write_many_to(const char *path, const uint64_t *vals, size_t count, uint64_t seed)
{
uint64_t *encoded;
size_t i;
int32_t rv;
if (path == NULL || vals == NULL || count == 0)
return OBF_ERR_NULL;
encoded = (uint64_t *)malloc(count * sizeof(uint64_t));
if (encoded == NULL)
return OBF_ERR_IO;
for (i = 0; i < count; i++) {
rv = encode(vals[i], seed, &encoded[i]);
if (rv != OBF_OK) {
free(encoded);
return rv;
}
}
rv = write_encoded_file(path, encoded, count, seed);
free(encoded);
return rv;
}
int32_t write_to(const char *path, uint64_t val, uint64_t seed)
{
return write_many_to(path, &val, 1, seed);
}
int32_t read_many_from(const char *path, uint64_t seed, uint64_t *out_vals, size_t out_cap,
size_t *out_count)
{
FILE *fp;
long file_size;
size_t count;
size_t i;
uint8_t hdr[OBF_FILE_HDR_SIZE];
uint8_t tail[OBF_FILE_TAIL_SIZE];
uint8_t block[sizeof(uint64_t)];
uint64_t encoded;
uint64_t decoded;
int32_t rv;
if (path == NULL || out_vals == NULL || out_count == NULL)
return OBF_ERR_NULL;
fp = fopen(path, "rb");
if (fp == NULL)
return OBF_ERR_IO;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return OBF_ERR_IO;
}
file_size = ftell(fp);
if (file_size < 0 || fseek(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return OBF_ERR_IO;
}
rv = obf_file_payload_count(file_size, &count);
if (rv != OBF_OK) {
fclose(fp);
return rv;
}
if (count > out_cap) {
fclose(fp);
return OBF_ERR_OVERFLOW;
}
if (fread(hdr, 1, sizeof(hdr), fp) != sizeof(hdr)) {
fclose(fp);
return OBF_ERR_IO;
}
(void)get_u32_le(hdr);
for (i = 0; i < count; i++) {
if (fread(block, 1, sizeof(block), fp) != sizeof(block)) {
fclose(fp);
return OBF_ERR_IO;
}
encoded = get_u64_le(block);
rv = decode(encoded, seed, &decoded);
if (rv != OBF_OK) {
fclose(fp);
return rv;
}
out_vals[i] = decoded;
}
if (fread(tail, 1, sizeof(tail), fp) != sizeof(tail)) {
fclose(fp);
return OBF_ERR_IO;
}
(void)get_u32_le(tail);
fclose(fp);
*out_count = count;
return OBF_OK;
}
int32_t read_from(const char *path, uint64_t seed, uint64_t *out)
{
size_t count = 0;
return read_many_from(path, seed, out, 1, &count);
}

View File

@@ -0,0 +1,28 @@
#ifndef OBFUSCATE_FILE_H
#define OBFUSCATE_FILE_H
#include "obfuscate.h"
#include <stddef.h>
#include <stdint.h>
/* 32-bit dummy header/tail — skipped by decode; only uint64_t payloads are processed. */
#define OBF_FILE_HDR_SIZE 4u
#define OBF_FILE_TAIL_SIZE 4u
#define OBF_FILE_OVERHEAD (OBF_FILE_HDR_SIZE + OBF_FILE_TAIL_SIZE)
#define OBF_ERR_IO -4
#define OBF_ERR_FORMAT -5
#define OBF_ERR_OVERFLOW -6
int32_t write_to(const char *path, uint64_t val, uint64_t seed);
int32_t read_from(const char *path, uint64_t seed, uint64_t *out);
int32_t write_many_to(const char *path, const uint64_t *vals, size_t count, uint64_t seed);
int32_t read_many_from(const char *path, uint64_t seed, uint64_t *out_vals, size_t out_cap,
size_t *out_count);
/* How many uint64_t payloads are stored (excludes 32-bit head/tail). */
int32_t obf_file_payload_count(long file_size, size_t *count_out);
#endif /* OBFUSCATE_FILE_H */