diff --git a/AGENTS.md b/AGENTS.md
index aec3094..01caa6c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -13,6 +13,8 @@ This document defines the laws, principles, and rule sets that govern this codeb
3. **No running scripts** — Do not run build, dev, test, or migrate scripts unless explicitly approved.
4. **No touching migration files** — Do not mess with the `migrations` sql dir, as those are generated manually via drizzle orm
+5. **Log meaningful changes** — After completing any meaningful change or activity, append a numbered entry to `memory.log.md` summarizing what was done. This keeps context across sessions.
+
More rules are only to be added by the human, in case such a suggestion becomes viable.
---
diff --git a/README.md b/README.md
index 8c068af..64a44b6 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,13 @@ Currently in alpha. Greenfield. Subject to change.
## How It Works
-1. Admin generates a unique link and assigns it to a user (or a slot).
+1. Admin generates a unique link and assigns it to a specific Android app on a specific device.
2. User opens that link in their browser — served by `apps/front`.
-3. User is shown a loading screen for good UX purposes
-4. User is prompted to install the PWA.
-5. User opens the PWA — they are routed into a live stream of their assigned Android instance.
-6. Admin manages the entire fleet from `apps/main` (the dashboard), which communicates with `apps/orchestrator` running on each VPS to control Docker-Android containers.
+3. During the loading flow, `apps/front` validates the link and asks `apps/orchestrator` to reset the assigned Android session and launch the leased app.
+4. If that device is already in use by another end user, the link fails instead of taking over the session.
+5. User is prompted to install the PWA.
+6. User opens the PWA — they are routed into a live stream of their assigned Android app session.
+7. Admin manages the entire fleet from `apps/main` (the dashboard), which communicates with `apps/orchestrator` running on each VPS to control Docker-Android containers.
---
@@ -34,7 +35,7 @@ Currently in alpha. Greenfield. Subject to change.
### Device Management (Orchestrator + Admin)
-- [ ] Device schema — DB model for a device (host VPS, container ID, status, assigned session, etc.)
+- [ ] Device schema — DB model for a device (host VPS, container ID, status, `inUse`, assigned session, etc.)
- [ ] Device domain in `@pkg/logic` — controller + repository + errors
- [ ] Orchestrator command interface — secured Hono routes the admin dashboard calls:
- [ ] `POST /devices/:id/start` — start a Docker-Android container
@@ -42,17 +43,19 @@ Currently in alpha. Greenfield. Subject to change.
- [ ] `POST /devices/:id/restart` — restart a container
- [ ] `GET /devices` — list all devices and their current status
- [ ] `GET /devices/:id` — page to view the device in more detail (info, live stream feed with ws-scrcpy)
-- [ ] Device allocation logic — mark a device as in-use for a user session
-- [ ] Device release logic — free up a device when a session ends
+- [ ] Device allocation logic — atomically mark a device as `inUse` when a validated link starts a session
+- [ ] Device release logic — clear `inUse` when a session ends or fails during setup
- [ ] Admin dashboard: Devices page — list fleet, show status, trigger start/stop/restart
- [ ] Internal API key auth between `apps/main` and `apps/orchestrator`
### Link Management (Admin + Front App)
-- [ ] Link schema — DB model (unique token, expiry, status, linked device ID)
+- [ ] Link schema — DB model (unique token, expiry, status, linked device ID, leased app identity)
- [ ] Link domain in `@pkg/logic` — controller + repository + errors
-- [ ] Admin dashboard: Links page — generate links, view detail, configure linked device, revoke, delete
+- [ ] Admin dashboard: Links page — generate links, view detail, configure linked device + leased app, revoke, delete
- [ ] `apps/front`: validate incoming link token on request
+- [ ] `apps/front`: during loading, reject the link if the assigned device is already `inUse`
+- [ ] `apps/front`: call `apps/orchestrator` server-side to clean/reset the device and launch the leased app before handing off the session
- [ ] `apps/front`: return appropriate error page for invalid/expired/revoked links
- [ ] Front: keep on checking for link status change, if it gets revoked, we cutoff the connection
diff --git a/apps/front/package.json b/apps/front/package.json
index 89ff791..be52692 100644
--- a/apps/front/package.json
+++ b/apps/front/package.json
@@ -2,7 +2,7 @@
"name": "@apps/front",
"type": "module",
"scripts": {
- "dev": "PORT=3000 tsx watch src/index.ts",
+ "dev": "PORT=3001 tsx watch src/index.ts",
"build": "tsc",
"prod": "HOST=0.0.0.0 PORT=3000 tsx src/index.ts"
},
diff --git a/apps/main/src/lib/core/constants.ts b/apps/main/src/lib/core/constants.ts
index 9f1d02f..fd124f7 100644
--- a/apps/main/src/lib/core/constants.ts
+++ b/apps/main/src/lib/core/constants.ts
@@ -1,5 +1,4 @@
import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard";
-import Smartphone from "@lucide/svelte/icons/smartphone";
import { BellRingIcon, Link } from "@lucide/svelte";
import UserCircle from "~icons/lucide/user-circle";
@@ -26,11 +25,6 @@ export const mainNavTree = [
url: "/links",
icon: Link,
},
- {
- title: "Devices",
- url: "/devices",
- icon: Smartphone,
- },
] as AppSidebarItem[];
export const secondaryNavTree = [
@@ -46,6 +40,18 @@ export const secondaryNavTree = [
},
] as AppSidebarItem[];
+export const SUPPORTED_APPS = [
+ {
+ title: "Gmail",
+ packageName: "com.google.android.gm",
+ },
+ {
+ title: "Outlook",
+ packageName: "com.microsoft.outlook",
+ },
+ // will add more here when support increases
+];
+
export const COMPANY_NAME = "SaaS Template";
export const WEBSITE_URL = "https://company.com";
diff --git a/apps/main/src/lib/domains/device/device-details.vm.svelte.ts b/apps/main/src/lib/domains/device/device-details.vm.svelte.ts
new file mode 100644
index 0000000..f879e6f
--- /dev/null
+++ b/apps/main/src/lib/domains/device/device-details.vm.svelte.ts
@@ -0,0 +1,80 @@
+import { getDeviceByIdSQ } from "./device.remote";
+import { toast } from "svelte-sonner";
+
+type Device = {
+ id: number;
+ title: string;
+ version: string;
+ status: string;
+ isActive: boolean;
+ inUse: boolean;
+ containerId: string;
+ host: string;
+ wsPort: string;
+ createdAt: Date | string;
+ updatedAt: Date | string;
+};
+
+function normalizeViewerUrl(host: string, wsPort: string): string | null {
+ const trimmedHost = host.trim();
+ if (!trimmedHost) return null;
+
+ if (trimmedHost.startsWith("http://") || trimmedHost.startsWith("https://")) {
+ try {
+ const url = new URL(trimmedHost);
+ if (!url.port) url.port = wsPort;
+ return url.toString();
+ } catch {
+ return trimmedHost;
+ }
+ }
+
+ const hostWithProtocol = `https://${trimmedHost}`;
+
+ try {
+ const url = new URL(hostWithProtocol);
+ if (!url.port) url.port = wsPort;
+ return url.toString();
+ } catch {
+ return null;
+ }
+}
+
+class DeviceDetailsViewModel {
+ device = $state Version {device.version} Active {device.isActive ? "Yes" : "No"} In Use {device.inUse ? "Yes" : "No"}
+ Container
+ {device.containerId}
+ WS Port
+ {device.wsPort} Device not found
+ This device could not be loaded from the admin API.
+ Device ID {currentDevice.id} Host
+ {currentDevice.host}
+ WS Port {currentDevice.wsPort} In Use {currentDevice.inUse ? "Yes" : "No"} Container ID
+ {currentDevice.containerId}
+ Created {formatDate(currentDevice.createdAt)} Updated {formatDate(currentDevice.updatedAt)}
+ Viewer URL
+
+ {streamUrl || "Missing host configuration"}
+
+ Stream URL unavailable
+
+ Save a valid ws-scrcpy host or host/port for
+ this device to embed the live session here.
+
- {device.title}
-
- {device.host}
- Version {device.version} Active {device.isActive ? "Yes" : "No"}
- Container
-
- {device.containerId || "—"}
-
- WS Port
- {device.wsPort || "—"}isActive,
+ inUse, and
+ status when needed.
+
App
+{link.appName}
++ {link.appPackage} +
+Expires
{formatDate(link.expiresAt)}
@@ -238,6 +258,7 @@+ {link.appName} +
++ {link.appPackage} +
+