From 6639bcd799622cf711d5a588bb61abe272a889b8 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Mar 2026 15:34:03 +0200 Subject: [PATCH] major updates to device and links management in admin --- AGENTS.md | 2 + README.md | 23 +- apps/front/package.json | 2 +- apps/main/src/lib/core/constants.ts | 18 +- .../device/device-details.vm.svelte.ts | 80 ++ .../domains/device/device-edit-form.svelte | 0 .../src/lib/domains/device/device-form.svelte | 134 +++ .../lib/domains/device/device.vm.svelte.ts | 54 +- apps/main/src/lib/domains/link/link.remote.ts | 7 +- .../src/lib/domains/link/link.vm.svelte.ts | 8 +- .../src/routes/(main)/dashboard/+page.svelte | 502 +++++++++- .../routes/(main)/dashboard/[id]/+page.svelte | 388 ++++++++ .../src/routes/(main)/devices/+page.svelte | 399 -------- .../routes/(main)/devices/[id]/+page.svelte | 1 - .../main/src/routes/(main)/links/+page.svelte | 72 +- dockerfiles/front.Dockerfile | 27 + dockerfiles/main.Dockerfile | 2 +- dockerfiles/orchestrator.Dockerfile | 27 + memory.log.md | 52 +- .../migrations/0001_mysterious_thor_girl.sql | 3 + .../0002_remarkable_charles_xavier.sql | 2 + .../db/migrations/meta/0001_snapshot.json | 939 ++++++++++++++++++ .../db/migrations/meta/0002_snapshot.json | 939 ++++++++++++++++++ packages/db/migrations/meta/_journal.json | 14 + packages/db/schema/device.schema.ts | 5 +- packages/db/schema/link.schema.ts | 11 +- packages/logic/domains/device/controller.ts | 12 +- packages/logic/domains/device/data.ts | 14 +- packages/logic/domains/device/repository.ts | 5 +- packages/logic/domains/link/data.ts | 8 +- packages/logic/domains/link/repository.ts | 2 + packages/settings/index.ts | 48 +- 32 files changed, 3304 insertions(+), 496 deletions(-) create mode 100644 apps/main/src/lib/domains/device/device-details.vm.svelte.ts create mode 100644 apps/main/src/lib/domains/device/device-edit-form.svelte create mode 100644 apps/main/src/lib/domains/device/device-form.svelte create mode 100644 apps/main/src/routes/(main)/dashboard/[id]/+page.svelte delete mode 100644 apps/main/src/routes/(main)/devices/+page.svelte delete mode 100644 apps/main/src/routes/(main)/devices/[id]/+page.svelte create mode 100644 dockerfiles/front.Dockerfile create mode 100644 dockerfiles/orchestrator.Dockerfile create mode 100644 packages/db/migrations/0001_mysterious_thor_girl.sql create mode 100644 packages/db/migrations/0002_remarkable_charles_xavier.sql create mode 100644 packages/db/migrations/meta/0001_snapshot.json create mode 100644 packages/db/migrations/meta/0002_snapshot.json 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(null); + loading = $state(false); + currentId = $state(null); + + get streamUrl(): string | null { + if (!this.device) return null; + return normalizeViewerUrl(this.device.host, this.device.wsPort); + } + + async fetchDevice(id: number) { + this.loading = true; + this.currentId = id; + + try { + const result = await getDeviceByIdSQ({ id }); + if (result?.error || !result?.data) { + this.device = null; + toast.error(result?.error?.message || "Failed to fetch device", { + description: result?.error?.description || "Please try again", + }); + return; + } + + this.device = result.data as Device; + } catch (error) { + this.device = null; + toast.error("Failed to fetch device", { + description: + error instanceof Error ? error.message : "Please try again", + }); + } finally { + this.loading = false; + } + } +} + +export const deviceDetailsVM = new DeviceDetailsViewModel(); diff --git a/apps/main/src/lib/domains/device/device-edit-form.svelte b/apps/main/src/lib/domains/device/device-edit-form.svelte new file mode 100644 index 0000000..e69de29 diff --git a/apps/main/src/lib/domains/device/device-form.svelte b/apps/main/src/lib/domains/device/device-form.svelte new file mode 100644 index 0000000..76b60d8 --- /dev/null +++ b/apps/main/src/lib/domains/device/device-form.svelte @@ -0,0 +1,134 @@ + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + {#if showAdvanced} +
+ + +
+ {/if} +
+ + {#if showAdvanced} +
+ + +
+ {/if} + +
+ {#if oncancel} + + {/if} + +
+
diff --git a/apps/main/src/lib/domains/device/device.vm.svelte.ts b/apps/main/src/lib/domains/device/device.vm.svelte.ts index fb67702..f4303cd 100644 --- a/apps/main/src/lib/domains/device/device.vm.svelte.ts +++ b/apps/main/src/lib/domains/device/device.vm.svelte.ts @@ -3,6 +3,7 @@ import { createDeviceSC, deleteDeviceSC, setDeviceStatusSC, + updateDeviceSC, } from "./device.remote"; import { toast } from "svelte-sonner"; @@ -12,9 +13,10 @@ type Device = { version: string; status: string; isActive: boolean; - containerId: string | null; + inUse: boolean; + containerId: string; host: string; - wsPort: string | null; + wsPort: string; createdAt: Date; updatedAt: Date; }; @@ -23,17 +25,24 @@ type CreateDeviceInput = { title: string; version: string; host: string; - containerId?: string; - wsPort?: string; + containerId: string; + wsPort: string; isActive?: boolean; }; +type UpdateDeviceInput = CreateDeviceInput & { + status: "online" | "offline" | "busy" | "error"; + inUse: boolean; +}; + class DeviceViewModel { devices = $state([]); loading = $state(false); creating = $state(false); + updating = $state(false); deletingId = $state(null); showCreateDialog = $state(false); + editingId = $state(null); async fetchDevices() { this.loading = true; @@ -123,6 +132,43 @@ class DeviceViewModel { } } + async updateDevice(id: number, data: UpdateDeviceInput): Promise { + this.updating = true; + this.editingId = id; + try { + const result = await updateDeviceSC({ id, data }); + if (result?.error) { + toast.error( + result.error.message || "Failed to update device", + { + description: + result.error.description || "Please try again", + }, + ); + return false; + } + toast.success("Device updated"); + if (result.data) { + const updated = result.data as Device; + this.devices = this.devices.map((device) => + device.id === id ? updated : device, + ); + } + return true; + } catch (error) { + toast.error("Failed to update device", { + description: + error instanceof Error + ? error.message + : "Please try again", + }); + return false; + } finally { + this.updating = false; + this.editingId = null; + } + } + async setStatus(id: number, status: string) { try { const result = await setDeviceStatusSC({ diff --git a/apps/main/src/lib/domains/link/link.remote.ts b/apps/main/src/lib/domains/link/link.remote.ts index df65480..981bad7 100644 --- a/apps/main/src/lib/domains/link/link.remote.ts +++ b/apps/main/src/lib/domains/link/link.remote.ts @@ -1,5 +1,5 @@ import { getLinkController } from "@pkg/logic/domains/link/controller"; -import { updateLinkSchema } from "@pkg/logic/domains/link/data"; +import { createLinkSchema, updateLinkSchema } from "@pkg/logic/domains/link/data"; import { getFlowExecCtxForRemoteFuncs, unauthorized } from "$lib/core/server.utils"; import { command, getRequestEvent, query } from "$app/server"; import * as v from "valibot"; @@ -32,10 +32,7 @@ export const getLinkByIdSQ = query( ); export const createLinkSC = command( - v.object({ - linkedDeviceId: v.optional(v.nullable(v.number())), - expiresAt: v.optional(v.nullable(v.date())), - }), + v.omit(createLinkSchema, ["token"]), async (payload) => { const event = getRequestEvent(); const fctx = await getFlowExecCtxForRemoteFuncs(event.locals); diff --git a/apps/main/src/lib/domains/link/link.vm.svelte.ts b/apps/main/src/lib/domains/link/link.vm.svelte.ts index e1c8c87..844cdfb 100644 --- a/apps/main/src/lib/domains/link/link.vm.svelte.ts +++ b/apps/main/src/lib/domains/link/link.vm.svelte.ts @@ -12,6 +12,8 @@ type Link = { id: number; token: string; status: string; + appName: string; + appPackage: string; linkedDeviceId: number | null; expiresAt: Date | null; lastAccessedAt: Date | null; @@ -24,6 +26,7 @@ type DeviceOption = { title: string; host: string; status: string; + inUse: boolean; }; class LinkViewModel { @@ -71,6 +74,7 @@ class LinkViewModel { title: d.title, host: d.host, status: d.status, + inUse: d.inUse, })); } } catch { @@ -79,7 +83,9 @@ class LinkViewModel { } async createLink(data: { - linkedDeviceId?: number | null; + linkedDeviceId: number; + appName: string; + appPackage: string; expiresAt?: Date | null; }): Promise { this.creating = true; diff --git a/apps/main/src/routes/(main)/dashboard/+page.svelte b/apps/main/src/routes/(main)/dashboard/+page.svelte index 0ad2a2d..06d50c8 100644 --- a/apps/main/src/routes/(main)/dashboard/+page.svelte +++ b/apps/main/src/routes/(main)/dashboard/+page.svelte @@ -1,11 +1,511 @@ - dunno + + +
+
+ + Devices + + {deviceVM.devices.length} total + +
+
+ + +
+
+
+ + + {#if !deviceVM.loading && deviceVM.devices.length === 0} +
+ No devices registered yet. +
+ {:else} + +
+ {#each deviceVM.devices as device (device.id)} +
+
+ + + + {device.status} + +
+
+
+

Version

+

{device.version}

+
+
+

Active

+

{device.isActive ? "Yes" : "No"}

+
+
+

In Use

+

{device.inUse ? "Yes" : "No"}

+
+
+

+ Container +

+

{device.containerId}

+
+
+

+ WS Port +

+

{device.wsPort}

+
+
+
+ + + + + + + + + Delete device? + + + This will permanently remove + "{device.title}" and unlink it + from any associated links. + + + + + Cancel + + + deviceVM.deleteDevice( + device.id, + )} + > + Delete + + + + +
+
+ {/each} +
+ + + + {/if} +
+
+ + + + + + Add Device + + Register a new Docker-Android instance. + + + (deviceVM.showCreateDialog = false)} + /> + + + + + + + Edit Device + + Update connection details or manually override device state. + + + { + showEditDialog = false; + resetEditForm(); + }} + /> + + diff --git a/apps/main/src/routes/(main)/dashboard/[id]/+page.svelte b/apps/main/src/routes/(main)/dashboard/[id]/+page.svelte new file mode 100644 index 0000000..ebba701 --- /dev/null +++ b/apps/main/src/routes/(main)/dashboard/[id]/+page.svelte @@ -0,0 +1,388 @@ + + + + + +
+
+
+ + + {currentDevice?.title || `Device #${page.params.id}`} + +
+ + {#if currentDevice} +
+ + {currentDevice.status} + + + {currentDevice.isActive ? "Active" : "Inactive"} + + + {currentDevice.inUse ? "In Use" : "Available"} + + Version {currentDevice.version} +
+ {/if} +
+ +
+ + + {#if streamUrl} + + + Open Stream + + {/if} +
+
+
+ + + {#if deviceDetailsVM.loading && !currentDevice} +
+ + + + + + + + + + + + + + + + + +
+ + + + + + {:else if !currentDevice} +
+

Device not found

+

+ This device could not be loaded from the admin API. +

+
+ {:else} + +
+ + +
+ + + Metadata + +
+
+ +
+
+

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"} +

+
+
+
+
+ + + +
+ + + Edit Device + +
+ + Manually update connection details or override + isActive, + inUse, and + status when needed. + +
+ + + +
+
+ + + + +
+
+ + + Live Device Session + +
+ {#if streamUrl} + + + Pop out + + {/if} +
+ + Full interactive device access is embedded here so + admins do not need to leave the dashboard. + +
+ + {#if streamUrl} +
+ +
+ {:else} +
+

+ Stream URL unavailable +

+

+ Save a valid ws-scrcpy host or host/port for + this device to embed the live session here. +

+
+ {/if} +
+
+ {/if} +
+
+
diff --git a/apps/main/src/routes/(main)/devices/+page.svelte b/apps/main/src/routes/(main)/devices/+page.svelte deleted file mode 100644 index 8f55d2b..0000000 --- a/apps/main/src/routes/(main)/devices/+page.svelte +++ /dev/null @@ -1,399 +0,0 @@ - - - - - -
-
- - Devices - - {deviceVM.devices.length} total - -
-
- - -
-
-
- - - {#if !deviceVM.loading && deviceVM.devices.length === 0} -
- No devices registered yet. -
- {:else} - -
- {#each deviceVM.devices as device (device.id)} -
-
-
-

- {device.title} -

-

- {device.host} -

-
- - - {device.status} - -
-
-
-

Version

-

{device.version}

-
-
-

Active

-

{device.isActive ? "Yes" : "No"}

-
-
-

- Container -

-

- {device.containerId || "—"} -

-
-
-

- WS Port -

-

{device.wsPort || "—"}

-
-
-
- - - - - - - - Delete device? - - - This will permanently remove - "{device.title}" and unlink it - from any associated links. - - - - - Cancel - - - deviceVM.deleteDevice( - device.id, - )} - > - Delete - - - - -
-
- {/each} -
- - - - {/if} -
-
-
- - - - - - Add Device - - Register a new Docker-Android instance. - - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- - -
- - - - -
-
-
diff --git a/apps/main/src/routes/(main)/devices/[id]/+page.svelte b/apps/main/src/routes/(main)/devices/[id]/+page.svelte deleted file mode 100644 index 5714613..0000000 --- a/apps/main/src/routes/(main)/devices/[id]/+page.svelte +++ /dev/null @@ -1 +0,0 @@ -device id page diff --git a/apps/main/src/routes/(main)/links/+page.svelte b/apps/main/src/routes/(main)/links/+page.svelte index 5bc8e9d..a1f4a8e 100644 --- a/apps/main/src/routes/(main)/links/+page.svelte +++ b/apps/main/src/routes/(main)/links/+page.svelte @@ -9,7 +9,7 @@ import { Label } from "$lib/components/ui/label"; import * as Table from "$lib/components/ui/table"; import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte"; - import { mainNavTree } from "$lib/core/constants"; + import { mainNavTree, SUPPORTED_APPS } from "$lib/core/constants"; import { linkVM } from "$lib/domains/link/link.vm.svelte"; import { breadcrumbs } from "$lib/global.stores"; import LinkIcon from "@lucide/svelte/icons/link"; @@ -24,6 +24,7 @@ breadcrumbs.set([mainNavTree[0], mainNavTree[1]]); let selectedDeviceId = $state(""); + let selectedAppPackage = $state(""); let expiresAt = $state(""); onMount(async () => { @@ -33,15 +34,25 @@ function resetForm() { selectedDeviceId = ""; + selectedAppPackage = ""; expiresAt = ""; } + function getSupportedApp(packageName: string) { + return SUPPORTED_APPS.find((app) => app.packageName === packageName); + } + async function handleCreate(e: Event) { e.preventDefault(); + const selectedApp = getSupportedApp(selectedAppPackage); + if (!selectedApp) { + toast.error("Select a supported app"); + return; + } const success = await linkVM.createLink({ - linkedDeviceId: selectedDeviceId - ? Number(selectedDeviceId) - : null, + linkedDeviceId: Number(selectedDeviceId), + appName: selectedApp.title, + appPackage: selectedApp.packageName, expiresAt: expiresAt ? new Date(expiresAt) : null, }); if (success) resetForm(); @@ -163,6 +174,15 @@ )}

+
+

App

+

{link.appName}

+

+ {link.appPackage} +

+

Expires

{formatDate(link.expiresAt)}

@@ -238,6 +258,7 @@ Token Status Device + Leased App Expires Last Accessed Created @@ -277,6 +298,18 @@ link.linkedDeviceId, )} + +
+

+ {link.appName} +

+

+ {link.appPackage} +

+
+
{formatDate(link.expiresAt)} @@ -373,21 +406,44 @@
- +

- Optional. You can assign a device later. + Links now target one specific device. +

+
+
+ + +

+ Links can only target apps from the current supported app + list.

diff --git a/dockerfiles/front.Dockerfile b/dockerfiles/front.Dockerfile new file mode 100644 index 0000000..a59d75f --- /dev/null +++ b/dockerfiles/front.Dockerfile @@ -0,0 +1,27 @@ +FROM node:25.6.1 AS production + +RUN npm i -g pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ + +COPY apps/front/package.json ./apps/front/package.json + +COPY packages ./packages + +RUN pnpm install + +COPY apps/front ./apps/front + +RUN pnpm install + +RUN pnpm run build + +COPY scripts/prod.start.sh ./scripts/prod.start.sh + +EXPOSE 3000 + +RUN chmod +x scripts/prod.start.sh + +CMD ["/bin/sh", "scripts/prod.start.sh", "apps/front"] diff --git a/dockerfiles/main.Dockerfile b/dockerfiles/main.Dockerfile index 41eb13d..0d03f99 100644 --- a/dockerfiles/main.Dockerfile +++ b/dockerfiles/main.Dockerfile @@ -18,7 +18,7 @@ RUN pnpm install RUN pnpm run build -COPY scripts ./scripts +COPY scripts/prod.start.sh ./scripts/prod.start.sh EXPOSE 3000 diff --git a/dockerfiles/orchestrator.Dockerfile b/dockerfiles/orchestrator.Dockerfile new file mode 100644 index 0000000..6bc962c --- /dev/null +++ b/dockerfiles/orchestrator.Dockerfile @@ -0,0 +1,27 @@ +FROM node:25.6.1 AS production + +RUN npm i -g pnpm + +WORKDIR /app + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ + +COPY apps/orchestrator/package.json ./apps/orchestrator/package.json + +COPY packages ./packages + +RUN pnpm install + +COPY apps/orchestrator ./apps/orchestrator + +RUN pnpm install + +RUN pnpm run build + +COPY scripts/prod.start.sh ./scripts/prod.start.sh + +EXPOSE 3000 + +RUN chmod +x scripts/prod.start.sh + +CMD ["/bin/sh", "scripts/prod.start.sh", "apps/orchestrator"] diff --git a/memory.log.md b/memory.log.md index 4c7c5a6..e6df3c1 100644 --- a/memory.log.md +++ b/memory.log.md @@ -11,7 +11,7 @@ Update rule: --- -### 0 (genesis) +### 0 - Genesis - init @@ -30,7 +30,7 @@ Update rule: - Device CRUD page at /devices — list table, create dialog, delete with confirmation, status dots, refresh - Link CRUD page at /links — list table, generate link dialog (device select + expiry), revoke, delete, copy token - Remote functions for both domains (*SQ queries, *SC commands) -- View models (*.vm.svelte.ts) with optimistic UI updates (create appends, delete filters locally) +- View models (\*.vm.svelte.ts) with optimistic UI updates (create appends, delete filters locally) - Fixed bug: table not refreshing after create (was SvelteKit query cache — switched to optimistic local state) - Fixed bug: /links auth failure (hooks.server.ts path check matching "/link" inside "/links") @@ -42,3 +42,51 @@ Update rule: - Deployed via Dokploy with Traefik domain routing - Networking: redroid on bridge, ws-scrcpy on dokploy overlay — connected via host bridge gateway (172.17.0.1:5555) - ws-scrcpy live and streaming redroid at iotam-ws-scrcpy.snapyra.com + +### 4 — Device Detail Page + +- Implemented `/devices/[id]` admin detail page with metadata, refresh/open actions, and an embedded live ws-scrcpy iframe +- Added a dedicated device details VM and derived the viewer URL from stored device host/wsPort data +- Updated `/devices` so device entries navigate into their detail page + +### 5 — README Product Flow Update + +- Updated the implementation plan to reflect the app-leasing model: links target a specific app on a device, front triggers orchestrator reset/launch during loading, and devices need explicit `inUse` tracking + +### 6 — Admin App Leasing Model + +- Added `inUse` to the device schema/domain and surfaced it in the devices admin list/detail views +- Extended links to store required leased app data (`appName`, `appPackage`) alongside the assigned device +- Updated the admin links creation flow and listing UI so links are created against a specific device and app package + +### 7 — Required Device Connection Fields + +- Propagated `containerId` and `wsPort` as required device fields across validation, repository create logic, and the admin device creation form to match the non-null schema + +### 8 — Reused Device Edit Form + +- Added a shared device form component under the device domain and reused it for create/edit flows +- Added manual device editing in the devices list and device detail page, including overrides for host, container ID, ws port, status, `isActive`, and `inUse` + +### 9 — Supported App Link Creation + +- Updated admin link creation to select from `SUPPORTED_APPS` in constants instead of accepting freeform app name/package input + +### 10 — Device Detail Page Layout Improvement + +- Restructured device detail layout: Metadata + Edit Device side-by-side in top row, Live Device Session full-width below +- Iframe now uses `h-[75vh] min-h-[480px]` instead of fixed 720px height, making it fill available space +- Added "Pop out" link in the Live Device Session card header for quick external access +- Tightened metadata card with inline grid rows (port/in-use, created/updated) and smaller labels +- Styled backtick code refs in Edit Device description with `` tags +- Updated skeleton loading state to match the new two-row layout + +### 11 — Merged Devices Page into Dashboard + +- Moved device list (table, create/edit/delete dialogs) from `/devices` into `/dashboard` — dashboard is now the devices home +- Moved device detail page from `/devices/[id]` to `/dashboard/[id]` +- Removed `/devices` route directory entirely +- Removed "Devices" entry from `mainNavTree` sidebar navigation (was index 2) +- Cleaned up unused `Smartphone` import from constants.ts +- Updated detail page breadcrumbs to go Dashboard > Device instead of Dashboard > Devices > Device +- Updated `goto` calls to navigate to `/dashboard/:id` instead of `/devices/:id` diff --git a/packages/db/migrations/0001_mysterious_thor_girl.sql b/packages/db/migrations/0001_mysterious_thor_girl.sql new file mode 100644 index 0000000..95ffc38 --- /dev/null +++ b/packages/db/migrations/0001_mysterious_thor_girl.sql @@ -0,0 +1,3 @@ +ALTER TABLE "device" ADD COLUMN "in_use" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "link" ADD COLUMN "app_name" text NOT NULL;--> statement-breakpoint +ALTER TABLE "link" ADD COLUMN "app_package" text NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/0002_remarkable_charles_xavier.sql b/packages/db/migrations/0002_remarkable_charles_xavier.sql new file mode 100644 index 0000000..93c4c10 --- /dev/null +++ b/packages/db/migrations/0002_remarkable_charles_xavier.sql @@ -0,0 +1,2 @@ +ALTER TABLE "device" ALTER COLUMN "container_id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "device" ALTER COLUMN "ws_port" SET NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0001_snapshot.json b/packages/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..ae614e8 --- /dev/null +++ b/packages/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,939 @@ +{ + "id": "b3c84126-2831-4271-a12a-457f0dcc8c46", + "prevId": "c0dc4466-4211-49aa-97b0-917cc0c30871", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twofa_sessions": { + "name": "twofa_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_used": { + "name": "code_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "twofa_sessions_user_id_user_id_fk": { + "name": "twofa_sessions_user_id_user_id_fk", + "tableFrom": "twofa_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "twofa_sessions_verification_token_unique": { + "name": "twofa_sessions_verification_token_unique", + "nullsNotDistinct": false, + "columns": [ + "verification_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "onboarding_done": { + "name": "onboarding_done", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last2_fa_verified_at": { + "name": "last2_fa_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "in_use": { + "name": "in_use", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ws_port": { + "name": "ws_port", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "type": { + "name": "type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_url": { + "name": "action_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_user_id_fk": { + "name": "notifications_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.link": { + "name": "link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_package": { + "name": "app_package", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linked_device_id": { + "name": "linked_device_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "link_linked_device_id_device_id_fk": { + "name": "link_linked_device_id_device_id_fk", + "tableFrom": "link", + "tableTo": "device", + "columnsFrom": [ + "linked_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "link_token_unique": { + "name": "link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "task_user_id_user_id_fk": { + "name": "task_user_id_user_id_fk", + "tableFrom": "task", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/0002_snapshot.json b/packages/db/migrations/meta/0002_snapshot.json new file mode 100644 index 0000000..3bf7844 --- /dev/null +++ b/packages/db/migrations/meta/0002_snapshot.json @@ -0,0 +1,939 @@ +{ + "id": "a633b0b6-32a7-4f7f-8b17-4264fe54ca57", + "prevId": "b3c84126-2831-4271-a12a-457f0dcc8c46", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twofa_sessions": { + "name": "twofa_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_used": { + "name": "code_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "twofa_sessions_user_id_user_id_fk": { + "name": "twofa_sessions_user_id_user_id_fk", + "tableFrom": "twofa_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "twofa_sessions_verification_token_unique": { + "name": "twofa_sessions_verification_token_unique", + "nullsNotDistinct": false, + "columns": [ + "verification_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "onboarding_done": { + "name": "onboarding_done", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last2_fa_verified_at": { + "name": "last2_fa_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "in_use": { + "name": "in_use", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ws_port": { + "name": "ws_port", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "type": { + "name": "type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_url": { + "name": "action_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_user_id_fk": { + "name": "notifications_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.link": { + "name": "link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_package": { + "name": "app_package", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "linked_device_id": { + "name": "linked_device_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "link_linked_device_id_device_id_fk": { + "name": "link_linked_device_id_device_id_fk", + "tableFrom": "link", + "tableTo": "device", + "columnsFrom": [ + "linked_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "link_token_unique": { + "name": "link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "task_user_id_user_id_fk": { + "name": "task_user_id_user_id_fk", + "tableFrom": "task", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 0777fae..e35293b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1774650657798, "tag": "0000_colorful_the_leader", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1774703256575, + "tag": "0001_mysterious_thor_girl", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1774703478082, + "tag": "0002_remarkable_charles_xavier", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/schema/device.schema.ts b/packages/db/schema/device.schema.ts index 4424e2e..0955aaf 100644 --- a/packages/db/schema/device.schema.ts +++ b/packages/db/schema/device.schema.ts @@ -15,10 +15,11 @@ export const device = pgTable("device", { status: varchar("status", { length: 16 }).notNull().default("offline"), // "online" | "offline" | "busy" | "error" isActive: boolean("is_active").notNull().default(false), + inUse: boolean("in_use").notNull().default(false), - containerId: text("container_id"), // Docker container ID on the VPS + containerId: text("container_id").notNull(), // Docker container ID on the VPS host: text("host").notNull(), // VPS hostname or IP - wsPort: text("ws_port"), // ws-scrcpy WebSocket port + wsPort: text("ws_port").notNull(), // ws-scrcpy WebSocket port createdAt: timestamp("created_at").notNull(), updatedAt: timestamp("updated_at").notNull(), diff --git a/packages/db/schema/link.schema.ts b/packages/db/schema/link.schema.ts index 1086640..7299374 100644 --- a/packages/db/schema/link.schema.ts +++ b/packages/db/schema/link.schema.ts @@ -1,4 +1,11 @@ -import { integer, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core"; +import { + integer, + pgTable, + serial, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; import { device } from "./device.schema"; import { relations } from "drizzle-orm"; @@ -7,6 +14,8 @@ export const link = pgTable("link", { token: text("token").notNull().unique(), status: varchar("status", { length: 16 }).notNull().default("active"), // "active" | "inactive" | "expired" | "revoked" + appName: text("app_name").notNull(), + appPackage: text("app_package").notNull(), linkedDeviceId: integer("linked_device_id").references(() => device.id, { onDelete: "set null", diff --git a/packages/logic/domains/device/controller.ts b/packages/logic/domains/device/controller.ts index 118a895..28b4132 100644 --- a/packages/logic/domains/device/controller.ts +++ b/packages/logic/domains/device/controller.ts @@ -47,10 +47,13 @@ export class DeviceController { */ allocate(fctx: FlowExecCtx, id: number): ResultAsync { return this.repo.getById(fctx, id).andThen((dev) => { - if (dev.status !== DeviceStatus.ONLINE) { + if (dev.status !== DeviceStatus.ONLINE || dev.inUse) { return errAsync(deviceErrors.deviceNotAvailable(fctx, id)); } - return this.repo.setStatus(fctx, id, DeviceStatus.BUSY); + return this.repo.update(fctx, id, { + status: DeviceStatus.BUSY, + inUse: true, + }); }); } @@ -58,7 +61,10 @@ export class DeviceController { * Release a device back to online after a session ends. */ release(fctx: FlowExecCtx, id: number): ResultAsync { - return this.repo.setStatus(fctx, id, DeviceStatus.ONLINE); + return this.repo.update(fctx, id, { + status: DeviceStatus.ONLINE, + inUse: false, + }); } } diff --git a/packages/logic/domains/device/data.ts b/packages/logic/domains/device/data.ts index d63cf66..cad24a0 100644 --- a/packages/logic/domains/device/data.ts +++ b/packages/logic/domains/device/data.ts @@ -16,9 +16,10 @@ export const deviceSchema = v.object({ version: v.string(), status: deviceStatusSchema, isActive: v.boolean(), - containerId: v.nullable(v.string()), + inUse: v.boolean(), + containerId: v.string(), host: v.string(), - wsPort: v.nullable(v.string()), + wsPort: v.string(), createdAt: v.date(), updatedAt: v.date(), }); @@ -28,8 +29,8 @@ export const createDeviceSchema = v.object({ title: v.pipe(v.string(), v.minLength(1)), version: v.pipe(v.string(), v.minLength(1)), host: v.pipe(v.string(), v.minLength(1)), - containerId: v.optional(v.string()), - wsPort: v.optional(v.string()), + containerId: v.pipe(v.string(), v.minLength(1)), + wsPort: v.pipe(v.string(), v.minLength(1)), isActive: v.optional(v.boolean()), }); export type CreateDevice = v.InferOutput; @@ -39,9 +40,10 @@ export const updateDeviceSchema = v.partial( title: v.string(), version: v.string(), host: v.string(), - containerId: v.nullable(v.string()), - wsPort: v.nullable(v.string()), + containerId: v.string(), + wsPort: v.string(), isActive: v.boolean(), + inUse: v.boolean(), status: deviceStatusSchema, }), ); diff --git a/packages/logic/domains/device/repository.ts b/packages/logic/domains/device/repository.ts index 0e2aef7..529f6cc 100644 --- a/packages/logic/domains/device/repository.ts +++ b/packages/logic/domains/device/repository.ts @@ -60,10 +60,11 @@ export class DeviceRepository { title: data.title, version: data.version, host: data.host, - containerId: data.containerId ?? null, - wsPort: data.wsPort ?? null, + containerId: data.containerId, + wsPort: data.wsPort, status: DeviceStatus.OFFLINE, isActive: data.isActive ?? false, + inUse: false, createdAt: new Date(), updatedAt: new Date(), }) diff --git a/packages/logic/domains/link/data.ts b/packages/logic/domains/link/data.ts index 7da9786..d01136a 100644 --- a/packages/logic/domains/link/data.ts +++ b/packages/logic/domains/link/data.ts @@ -15,6 +15,8 @@ export const linkSchema = v.object({ id: v.number(), token: v.string(), status: linkStatusSchema, + appName: v.string(), + appPackage: v.string(), linkedDeviceId: v.nullable(v.number()), expiresAt: v.nullable(v.date()), lastAccessedAt: v.nullable(v.date()), @@ -31,7 +33,9 @@ export type LinkWithDevice = v.InferOutput; export const createLinkSchema = v.object({ token: v.pipe(v.string(), v.minLength(1)), - linkedDeviceId: v.optional(v.nullable(v.number())), + appName: v.pipe(v.string(), v.minLength(1)), + appPackage: v.pipe(v.string(), v.minLength(1)), + linkedDeviceId: v.number(), expiresAt: v.optional(v.nullable(v.date())), }); export type CreateLink = v.InferOutput; @@ -39,6 +43,8 @@ export type CreateLink = v.InferOutput; export const updateLinkSchema = v.partial( v.object({ status: linkStatusSchema, + appName: v.string(), + appPackage: v.string(), linkedDeviceId: v.nullable(v.number()), expiresAt: v.nullable(v.date()), lastAccessedAt: v.nullable(v.date()), diff --git a/packages/logic/domains/link/repository.ts b/packages/logic/domains/link/repository.ts index 726752a..bb88b69 100644 --- a/packages/logic/domains/link/repository.ts +++ b/packages/logic/domains/link/repository.ts @@ -81,6 +81,8 @@ export class LinkRepository { .values({ token: data.token, status: "active", + appName: data.appName, + appPackage: data.appPackage, linkedDeviceId: data.linkedDeviceId ?? null, expiresAt: data.expiresAt ?? null, createdAt: new Date(), diff --git a/packages/settings/index.ts b/packages/settings/index.ts index 916479f..3929a5d 100644 --- a/packages/settings/index.ts +++ b/packages/settings/index.ts @@ -18,7 +18,8 @@ export const settingsSchema = v.object({ internalApiKey: v.string(), debugKey: v.string(), - processorApiUrl: v.string(), + orchestratorApiUrl: v.string(), + wsScrcpySvcUrl: v.string(), betterAuthUrl: v.string(), betterAuthSecret: v.string(), @@ -32,19 +33,6 @@ export const settingsSchema = v.object({ otelServiceName: v.string(), otelExporterOtlpHttpEndpoint: v.string(), - - // R2/Object Storage settings - r2BucketName: v.string(), - r2Region: v.string(), - r2Endpoint: v.string(), - r2AccessKey: v.string(), - r2SecretKey: v.string(), - r2PublicUrl: v.optional(v.string()), - - // File upload settings - maxFileSize: v.number(), - allowedMimeTypes: v.array(v.string()), - allowedExtensions: v.array(v.string()), }); export type Settings = v.InferOutput; @@ -95,15 +83,11 @@ function loadSettings(): Settings { internalApiKey: getEnv("INTERNAL_API_KEY"), debugKey: getEnv("DEBUG_KEY"), - processorApiUrl: getEnv("PROCESSOR_API_URL", "http://localhost:3000"), - appBuilderApiUrl: getEnv( - "APP_BUILDER_API_URL", - "http://localhost:3001", - ), - appBuilderAssetsPublicUrl: getEnv( - "APP_BUILDER_ASSETS_PUBLIC_URL", - "http://localhost:3001", + orchestratorApiUrl: getEnv( + "ORCHESTRATOR_API_URL", + "http://localhost:3000", ), + wsScrcpySvcUrl: getEnv("WS_SCRCPY_SVC_URL"), betterAuthUrl: getEnv("BETTER_AUTH_URL"), betterAuthSecret: getEnv("BETTER_AUTH_SECRET"), @@ -122,26 +106,6 @@ function loadSettings(): Settings { otelExporterOtlpHttpEndpoint: getEnv( "OTEL_EXPORTER_OTLP_HTTP_ENDPOINT", ), - - // R2/Object Storage settings - r2BucketName: getEnv("R2_BUCKET_NAME"), - r2Region: getEnv("R2_REGION", "auto"), - r2Endpoint: getEnv("R2_ENDPOINT"), - r2AccessKey: getEnv("R2_ACCESS_KEY"), - r2SecretKey: getEnv("R2_SECRET_KEY"), - r2PublicUrl: getEnv("R2_PUBLIC_URL") || undefined, - - // File upload settings - maxFileSize: getEnvNumber("MAX_FILE_SIZE", 10485760), // 10MB default - allowedMimeTypes: parseCommaSeparated( - getEnv( - "ALLOWED_MIME_TYPES", - "image/jpeg,image/png,image/webp,image/gif,application/pdf,text/plain", - ), - ), - allowedExtensions: parseCommaSeparated( - getEnv("ALLOWED_EXTENSIONS", "jpg,jpeg,png,webp,gif,pdf,txt"), - ), }; try {