supported apps domain + some refactor of data types redundancy
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard";
|
import LayoutDashboard from "@lucide/svelte/icons/layout-dashboard";
|
||||||
|
import AppWindow from "@lucide/svelte/icons/app-window";
|
||||||
import { BellRingIcon, Link } from "@lucide/svelte";
|
import { BellRingIcon, Link } from "@lucide/svelte";
|
||||||
import UserCircle from "~icons/lucide/user-circle";
|
import UserCircle from "~icons/lucide/user-circle";
|
||||||
|
|
||||||
@@ -25,6 +26,11 @@ export const mainNavTree = [
|
|||||||
url: "/links",
|
url: "/links",
|
||||||
icon: Link,
|
icon: Link,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Supported Apps",
|
||||||
|
url: "/supported-apps",
|
||||||
|
icon: AppWindow,
|
||||||
|
},
|
||||||
] as AppSidebarItem[];
|
] as AppSidebarItem[];
|
||||||
|
|
||||||
export const secondaryNavTree = [
|
export const secondaryNavTree = [
|
||||||
@@ -40,17 +46,7 @@ export const secondaryNavTree = [
|
|||||||
},
|
},
|
||||||
] as AppSidebarItem[];
|
] as AppSidebarItem[];
|
||||||
|
|
||||||
export const SUPPORTED_APPS = [
|
export const WS_SCRCPY_URL = "https://iotam-ws-scrcpy.snapyra.com";
|
||||||
{
|
|
||||||
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 COMPANY_NAME = "SaaS Template";
|
||||||
export const WEBSITE_URL = "https://company.com";
|
export const WEBSITE_URL = "https://company.com";
|
||||||
|
|||||||
@@ -1,53 +1,48 @@
|
|||||||
|
import { WS_SCRCPY_URL } from "$lib/core/constants";
|
||||||
|
import type { Device } from "@pkg/logic/domains/device/data";
|
||||||
import { getDeviceByIdSQ } from "./device.remote";
|
import { getDeviceByIdSQ } from "./device.remote";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
|
||||||
type Device = {
|
type DeviceForUI = Omit<Device, "createdAt" | "updatedAt"> & {
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
version: string;
|
|
||||||
status: string;
|
|
||||||
isActive: boolean;
|
|
||||||
inUse: boolean;
|
|
||||||
containerId: string;
|
|
||||||
host: string;
|
|
||||||
wsPort: string;
|
|
||||||
createdAt: Date | string;
|
createdAt: Date | string;
|
||||||
updatedAt: Date | string;
|
updatedAt: Date | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeViewerUrl(host: string, wsPort: string): string | null {
|
/** Port the scrcpy server listens on inside the Android container. */
|
||||||
|
const SCRCPY_SERVER_PORT = 8886;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the ws-scrcpy hash-based stream URL for a device.
|
||||||
|
*
|
||||||
|
* Example output:
|
||||||
|
* https://iotam-ws-scrcpy.snapyra.com/#!action=stream&udid=172.17.0.1:5555
|
||||||
|
* &player=mse&ws=wss://iotam-ws-scrcpy.snapyra.com/?action=proxy-adb
|
||||||
|
* &remote=tcp:8886&udid=172.17.0.1:5555
|
||||||
|
*/
|
||||||
|
function buildStreamUrl(host: string, wsPort: string): string | null {
|
||||||
const trimmedHost = host.trim();
|
const trimmedHost = host.trim();
|
||||||
if (!trimmedHost) return null;
|
if (!trimmedHost) return null;
|
||||||
|
|
||||||
if (trimmedHost.startsWith("http://") || trimmedHost.startsWith("https://")) {
|
const udid = `${trimmedHost}:${wsPort}`;
|
||||||
try {
|
const baseWss = WS_SCRCPY_URL.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
||||||
const url = new URL(trimmedHost);
|
|
||||||
if (!url.port) url.port = wsPort;
|
|
||||||
return url.toString();
|
|
||||||
} catch {
|
|
||||||
return trimmedHost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostWithProtocol = `https://${trimmedHost}`;
|
const wsParam =
|
||||||
|
`${baseWss}/?action=proxy-adb&remote=tcp:${SCRCPY_SERVER_PORT}&udid=${encodeURIComponent(udid)}`;
|
||||||
|
|
||||||
try {
|
const hash =
|
||||||
const url = new URL(hostWithProtocol);
|
`#!action=stream&udid=${encodeURIComponent(udid)}&player=mse&ws=${encodeURIComponent(wsParam)}`;
|
||||||
if (!url.port) url.port = wsPort;
|
|
||||||
return url.toString();
|
return `${WS_SCRCPY_URL}/${hash}`;
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeviceDetailsViewModel {
|
class DeviceDetailsViewModel {
|
||||||
device = $state<Device | null>(null);
|
device = $state<DeviceForUI | null>(null);
|
||||||
loading = $state(false);
|
loading = $state(false);
|
||||||
currentId = $state<number | null>(null);
|
currentId = $state<number | null>(null);
|
||||||
|
|
||||||
get streamUrl(): string | null {
|
get streamUrl(): string | null {
|
||||||
if (!this.device) return null;
|
if (!this.device) return null;
|
||||||
return normalizeViewerUrl(this.device.host, this.device.wsPort);
|
return buildStreamUrl(this.device.host, this.device.wsPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchDevice(id: number) {
|
async fetchDevice(id: number) {
|
||||||
@@ -64,7 +59,7 @@ class DeviceDetailsViewModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.device = result.data as Device;
|
this.device = result.data as DeviceForUI;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.device = null;
|
this.device = null;
|
||||||
toast.error("Failed to fetch device", {
|
toast.error("Failed to fetch device", {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import { Input } from "$lib/components/ui/input";
|
import { Input } from "$lib/components/ui/input";
|
||||||
import { Label } from "$lib/components/ui/label";
|
import { Label } from "$lib/components/ui/label";
|
||||||
import { Switch } from "$lib/components/ui/switch";
|
import { Switch } from "$lib/components/ui/switch";
|
||||||
|
import type { DeviceStatusValue } from "@pkg/logic/domains/device/data";
|
||||||
type DeviceStatus = "online" | "offline" | "busy" | "error";
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
fieldPrefix = "device-form",
|
fieldPrefix = "device-form",
|
||||||
@@ -15,7 +14,7 @@
|
|||||||
wsPort = $bindable(""),
|
wsPort = $bindable(""),
|
||||||
isActive = $bindable(false),
|
isActive = $bindable(false),
|
||||||
inUse = $bindable(false),
|
inUse = $bindable(false),
|
||||||
status = $bindable("offline" as DeviceStatus),
|
status = $bindable("offline" as DeviceStatusValue),
|
||||||
submitLabel = "Save",
|
submitLabel = "Save",
|
||||||
submitting = false,
|
submitting = false,
|
||||||
showAdvanced = false,
|
showAdvanced = false,
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
wsPort?: string;
|
wsPort?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
inUse?: boolean;
|
inUse?: boolean;
|
||||||
status?: DeviceStatus;
|
status?: DeviceStatusValue;
|
||||||
submitLabel?: string;
|
submitLabel?: string;
|
||||||
submitting?: boolean;
|
submitting?: boolean;
|
||||||
showAdvanced?: boolean;
|
showAdvanced?: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { getDeviceController } from "@pkg/logic/domains/device/controller";
|
import { getDeviceController } from "@pkg/logic/domains/device/controller";
|
||||||
import { createDeviceSchema, updateDeviceSchema } from "@pkg/logic/domains/device/data";
|
import {
|
||||||
|
createDeviceSchema,
|
||||||
|
deviceStatusSchema,
|
||||||
|
updateDeviceSchema,
|
||||||
|
} from "@pkg/logic/domains/device/data";
|
||||||
import { getFlowExecCtxForRemoteFuncs, unauthorized } from "$lib/core/server.utils";
|
import { getFlowExecCtxForRemoteFuncs, unauthorized } from "$lib/core/server.utils";
|
||||||
import { command, getRequestEvent, query } from "$app/server";
|
import { command, getRequestEvent, query } from "$app/server";
|
||||||
import * as v from "valibot";
|
import * as v from "valibot";
|
||||||
@@ -73,14 +77,14 @@ export const deleteDeviceSC = command(
|
|||||||
export const setDeviceStatusSC = command(
|
export const setDeviceStatusSC = command(
|
||||||
v.object({
|
v.object({
|
||||||
id: v.number(),
|
id: v.number(),
|
||||||
status: v.picklist(["online", "offline", "busy", "error"]),
|
status: deviceStatusSchema,
|
||||||
}),
|
}),
|
||||||
async (payload) => {
|
async (payload) => {
|
||||||
const event = getRequestEvent();
|
const event = getRequestEvent();
|
||||||
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
|
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
|
||||||
if (!fctx.userId) return unauthorized(fctx);
|
if (!fctx.userId) return unauthorized(fctx);
|
||||||
|
|
||||||
const res = await dc.setStatus(fctx, payload.id, payload.status as any);
|
const res = await dc.setStatus(fctx, payload.id, payload.status);
|
||||||
return res.isOk()
|
return res.isOk()
|
||||||
? { data: res.value, error: null }
|
? { data: res.value, error: null }
|
||||||
: { data: null, error: res.error };
|
: { data: null, error: res.error };
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import {
|
||||||
|
type CreateDevice,
|
||||||
|
type Device,
|
||||||
|
type DeviceStatusValue,
|
||||||
|
type UpdateDevice,
|
||||||
|
} from "@pkg/logic/domains/device/data";
|
||||||
import {
|
import {
|
||||||
listDevicesSQ,
|
listDevicesSQ,
|
||||||
createDeviceSC,
|
createDeviceSC,
|
||||||
@@ -7,34 +13,6 @@ import {
|
|||||||
} from "./device.remote";
|
} from "./device.remote";
|
||||||
import { toast } from "svelte-sonner";
|
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;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CreateDeviceInput = {
|
|
||||||
title: string;
|
|
||||||
version: string;
|
|
||||||
host: string;
|
|
||||||
containerId: string;
|
|
||||||
wsPort: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type UpdateDeviceInput = CreateDeviceInput & {
|
|
||||||
status: "online" | "offline" | "busy" | "error";
|
|
||||||
inUse: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
class DeviceViewModel {
|
class DeviceViewModel {
|
||||||
devices = $state<Device[]>([]);
|
devices = $state<Device[]>([]);
|
||||||
loading = $state(false);
|
loading = $state(false);
|
||||||
@@ -71,7 +49,7 @@ class DeviceViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDevice(data: CreateDeviceInput): Promise<boolean> {
|
async createDevice(data: CreateDevice): Promise<boolean> {
|
||||||
this.creating = true;
|
this.creating = true;
|
||||||
try {
|
try {
|
||||||
const result = await createDeviceSC(data);
|
const result = await createDeviceSC(data);
|
||||||
@@ -132,7 +110,7 @@ class DeviceViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDevice(id: number, data: UpdateDeviceInput): Promise<boolean> {
|
async updateDevice(id: number, data: UpdateDevice): Promise<boolean> {
|
||||||
this.updating = true;
|
this.updating = true;
|
||||||
this.editingId = id;
|
this.editingId = id;
|
||||||
try {
|
try {
|
||||||
@@ -169,11 +147,11 @@ class DeviceViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setStatus(id: number, status: string) {
|
async setStatus(id: number, status: DeviceStatusValue) {
|
||||||
try {
|
try {
|
||||||
const result = await setDeviceStatusSC({
|
const result = await setDeviceStatusSC({
|
||||||
id,
|
id,
|
||||||
status: status as any,
|
status,
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { Device } from "@pkg/logic/domains/device/data";
|
||||||
|
import type { CreateLink, LinkWithDevice } from "@pkg/logic/domains/link/data";
|
||||||
|
import type { SupportedApp } from "@pkg/logic/domains/supported-app/data";
|
||||||
import {
|
import {
|
||||||
listLinksSQ,
|
listLinksSQ,
|
||||||
createLinkSC,
|
createLinkSC,
|
||||||
@@ -6,32 +9,17 @@ import {
|
|||||||
assignDeviceSC,
|
assignDeviceSC,
|
||||||
} from "./link.remote";
|
} from "./link.remote";
|
||||||
import { listDevicesSQ } from "../device/device.remote";
|
import { listDevicesSQ } from "../device/device.remote";
|
||||||
|
import { listSupportedAppsSQ } from "../supported-app/supported-app.remote";
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
|
||||||
type Link = {
|
type DeviceOption = Pick<Device, "id" | "title" | "host" | "status" | "inUse">;
|
||||||
id: number;
|
type SupportedAppOption = Pick<SupportedApp, "id" | "title" | "packageName">;
|
||||||
token: string;
|
type CreateLinkInput = Omit<CreateLink, "token">;
|
||||||
status: string;
|
|
||||||
appName: string;
|
|
||||||
appPackage: string;
|
|
||||||
linkedDeviceId: number | null;
|
|
||||||
expiresAt: Date | null;
|
|
||||||
lastAccessedAt: Date | null;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DeviceOption = {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
host: string;
|
|
||||||
status: string;
|
|
||||||
inUse: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
class LinkViewModel {
|
class LinkViewModel {
|
||||||
links = $state<Link[]>([]);
|
links = $state<LinkWithDevice[]>([]);
|
||||||
availableDevices = $state<DeviceOption[]>([]);
|
availableDevices = $state<DeviceOption[]>([]);
|
||||||
|
availableSupportedApps = $state<SupportedAppOption[]>([]);
|
||||||
loading = $state(false);
|
loading = $state(false);
|
||||||
creating = $state(false);
|
creating = $state(false);
|
||||||
deletingId = $state<number | null>(null);
|
deletingId = $state<number | null>(null);
|
||||||
@@ -52,7 +40,7 @@ class LinkViewModel {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.links = result.data as Link[];
|
this.links = result.data as LinkWithDevice[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to fetch links", {
|
toast.error("Failed to fetch links", {
|
||||||
description:
|
description:
|
||||||
@@ -69,7 +57,7 @@ class LinkViewModel {
|
|||||||
try {
|
try {
|
||||||
const result = await listDevicesSQ();
|
const result = await listDevicesSQ();
|
||||||
if (result?.data) {
|
if (result?.data) {
|
||||||
this.availableDevices = (result.data as any[]).map((d) => ({
|
this.availableDevices = (result.data as Device[]).map((d) => ({
|
||||||
id: d.id,
|
id: d.id,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
host: d.host,
|
host: d.host,
|
||||||
@@ -78,16 +66,22 @@ class LinkViewModel {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Non-critical — select will just be empty
|
// Non-critical - select will just be empty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createLink(data: {
|
async fetchSupportedAppsForSelect() {
|
||||||
linkedDeviceId: number;
|
try {
|
||||||
appName: string;
|
const result = await listSupportedAppsSQ();
|
||||||
appPackage: string;
|
if (result?.data) {
|
||||||
expiresAt?: Date | null;
|
this.availableSupportedApps = result.data as SupportedAppOption[];
|
||||||
}): Promise<boolean> {
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-critical - select will just be empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLink(data: CreateLinkInput): Promise<boolean> {
|
||||||
this.creating = true;
|
this.creating = true;
|
||||||
try {
|
try {
|
||||||
const result = await createLinkSC(data);
|
const result = await createLinkSC(data);
|
||||||
@@ -100,9 +94,7 @@ class LinkViewModel {
|
|||||||
}
|
}
|
||||||
toast.success("Link created");
|
toast.success("Link created");
|
||||||
this.showCreateDialog = false;
|
this.showCreateDialog = false;
|
||||||
if (result.data) {
|
await this.fetchLinks();
|
||||||
this.links = [...this.links, result.data as Link];
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Failed to create link", {
|
toast.error("Failed to create link", {
|
||||||
@@ -197,6 +189,11 @@ class LinkViewModel {
|
|||||||
const device = this.availableDevices.find((d) => d.id === deviceId);
|
const device = this.availableDevices.find((d) => d.id === deviceId);
|
||||||
return device ? device.title : `Device #${deviceId}`;
|
return device ? device.title : `Device #${deviceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSupportedAppName(supportedAppId: number): string {
|
||||||
|
const app = this.availableSupportedApps.find((d) => d.id === supportedAppId);
|
||||||
|
return app ? app.title : `App #${supportedAppId}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const linkVM = new LinkViewModel();
|
export const linkVM = new LinkViewModel();
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { getSupportedAppController } from "@pkg/logic/domains/supported-app/controller";
|
||||||
|
import {
|
||||||
|
createSupportedAppSchema,
|
||||||
|
updateSupportedAppSchema,
|
||||||
|
} from "@pkg/logic/domains/supported-app/data";
|
||||||
|
import { getFlowExecCtxForRemoteFuncs, unauthorized } from "$lib/core/server.utils";
|
||||||
|
import { command, getRequestEvent, query } from "$app/server";
|
||||||
|
import * as v from "valibot";
|
||||||
|
|
||||||
|
const sac = getSupportedAppController();
|
||||||
|
|
||||||
|
export const listSupportedAppsSQ = query(async () => {
|
||||||
|
const event = getRequestEvent();
|
||||||
|
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
|
||||||
|
if (!fctx.userId) return unauthorized(fctx);
|
||||||
|
|
||||||
|
const res = await sac.list(fctx);
|
||||||
|
return res.isOk()
|
||||||
|
? { data: res.value, error: null }
|
||||||
|
: { data: null, error: res.error };
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSupportedAppSC = command(
|
||||||
|
createSupportedAppSchema,
|
||||||
|
async (payload) => {
|
||||||
|
const event = getRequestEvent();
|
||||||
|
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
|
||||||
|
if (!fctx.userId) return unauthorized(fctx);
|
||||||
|
|
||||||
|
const res = await sac.create(fctx, payload);
|
||||||
|
return res.isOk()
|
||||||
|
? { data: res.value, error: null }
|
||||||
|
: { data: null, error: res.error };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const updateSupportedAppSC = command(
|
||||||
|
v.object({ id: v.number(), data: updateSupportedAppSchema }),
|
||||||
|
async (payload) => {
|
||||||
|
const event = getRequestEvent();
|
||||||
|
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
|
||||||
|
if (!fctx.userId) return unauthorized(fctx);
|
||||||
|
|
||||||
|
const res = await sac.update(fctx, payload.id, payload.data);
|
||||||
|
return res.isOk()
|
||||||
|
? { data: res.value, error: null }
|
||||||
|
: { data: null, error: res.error };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const deleteSupportedAppSC = command(
|
||||||
|
v.object({ id: v.number() }),
|
||||||
|
async (payload) => {
|
||||||
|
const event = getRequestEvent();
|
||||||
|
const fctx = await getFlowExecCtxForRemoteFuncs(event.locals);
|
||||||
|
if (!fctx.userId) return unauthorized(fctx);
|
||||||
|
|
||||||
|
const res = await sac.delete(fctx, payload.id);
|
||||||
|
return res.isOk()
|
||||||
|
? { data: res.value, error: null }
|
||||||
|
: { data: null, error: res.error };
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import type {
|
||||||
|
CreateSupportedApp,
|
||||||
|
SupportedApp,
|
||||||
|
UpdateSupportedApp,
|
||||||
|
} from "@pkg/logic/domains/supported-app/data";
|
||||||
|
import {
|
||||||
|
createSupportedAppSC,
|
||||||
|
deleteSupportedAppSC,
|
||||||
|
listSupportedAppsSQ,
|
||||||
|
updateSupportedAppSC,
|
||||||
|
} from "./supported-app.remote";
|
||||||
|
import { toast } from "svelte-sonner";
|
||||||
|
|
||||||
|
class SupportedAppViewModel {
|
||||||
|
supportedApps = $state<SupportedApp[]>([]);
|
||||||
|
loading = $state(false);
|
||||||
|
creating = $state(false);
|
||||||
|
updating = $state(false);
|
||||||
|
deletingId = $state<number | null>(null);
|
||||||
|
showCreateDialog = $state(false);
|
||||||
|
|
||||||
|
async fetchSupportedApps() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const result = await listSupportedAppsSQ();
|
||||||
|
if (result?.error || !result?.data) {
|
||||||
|
toast.error(
|
||||||
|
result?.error?.message || "Failed to fetch supported apps",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
result?.error?.description || "Please try again",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.supportedApps = result.data as SupportedApp[];
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to fetch supported apps", {
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Please try again",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSupportedApp(data: CreateSupportedApp): Promise<boolean> {
|
||||||
|
this.creating = true;
|
||||||
|
try {
|
||||||
|
const result = await createSupportedAppSC(data);
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error(
|
||||||
|
result.error.message || "Failed to create supported app",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
result.error.description || "Please try again",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
toast.success("Supported app created");
|
||||||
|
this.showCreateDialog = false;
|
||||||
|
if (result.data) {
|
||||||
|
this.supportedApps = [
|
||||||
|
...this.supportedApps,
|
||||||
|
result.data as SupportedApp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to create supported app", {
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Please try again",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.creating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSupportedApp(
|
||||||
|
id: number,
|
||||||
|
data: UpdateSupportedApp,
|
||||||
|
): Promise<boolean> {
|
||||||
|
this.updating = true;
|
||||||
|
try {
|
||||||
|
const result = await updateSupportedAppSC({ id, data });
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error(
|
||||||
|
result.error.message || "Failed to update supported app",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
result.error.description || "Please try again",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
toast.success("Supported app updated");
|
||||||
|
if (result.data) {
|
||||||
|
const updated = result.data as SupportedApp;
|
||||||
|
this.supportedApps = this.supportedApps.map((app) =>
|
||||||
|
app.id === id ? updated : app,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to update supported app", {
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Please try again",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
this.updating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSupportedApp(id: number) {
|
||||||
|
this.deletingId = id;
|
||||||
|
try {
|
||||||
|
const result = await deleteSupportedAppSC({ id });
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error(
|
||||||
|
result.error.message || "Failed to delete supported app",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
result.error.description || "Please try again",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success("Supported app deleted");
|
||||||
|
this.supportedApps = this.supportedApps.filter((app) => app.id !== id);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to delete supported app", {
|
||||||
|
description:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Please try again",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.deletingId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportedAppVM = new SupportedAppViewModel();
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
import { Label } from "$lib/components/ui/label";
|
import { Label } from "$lib/components/ui/label";
|
||||||
import * as Table from "$lib/components/ui/table";
|
import * as Table from "$lib/components/ui/table";
|
||||||
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
|
import MaxWidthWrapper from "$lib/components/molecules/max-width-wrapper.svelte";
|
||||||
import { mainNavTree, SUPPORTED_APPS } from "$lib/core/constants";
|
import { mainNavTree } from "$lib/core/constants";
|
||||||
import { linkVM } from "$lib/domains/link/link.vm.svelte";
|
import { linkVM } from "$lib/domains/link/link.vm.svelte";
|
||||||
import { breadcrumbs } from "$lib/global.stores";
|
import { breadcrumbs } from "$lib/global.stores";
|
||||||
import LinkIcon from "@lucide/svelte/icons/link";
|
import LinkIcon from "@lucide/svelte/icons/link";
|
||||||
@@ -24,35 +24,26 @@
|
|||||||
breadcrumbs.set([mainNavTree[0], mainNavTree[1]]);
|
breadcrumbs.set([mainNavTree[0], mainNavTree[1]]);
|
||||||
|
|
||||||
let selectedDeviceId = $state("");
|
let selectedDeviceId = $state("");
|
||||||
let selectedAppPackage = $state("");
|
let selectedSupportedAppId = $state("");
|
||||||
let expiresAt = $state("");
|
let expiresAt = $state("");
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await linkVM.fetchLinks();
|
await linkVM.fetchLinks();
|
||||||
await linkVM.fetchDevicesForSelect();
|
await linkVM.fetchDevicesForSelect();
|
||||||
|
await linkVM.fetchSupportedAppsForSelect();
|
||||||
});
|
});
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
selectedDeviceId = "";
|
selectedDeviceId = "";
|
||||||
selectedAppPackage = "";
|
selectedSupportedAppId = "";
|
||||||
expiresAt = "";
|
expiresAt = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSupportedApp(packageName: string) {
|
|
||||||
return SUPPORTED_APPS.find((app) => app.packageName === packageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleCreate(e: Event) {
|
async function handleCreate(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const selectedApp = getSupportedApp(selectedAppPackage);
|
|
||||||
if (!selectedApp) {
|
|
||||||
toast.error("Select a supported app");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = await linkVM.createLink({
|
const success = await linkVM.createLink({
|
||||||
linkedDeviceId: Number(selectedDeviceId),
|
linkedDeviceId: Number(selectedDeviceId),
|
||||||
appName: selectedApp.title,
|
supportedAppId: Number(selectedSupportedAppId),
|
||||||
appPackage: selectedApp.packageName,
|
|
||||||
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
expiresAt: expiresAt ? new Date(expiresAt) : null,
|
||||||
});
|
});
|
||||||
if (success) resetForm();
|
if (success) resetForm();
|
||||||
@@ -121,6 +112,7 @@
|
|||||||
onclick={() => {
|
onclick={() => {
|
||||||
linkVM.showCreateDialog = true;
|
linkVM.showCreateDialog = true;
|
||||||
linkVM.fetchDevicesForSelect();
|
linkVM.fetchDevicesForSelect();
|
||||||
|
linkVM.fetchSupportedAppsForSelect();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon={Plus} cls="h-4 w-4 mr-2" />
|
<Icon icon={Plus} cls="h-4 w-4 mr-2" />
|
||||||
@@ -176,11 +168,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="text-muted-foreground">App</p>
|
<p class="text-muted-foreground">App</p>
|
||||||
<p>{link.appName}</p>
|
<p>
|
||||||
<p
|
{link.supportedApp?.title ||
|
||||||
class="text-muted-foreground break-all font-mono text-[11px]"
|
linkVM.getSupportedAppName(
|
||||||
>
|
link.supportedAppId,
|
||||||
{link.appPackage}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -301,12 +293,10 @@
|
|||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<p class="text-sm font-medium">
|
<p class="text-sm font-medium">
|
||||||
{link.appName}
|
{link.supportedApp?.title ||
|
||||||
</p>
|
linkVM.getSupportedAppName(
|
||||||
<p
|
link.supportedAppId,
|
||||||
class="text-muted-foreground font-mono text-[11px]"
|
)}
|
||||||
>
|
|
||||||
{link.appPackage}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
@@ -430,20 +420,19 @@
|
|||||||
<Label for="supportedApp">Supported App</Label>
|
<Label for="supportedApp">Supported App</Label>
|
||||||
<select
|
<select
|
||||||
id="supportedApp"
|
id="supportedApp"
|
||||||
bind:value={selectedAppPackage}
|
bind:value={selectedSupportedAppId}
|
||||||
required
|
required
|
||||||
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1"
|
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1"
|
||||||
>
|
>
|
||||||
<option value="" disabled>Select a supported app</option>
|
<option value="" disabled>Select a supported app</option>
|
||||||
{#each SUPPORTED_APPS as app}
|
{#each linkVM.availableSupportedApps as app}
|
||||||
<option value={app.packageName}>
|
<option value={String(app.id)}>
|
||||||
{app.title} — {app.packageName}
|
{app.title}
|
||||||
</option>
|
</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<p class="text-muted-foreground text-xs">
|
<p class="text-muted-foreground text-xs">
|
||||||
Links can only target apps from the current supported app
|
Supported apps are managed from the dedicated catalogue page.
|
||||||
list.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
358
apps/main/src/routes/(main)/supported-apps/+page.svelte
Normal file
358
apps/main/src/routes/(main)/supported-apps/+page.svelte
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from "$lib/components/atoms/icon.svelte";
|
||||||
|
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
|
||||||
|
import { Button, buttonVariants } from "$lib/components/ui/button";
|
||||||
|
import * as Card from "$lib/components/ui/card";
|
||||||
|
import * as Dialog from "$lib/components/ui/dialog";
|
||||||
|
import { Input } from "$lib/components/ui/input";
|
||||||
|
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 { supportedAppVM } from "$lib/domains/supported-app/supported-app.vm.svelte";
|
||||||
|
import { breadcrumbs } from "$lib/global.stores";
|
||||||
|
import AppWindow from "@lucide/svelte/icons/app-window";
|
||||||
|
import Pencil from "@lucide/svelte/icons/pencil";
|
||||||
|
import Plus from "@lucide/svelte/icons/plus";
|
||||||
|
import RefreshCw from "@lucide/svelte/icons/refresh-cw";
|
||||||
|
import Trash2 from "@lucide/svelte/icons/trash-2";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
breadcrumbs.set([mainNavTree[0], mainNavTree[2]]);
|
||||||
|
|
||||||
|
let title = $state("");
|
||||||
|
let packageName = $state("");
|
||||||
|
let editId = $state<number | null>(null);
|
||||||
|
let editTitle = $state("");
|
||||||
|
let editPackageName = $state("");
|
||||||
|
let showEditDialog = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await supportedAppVM.fetchSupportedApps();
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetCreateForm() {
|
||||||
|
title = "";
|
||||||
|
packageName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetEditForm() {
|
||||||
|
editId = null;
|
||||||
|
editTitle = "";
|
||||||
|
editPackageName = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreate(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
const success = await supportedAppVM.createSupportedApp({
|
||||||
|
title,
|
||||||
|
packageName,
|
||||||
|
});
|
||||||
|
if (success) resetCreateForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog(app: (typeof supportedAppVM.supportedApps)[number]) {
|
||||||
|
editId = app.id;
|
||||||
|
editTitle = app.title;
|
||||||
|
editPackageName = app.packageName;
|
||||||
|
showEditDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdate(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!editId) return;
|
||||||
|
const success = await supportedAppVM.updateSupportedApp(editId, {
|
||||||
|
title: editTitle,
|
||||||
|
packageName: editPackageName,
|
||||||
|
});
|
||||||
|
if (success) {
|
||||||
|
showEditDialog = false;
|
||||||
|
resetEditForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(value: Date | string): string {
|
||||||
|
return new Date(value).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (showEditDialog) return;
|
||||||
|
resetEditForm();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<MaxWidthWrapper cls="space-y-4">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Icon icon={AppWindow} cls="h-5 w-5 text-primary" />
|
||||||
|
<Card.Title>Supported Apps</Card.Title>
|
||||||
|
<span class="text-muted-foreground text-xs">
|
||||||
|
{supportedAppVM.supportedApps.length} total
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => void supportedAppVM.fetchSupportedApps()}
|
||||||
|
disabled={supportedAppVM.loading}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={RefreshCw}
|
||||||
|
cls={`mr-2 h-4 w-4 ${supportedAppVM.loading ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onclick={() => (supportedAppVM.showCreateDialog = true)}
|
||||||
|
>
|
||||||
|
<Icon icon={Plus} cls="mr-2 h-4 w-4" />
|
||||||
|
Add Supported App
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
{#if !supportedAppVM.loading && supportedAppVM.supportedApps.length === 0}
|
||||||
|
<div class="py-10 text-center text-sm text-muted-foreground">
|
||||||
|
No supported apps configured yet.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-3 md:hidden">
|
||||||
|
{#each supportedAppVM.supportedApps as app (app.id)}
|
||||||
|
<div class="rounded-lg border bg-background p-3">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p class="text-sm font-medium">{app.title}</p>
|
||||||
|
<p
|
||||||
|
class="text-muted-foreground break-all font-mono text-xs"
|
||||||
|
>
|
||||||
|
{app.packageName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground mt-3 text-xs">
|
||||||
|
Updated {formatDate(app.updatedAt)}
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => openEditDialog(app)}
|
||||||
|
>
|
||||||
|
<Icon icon={Pencil} cls="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger
|
||||||
|
class={buttonVariants({
|
||||||
|
variant: "destructive",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
disabled={supportedAppVM.deletingId ===
|
||||||
|
app.id}
|
||||||
|
>
|
||||||
|
<Icon icon={Trash2} cls="h-4 w-4" />
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Delete supported app?
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
This removes "{app.title}" from
|
||||||
|
the supported app catalogue.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
Cancel
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action
|
||||||
|
onclick={() =>
|
||||||
|
supportedAppVM.deleteSupportedApp(
|
||||||
|
app.id,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Head>Title</Table.Head>
|
||||||
|
<Table.Head>Package Name</Table.Head>
|
||||||
|
<Table.Head>Updated</Table.Head>
|
||||||
|
<Table.Head>Actions</Table.Head>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#each supportedAppVM.supportedApps as app (app.id)}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell class="font-medium">
|
||||||
|
{app.title}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="font-mono text-xs">
|
||||||
|
{app.packageName}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-xs">
|
||||||
|
{formatDate(app.updatedAt)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() =>
|
||||||
|
openEditDialog(app)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={Pencil}
|
||||||
|
cls="h-4 w-4"
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<AlertDialog.Root>
|
||||||
|
<AlertDialog.Trigger
|
||||||
|
class={buttonVariants({
|
||||||
|
variant: "destructive",
|
||||||
|
size: "sm",
|
||||||
|
})}
|
||||||
|
disabled={supportedAppVM.deletingId ===
|
||||||
|
app.id}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={Trash2}
|
||||||
|
cls="h-4 w-4"
|
||||||
|
/>
|
||||||
|
Delete
|
||||||
|
</AlertDialog.Trigger>
|
||||||
|
<AlertDialog.Content>
|
||||||
|
<AlertDialog.Header>
|
||||||
|
<AlertDialog.Title>
|
||||||
|
Delete supported
|
||||||
|
app?
|
||||||
|
</AlertDialog.Title>
|
||||||
|
<AlertDialog.Description>
|
||||||
|
This removes
|
||||||
|
"{app.title}" from
|
||||||
|
the supported app
|
||||||
|
catalogue.
|
||||||
|
</AlertDialog.Description>
|
||||||
|
</AlertDialog.Header>
|
||||||
|
<AlertDialog.Footer>
|
||||||
|
<AlertDialog.Cancel>
|
||||||
|
Cancel
|
||||||
|
</AlertDialog.Cancel>
|
||||||
|
<AlertDialog.Action
|
||||||
|
onclick={() =>
|
||||||
|
supportedAppVM.deleteSupportedApp(
|
||||||
|
app.id,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialog.Action>
|
||||||
|
</AlertDialog.Footer>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</MaxWidthWrapper>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={supportedAppVM.showCreateDialog}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Add Supported App</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Register an app that links are allowed to lease.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<form onsubmit={handleCreate} class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="title">Title</Label>
|
||||||
|
<Input id="title" bind:value={title} required />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="packageName">Package Name</Label>
|
||||||
|
<Input
|
||||||
|
id="packageName"
|
||||||
|
bind:value={packageName}
|
||||||
|
placeholder="com.example.app"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onclick={() => (supportedAppVM.showCreateDialog = false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={supportedAppVM.creating}>
|
||||||
|
{supportedAppVM.creating ? "Creating..." : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open={showEditDialog}>
|
||||||
|
<Dialog.Content class="sm:max-w-[425px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Edit Supported App</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Update the supported app record used by the links flow.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
<form onsubmit={handleUpdate} class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-title">Title</Label>
|
||||||
|
<Input id="edit-title" bind:value={editTitle} required />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="edit-packageName">Package Name</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-packageName"
|
||||||
|
bind:value={editPackageName}
|
||||||
|
placeholder="com.example.app"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
showEditDialog = false;
|
||||||
|
resetEditForm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={supportedAppVM.updating}>
|
||||||
|
{supportedAppVM.updating ? "Saving..." : "Save Changes"}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
@@ -90,3 +90,23 @@ Update rule:
|
|||||||
- Cleaned up unused `Smartphone` import from constants.ts
|
- Cleaned up unused `Smartphone` import from constants.ts
|
||||||
- Updated detail page breadcrumbs to go Dashboard > Device instead of Dashboard > Devices > Device
|
- 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`
|
- Updated `goto` calls to navigate to `/dashboard/:id` instead of `/devices/:id`
|
||||||
|
|
||||||
|
### 12 — Supported Apps Catalogue
|
||||||
|
|
||||||
|
- Added a normalized `supported_app` schema and matching logic domain for supported-app CRUD
|
||||||
|
- Added an admin `/supported-apps` page and sidebar entry for managing the app catalogue
|
||||||
|
- Refactored links to reference `supportedAppId` and load supported app labels from the catalogue instead of freeform app fields/constants
|
||||||
|
|
||||||
|
### 13 — Fixed ws-scrcpy Stream URL
|
||||||
|
|
||||||
|
- Added `WS_SCRCPY_URL` constant in `constants.ts` for the static public ws-scrcpy domain
|
||||||
|
- Replaced broken `normalizeViewerUrl` (which wrongly jammed ADB port into the public domain) with `buildStreamUrl` in `device-details.vm.svelte.ts`
|
||||||
|
- New builder constructs the correct hash-based ws-scrcpy stream URL: `#!action=stream&udid={host}:{wsPort}&player=mse&ws=wss://.../?action=proxy-adb&remote=tcp:8886&udid={host}:{wsPort}`
|
||||||
|
- Device `host` and `wsPort` are now correctly treated as internal ADB address (e.g. `172.17.0.1:5555`), not the public domain
|
||||||
|
- Scrcpy server port (8886) hardcoded as constant since it's static per the ws-scrcpy architecture
|
||||||
|
|
||||||
|
### 14 — Frontend Domain Type Source-of-Truth Refactor
|
||||||
|
|
||||||
|
- Refactored `apps/main/src/lib/domains/{device,link,supported-app}` to remove local duplicate data type declarations and import canonical types from `@pkg/logic/domains/*/data`
|
||||||
|
- Updated device status validation in `device.remote.ts` to reuse `deviceStatusSchema` from logic instead of a duplicated local picklist
|
||||||
|
- Kept only derived UI helper types (`Pick`/`Omit`) where needed for presentation and transport-shape compatibility
|
||||||
|
|||||||
13
packages/db/migrations/0003_workable_runaways.sql
Normal file
13
packages/db/migrations/0003_workable_runaways.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE "supported_app" (
|
||||||
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"package_name" text NOT NULL,
|
||||||
|
"created_at" timestamp NOT NULL,
|
||||||
|
"updated_at" timestamp NOT NULL,
|
||||||
|
CONSTRAINT "supported_app_package_name_unique" UNIQUE("package_name")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "link" ADD COLUMN "supported_app_id" integer NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "link" ADD CONSTRAINT "link_supported_app_id_supported_app_id_fk" FOREIGN KEY ("supported_app_id") REFERENCES "public"."supported_app"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "link" DROP COLUMN "app_name";--> statement-breakpoint
|
||||||
|
ALTER TABLE "link" DROP COLUMN "app_package";
|
||||||
997
packages/db/migrations/meta/0003_snapshot.json
Normal file
997
packages/db/migrations/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,997 @@
|
|||||||
|
{
|
||||||
|
"id": "f9535ed5-4fb8-4f58-b741-712a5c7356a1",
|
||||||
|
"prevId": "a633b0b6-32a7-4f7f-8b17-4264fe54ca57",
|
||||||
|
"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'"
|
||||||
|
},
|
||||||
|
"linked_device_id": {
|
||||||
|
"name": "linked_device_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"supported_app_id": {
|
||||||
|
"name": "supported_app_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"link_supported_app_id_supported_app_id_fk": {
|
||||||
|
"name": "link_supported_app_id_supported_app_id_fk",
|
||||||
|
"tableFrom": "link",
|
||||||
|
"tableTo": "supported_app",
|
||||||
|
"columnsFrom": [
|
||||||
|
"supported_app_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "restrict",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"link_token_unique": {
|
||||||
|
"name": "link_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.supported_app": {
|
||||||
|
"name": "supported_app",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"package_name": {
|
||||||
|
"name": "package_name",
|
||||||
|
"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": {
|
||||||
|
"supported_app_package_name_unique": {
|
||||||
|
"name": "supported_app_package_name_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"package_name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,13 @@
|
|||||||
"when": 1774703478082,
|
"when": 1774703478082,
|
||||||
"tag": "0002_remarkable_charles_xavier",
|
"tag": "0002_remarkable_charles_xavier",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774705734646,
|
||||||
|
"tag": "0003_workable_runaways",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,5 @@ export * from "./better.auth.schema";
|
|||||||
export * from "./device.schema";
|
export * from "./device.schema";
|
||||||
export * from "./general.schema";
|
export * from "./general.schema";
|
||||||
export * from "./link.schema";
|
export * from "./link.schema";
|
||||||
|
export * from "./supported-app.schema";
|
||||||
export * from "./task.schema";
|
export * from "./task.schema";
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
timestamp,
|
timestamp,
|
||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { supportedApp } from "./supported-app.schema";
|
||||||
import { device } from "./device.schema";
|
import { device } from "./device.schema";
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
|
|
||||||
@@ -14,12 +15,14 @@ export const link = pgTable("link", {
|
|||||||
|
|
||||||
token: text("token").notNull().unique(),
|
token: text("token").notNull().unique(),
|
||||||
status: varchar("status", { length: 16 }).notNull().default("active"), // "active" | "inactive" | "expired" | "revoked"
|
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, {
|
linkedDeviceId: integer("linked_device_id").references(() => device.id, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
supportedAppId: integer("supported_app_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => supportedApp.id, {
|
||||||
|
onDelete: "restrict",
|
||||||
|
}),
|
||||||
|
|
||||||
expiresAt: timestamp("expires_at"),
|
expiresAt: timestamp("expires_at"),
|
||||||
lastAccessedAt: timestamp("last_accessed_at"),
|
lastAccessedAt: timestamp("last_accessed_at"),
|
||||||
@@ -32,4 +35,8 @@ export const linkRelations = relations(link, ({ one }) => ({
|
|||||||
fields: [link.linkedDeviceId],
|
fields: [link.linkedDeviceId],
|
||||||
references: [device.id],
|
references: [device.id],
|
||||||
}),
|
}),
|
||||||
|
supportedApp: one(supportedApp, {
|
||||||
|
fields: [link.supportedAppId],
|
||||||
|
references: [supportedApp.id],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
9
packages/db/schema/supported-app.schema.ts
Normal file
9
packages/db/schema/supported-app.schema.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const supportedApp = pgTable("supported_app", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
title: text("title").notNull(),
|
||||||
|
packageName: text("package_name").notNull().unique(),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
});
|
||||||
@@ -3,18 +3,24 @@ import { nanoid } from "nanoid";
|
|||||||
import { db } from "@pkg/db";
|
import { db } from "@pkg/db";
|
||||||
import { type Err } from "@pkg/result";
|
import { type Err } from "@pkg/result";
|
||||||
import { FlowExecCtx } from "@core/flow.execution.context";
|
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||||
import { CreateLink, Link, LinkStatus, LinkWithDevice, UpdateLink } from "./data";
|
import {
|
||||||
|
CreateLink,
|
||||||
|
Link,
|
||||||
|
LinkStatus,
|
||||||
|
LinkWithDevice,
|
||||||
|
UpdateLink,
|
||||||
|
} from "./data";
|
||||||
import { LinkRepository } from "./repository";
|
import { LinkRepository } from "./repository";
|
||||||
import { linkErrors } from "./errors";
|
import { linkErrors } from "./errors";
|
||||||
|
|
||||||
export class LinkController {
|
export class LinkController {
|
||||||
constructor(private repo: LinkRepository) {}
|
constructor(private repo: LinkRepository) {}
|
||||||
|
|
||||||
list(fctx: FlowExecCtx): ResultAsync<Link[], Err> {
|
list(fctx: FlowExecCtx): ResultAsync<LinkWithDevice[], Err> {
|
||||||
return this.repo.list(fctx);
|
return this.repo.list(fctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(fctx: FlowExecCtx, id: number): ResultAsync<Link, Err> {
|
getById(fctx: FlowExecCtx, id: number): ResultAsync<LinkWithDevice, Err> {
|
||||||
return this.repo.getById(fctx, id);
|
return this.repo.getById(fctx, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as v from "valibot";
|
import * as v from "valibot";
|
||||||
import { deviceSchema } from "@domains/device/data";
|
import { deviceSchema } from "@domains/device/data";
|
||||||
|
import { supportedAppSchema } from "@domains/supported-app/data";
|
||||||
|
|
||||||
export enum LinkStatus {
|
export enum LinkStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
@@ -15,9 +16,8 @@ export const linkSchema = v.object({
|
|||||||
id: v.number(),
|
id: v.number(),
|
||||||
token: v.string(),
|
token: v.string(),
|
||||||
status: linkStatusSchema,
|
status: linkStatusSchema,
|
||||||
appName: v.string(),
|
|
||||||
appPackage: v.string(),
|
|
||||||
linkedDeviceId: v.nullable(v.number()),
|
linkedDeviceId: v.nullable(v.number()),
|
||||||
|
supportedAppId: v.number(),
|
||||||
expiresAt: v.nullable(v.date()),
|
expiresAt: v.nullable(v.date()),
|
||||||
lastAccessedAt: v.nullable(v.date()),
|
lastAccessedAt: v.nullable(v.date()),
|
||||||
createdAt: v.date(),
|
createdAt: v.date(),
|
||||||
@@ -28,14 +28,14 @@ export type Link = v.InferOutput<typeof linkSchema>;
|
|||||||
export const linkWithDeviceSchema = v.object({
|
export const linkWithDeviceSchema = v.object({
|
||||||
...linkSchema.entries,
|
...linkSchema.entries,
|
||||||
device: v.nullable(deviceSchema),
|
device: v.nullable(deviceSchema),
|
||||||
|
supportedApp: v.nullable(supportedAppSchema),
|
||||||
});
|
});
|
||||||
export type LinkWithDevice = v.InferOutput<typeof linkWithDeviceSchema>;
|
export type LinkWithDevice = v.InferOutput<typeof linkWithDeviceSchema>;
|
||||||
|
|
||||||
export const createLinkSchema = v.object({
|
export const createLinkSchema = v.object({
|
||||||
token: v.pipe(v.string(), v.minLength(1)),
|
token: v.pipe(v.string(), v.minLength(1)),
|
||||||
appName: v.pipe(v.string(), v.minLength(1)),
|
|
||||||
appPackage: v.pipe(v.string(), v.minLength(1)),
|
|
||||||
linkedDeviceId: v.number(),
|
linkedDeviceId: v.number(),
|
||||||
|
supportedAppId: v.number(),
|
||||||
expiresAt: v.optional(v.nullable(v.date())),
|
expiresAt: v.optional(v.nullable(v.date())),
|
||||||
});
|
});
|
||||||
export type CreateLink = v.InferOutput<typeof createLinkSchema>;
|
export type CreateLink = v.InferOutput<typeof createLinkSchema>;
|
||||||
@@ -43,9 +43,8 @@ export type CreateLink = v.InferOutput<typeof createLinkSchema>;
|
|||||||
export const updateLinkSchema = v.partial(
|
export const updateLinkSchema = v.partial(
|
||||||
v.object({
|
v.object({
|
||||||
status: linkStatusSchema,
|
status: linkStatusSchema,
|
||||||
appName: v.string(),
|
|
||||||
appPackage: v.string(),
|
|
||||||
linkedDeviceId: v.nullable(v.number()),
|
linkedDeviceId: v.nullable(v.number()),
|
||||||
|
supportedAppId: v.number(),
|
||||||
expiresAt: v.nullable(v.date()),
|
expiresAt: v.nullable(v.date()),
|
||||||
lastAccessedAt: v.nullable(v.date()),
|
lastAccessedAt: v.nullable(v.date()),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||||
import { FlowExecCtx } from "@core/flow.execution.context";
|
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||||
import { Database, asc, eq } from "@pkg/db";
|
import { Database, eq } from "@pkg/db";
|
||||||
import { link } from "@pkg/db/schema";
|
import { link } from "@pkg/db/schema";
|
||||||
import { type Err } from "@pkg/result";
|
import { type Err } from "@pkg/result";
|
||||||
import { logger } from "@pkg/logger";
|
import { logger } from "@pkg/logger";
|
||||||
@@ -11,29 +11,41 @@ import { linkErrors } from "./errors";
|
|||||||
export class LinkRepository {
|
export class LinkRepository {
|
||||||
constructor(private db: Database) {}
|
constructor(private db: Database) {}
|
||||||
|
|
||||||
list(fctx: FlowExecCtx): ResultAsync<Link[], Err> {
|
list(fctx: FlowExecCtx): ResultAsync<LinkWithDevice[], Err> {
|
||||||
return traceResultAsync({
|
return traceResultAsync({
|
||||||
name: "link.list",
|
name: "link.list",
|
||||||
fctx,
|
fctx,
|
||||||
fn: () =>
|
fn: () =>
|
||||||
ResultAsync.fromPromise(
|
ResultAsync.fromPromise(
|
||||||
this.db.select().from(link).orderBy(asc(link.createdAt)),
|
this.db.query.link.findMany({
|
||||||
|
orderBy: (link, { asc }) => [asc(link.createdAt)],
|
||||||
|
with: {
|
||||||
|
device: true,
|
||||||
|
supportedApp: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
(e) =>
|
(e) =>
|
||||||
linkErrors.listFailed(
|
linkErrors.listFailed(
|
||||||
fctx,
|
fctx,
|
||||||
e instanceof Error ? e.message : String(e),
|
e instanceof Error ? e.message : String(e),
|
||||||
),
|
),
|
||||||
).map((rows) => rows as Link[]),
|
).map((rows) => rows as LinkWithDevice[]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getById(fctx: FlowExecCtx, id: number): ResultAsync<Link, Err> {
|
getById(fctx: FlowExecCtx, id: number): ResultAsync<LinkWithDevice, Err> {
|
||||||
return traceResultAsync({
|
return traceResultAsync({
|
||||||
name: "link.getById",
|
name: "link.getById",
|
||||||
fctx,
|
fctx,
|
||||||
fn: () =>
|
fn: () =>
|
||||||
ResultAsync.fromPromise(
|
ResultAsync.fromPromise(
|
||||||
this.db.query.link.findFirst({ where: eq(link.id, id) }),
|
this.db.query.link.findFirst({
|
||||||
|
where: eq(link.id, id),
|
||||||
|
with: {
|
||||||
|
device: true,
|
||||||
|
supportedApp: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
(e) =>
|
(e) =>
|
||||||
linkErrors.dbError(
|
linkErrors.dbError(
|
||||||
fctx,
|
fctx,
|
||||||
@@ -41,7 +53,7 @@ export class LinkRepository {
|
|||||||
),
|
),
|
||||||
).andThen((row) => {
|
).andThen((row) => {
|
||||||
if (!row) return errAsync(linkErrors.linkNotFound(fctx, id));
|
if (!row) return errAsync(linkErrors.linkNotFound(fctx, id));
|
||||||
return okAsync(row as Link);
|
return okAsync(row as LinkWithDevice);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -54,7 +66,7 @@ export class LinkRepository {
|
|||||||
ResultAsync.fromPromise(
|
ResultAsync.fromPromise(
|
||||||
this.db.query.link.findFirst({
|
this.db.query.link.findFirst({
|
||||||
where: eq(link.token, token),
|
where: eq(link.token, token),
|
||||||
with: { device: true },
|
with: { device: true, supportedApp: true },
|
||||||
}),
|
}),
|
||||||
(e) =>
|
(e) =>
|
||||||
linkErrors.dbError(
|
linkErrors.dbError(
|
||||||
@@ -81,9 +93,8 @@ export class LinkRepository {
|
|||||||
.values({
|
.values({
|
||||||
token: data.token,
|
token: data.token,
|
||||||
status: "active",
|
status: "active",
|
||||||
appName: data.appName,
|
|
||||||
appPackage: data.appPackage,
|
|
||||||
linkedDeviceId: data.linkedDeviceId ?? null,
|
linkedDeviceId: data.linkedDeviceId ?? null,
|
||||||
|
supportedAppId: data.supportedAppId,
|
||||||
expiresAt: data.expiresAt ?? null,
|
expiresAt: data.expiresAt ?? null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|||||||
45
packages/logic/domains/supported-app/controller.ts
Normal file
45
packages/logic/domains/supported-app/controller.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { db } from "@pkg/db";
|
||||||
|
import { type Err } from "@pkg/result";
|
||||||
|
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||||
|
import { ResultAsync } from "neverthrow";
|
||||||
|
import {
|
||||||
|
CreateSupportedApp,
|
||||||
|
SupportedApp,
|
||||||
|
UpdateSupportedApp,
|
||||||
|
} from "./data";
|
||||||
|
import { SupportedAppRepository } from "./repository";
|
||||||
|
|
||||||
|
export class SupportedAppController {
|
||||||
|
constructor(private repo: SupportedAppRepository) {}
|
||||||
|
|
||||||
|
list(fctx: FlowExecCtx): ResultAsync<SupportedApp[], Err> {
|
||||||
|
return this.repo.list(fctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(fctx: FlowExecCtx, id: number): ResultAsync<SupportedApp, Err> {
|
||||||
|
return this.repo.getById(fctx, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(
|
||||||
|
fctx: FlowExecCtx,
|
||||||
|
data: CreateSupportedApp,
|
||||||
|
): ResultAsync<SupportedApp, Err> {
|
||||||
|
return this.repo.create(fctx, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
fctx: FlowExecCtx,
|
||||||
|
id: number,
|
||||||
|
data: UpdateSupportedApp,
|
||||||
|
): ResultAsync<SupportedApp, Err> {
|
||||||
|
return this.repo.update(fctx, id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(fctx: FlowExecCtx, id: number): ResultAsync<boolean, Err> {
|
||||||
|
return this.repo.delete(fctx, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSupportedAppController(): SupportedAppController {
|
||||||
|
return new SupportedAppController(new SupportedAppRepository(db));
|
||||||
|
}
|
||||||
28
packages/logic/domains/supported-app/data.ts
Normal file
28
packages/logic/domains/supported-app/data.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as v from "valibot";
|
||||||
|
|
||||||
|
export const supportedAppSchema = v.object({
|
||||||
|
id: v.number(),
|
||||||
|
title: v.string(),
|
||||||
|
packageName: v.string(),
|
||||||
|
createdAt: v.date(),
|
||||||
|
updatedAt: v.date(),
|
||||||
|
});
|
||||||
|
export type SupportedApp = v.InferOutput<typeof supportedAppSchema>;
|
||||||
|
|
||||||
|
export const createSupportedAppSchema = v.object({
|
||||||
|
title: v.pipe(v.string(), v.minLength(1)),
|
||||||
|
packageName: v.pipe(v.string(), v.minLength(1)),
|
||||||
|
});
|
||||||
|
export type CreateSupportedApp = v.InferOutput<
|
||||||
|
typeof createSupportedAppSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const updateSupportedAppSchema = v.partial(
|
||||||
|
v.object({
|
||||||
|
title: v.string(),
|
||||||
|
packageName: v.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
export type UpdateSupportedApp = v.InferOutput<
|
||||||
|
typeof updateSupportedAppSchema
|
||||||
|
>;
|
||||||
59
packages/logic/domains/supported-app/errors.ts
Normal file
59
packages/logic/domains/supported-app/errors.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { FlowExecCtx } from "@/core/flow.execution.context";
|
||||||
|
import { ERROR_CODES, type Err } from "@pkg/result";
|
||||||
|
import { getError } from "@pkg/logger";
|
||||||
|
|
||||||
|
export const supportedAppErrors = {
|
||||||
|
dbError: (fctx: FlowExecCtx, detail: string): Err =>
|
||||||
|
getError({
|
||||||
|
flowId: fctx.flowId,
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Database operation failed",
|
||||||
|
description: "Please try again later",
|
||||||
|
detail,
|
||||||
|
}),
|
||||||
|
|
||||||
|
supportedAppNotFound: (fctx: FlowExecCtx, id: number): Err =>
|
||||||
|
getError({
|
||||||
|
flowId: fctx.flowId,
|
||||||
|
code: ERROR_CODES.NOT_FOUND,
|
||||||
|
message: "Supported app not found",
|
||||||
|
description: "The requested supported app does not exist",
|
||||||
|
detail: `No supported app found with ID: ${id}`,
|
||||||
|
}),
|
||||||
|
|
||||||
|
listFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||||
|
getError({
|
||||||
|
flowId: fctx.flowId,
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to list supported apps",
|
||||||
|
description: "Try again later",
|
||||||
|
detail,
|
||||||
|
}),
|
||||||
|
|
||||||
|
createFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||||
|
getError({
|
||||||
|
flowId: fctx.flowId,
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to create supported app",
|
||||||
|
description: "Try again later",
|
||||||
|
detail,
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||||
|
getError({
|
||||||
|
flowId: fctx.flowId,
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to update supported app",
|
||||||
|
description: "Try again later",
|
||||||
|
detail,
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteFailed: (fctx: FlowExecCtx, detail: string): Err =>
|
||||||
|
getError({
|
||||||
|
flowId: fctx.flowId,
|
||||||
|
code: ERROR_CODES.DATABASE_ERROR,
|
||||||
|
message: "Failed to delete supported app",
|
||||||
|
description: "Try again later",
|
||||||
|
detail,
|
||||||
|
}),
|
||||||
|
};
|
||||||
149
packages/logic/domains/supported-app/repository.ts
Normal file
149
packages/logic/domains/supported-app/repository.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||||
|
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||||
|
import { Database, asc, eq } from "@pkg/db";
|
||||||
|
import { supportedApp } from "@pkg/db/schema";
|
||||||
|
import { type Err } from "@pkg/result";
|
||||||
|
import { logger } from "@pkg/logger";
|
||||||
|
import { traceResultAsync } from "@core/observability";
|
||||||
|
import {
|
||||||
|
CreateSupportedApp,
|
||||||
|
SupportedApp,
|
||||||
|
UpdateSupportedApp,
|
||||||
|
} from "./data";
|
||||||
|
import { supportedAppErrors } from "./errors";
|
||||||
|
|
||||||
|
export class SupportedAppRepository {
|
||||||
|
constructor(private db: Database) {}
|
||||||
|
|
||||||
|
list(fctx: FlowExecCtx): ResultAsync<SupportedApp[], Err> {
|
||||||
|
return traceResultAsync({
|
||||||
|
name: "supportedApp.list",
|
||||||
|
fctx,
|
||||||
|
fn: () =>
|
||||||
|
ResultAsync.fromPromise(
|
||||||
|
this.db
|
||||||
|
.select()
|
||||||
|
.from(supportedApp)
|
||||||
|
.orderBy(asc(supportedApp.createdAt)),
|
||||||
|
(e) =>
|
||||||
|
supportedAppErrors.listFailed(
|
||||||
|
fctx,
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
),
|
||||||
|
).map((rows) => rows as SupportedApp[]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(fctx: FlowExecCtx, id: number): ResultAsync<SupportedApp, Err> {
|
||||||
|
return traceResultAsync({
|
||||||
|
name: "supportedApp.getById",
|
||||||
|
fctx,
|
||||||
|
fn: () =>
|
||||||
|
ResultAsync.fromPromise(
|
||||||
|
this.db.query.supportedApp.findFirst({
|
||||||
|
where: eq(supportedApp.id, id),
|
||||||
|
}),
|
||||||
|
(e) =>
|
||||||
|
supportedAppErrors.dbError(
|
||||||
|
fctx,
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
),
|
||||||
|
).andThen((row) => {
|
||||||
|
if (!row) {
|
||||||
|
return errAsync(
|
||||||
|
supportedAppErrors.supportedAppNotFound(fctx, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return okAsync(row as SupportedApp);
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
create(
|
||||||
|
fctx: FlowExecCtx,
|
||||||
|
data: CreateSupportedApp,
|
||||||
|
): ResultAsync<SupportedApp, Err> {
|
||||||
|
logger.info("Creating supported app", { ...fctx, title: data.title });
|
||||||
|
|
||||||
|
return traceResultAsync({
|
||||||
|
name: "supportedApp.create",
|
||||||
|
fctx,
|
||||||
|
fn: () =>
|
||||||
|
ResultAsync.fromPromise(
|
||||||
|
this.db
|
||||||
|
.insert(supportedApp)
|
||||||
|
.values({
|
||||||
|
title: data.title,
|
||||||
|
packageName: data.packageName,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.execute(),
|
||||||
|
(e) =>
|
||||||
|
supportedAppErrors.createFailed(
|
||||||
|
fctx,
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
),
|
||||||
|
).map((rows) => rows[0] as SupportedApp),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
fctx: FlowExecCtx,
|
||||||
|
id: number,
|
||||||
|
updates: UpdateSupportedApp,
|
||||||
|
): ResultAsync<SupportedApp, Err> {
|
||||||
|
return traceResultAsync({
|
||||||
|
name: "supportedApp.update",
|
||||||
|
fctx,
|
||||||
|
fn: () =>
|
||||||
|
this.getById(fctx, id).andThen(() =>
|
||||||
|
ResultAsync.fromPromise(
|
||||||
|
this.db
|
||||||
|
.update(supportedApp)
|
||||||
|
.set({ ...updates, updatedAt: new Date() })
|
||||||
|
.where(eq(supportedApp.id, id))
|
||||||
|
.returning()
|
||||||
|
.execute(),
|
||||||
|
(e) =>
|
||||||
|
supportedAppErrors.updateFailed(
|
||||||
|
fctx,
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
),
|
||||||
|
).andThen((rows) => {
|
||||||
|
if (!rows[0]) {
|
||||||
|
return errAsync(
|
||||||
|
supportedAppErrors.supportedAppNotFound(
|
||||||
|
fctx,
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return okAsync(rows[0] as SupportedApp);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(fctx: FlowExecCtx, id: number): ResultAsync<boolean, Err> {
|
||||||
|
return traceResultAsync({
|
||||||
|
name: "supportedApp.delete",
|
||||||
|
fctx,
|
||||||
|
fn: () =>
|
||||||
|
this.getById(fctx, id).andThen(() =>
|
||||||
|
ResultAsync.fromPromise(
|
||||||
|
this.db
|
||||||
|
.delete(supportedApp)
|
||||||
|
.where(eq(supportedApp.id, id))
|
||||||
|
.execute(),
|
||||||
|
(e) =>
|
||||||
|
supportedAppErrors.deleteFailed(
|
||||||
|
fctx,
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
),
|
||||||
|
).map(() => true),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export const settingsSchema = v.object({
|
|||||||
debugKey: v.string(),
|
debugKey: v.string(),
|
||||||
|
|
||||||
orchestratorApiUrl: v.string(),
|
orchestratorApiUrl: v.string(),
|
||||||
wsScrcpySvcUrl: v.string(),
|
publicWsScrcpySvcUrl: v.string(),
|
||||||
|
|
||||||
betterAuthUrl: v.string(),
|
betterAuthUrl: v.string(),
|
||||||
betterAuthSecret: v.string(),
|
betterAuthSecret: v.string(),
|
||||||
@@ -87,7 +87,7 @@ function loadSettings(): Settings {
|
|||||||
"ORCHESTRATOR_API_URL",
|
"ORCHESTRATOR_API_URL",
|
||||||
"http://localhost:3000",
|
"http://localhost:3000",
|
||||||
),
|
),
|
||||||
wsScrcpySvcUrl: getEnv("WS_SCRCPY_SVC_URL"),
|
publicWsScrcpySvcUrl: getEnv("PUBLIC_WS_SCRCPY_SVC_URL"),
|
||||||
|
|
||||||
betterAuthUrl: getEnv("BETTER_AUTH_URL"),
|
betterAuthUrl: getEnv("BETTER_AUTH_URL"),
|
||||||
betterAuthSecret: getEnv("BETTER_AUTH_SECRET"),
|
betterAuthSecret: getEnv("BETTER_AUTH_SECRET"),
|
||||||
|
|||||||
1
ws-scrcpy
Submodule
1
ws-scrcpy
Submodule
Submodule ws-scrcpy added at 49d2623184
Reference in New Issue
Block a user