From 8e0036358490009369f5f084f9f5985884595487 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 15:59:50 +0700 Subject: [PATCH 1/8] ui-2d: Fix build errors --- pkgs/clan-app/ui-2d/package.json | 85 +---------- .../clan-app/ui-2d/src/Form/fields/Select.tsx | 4 - pkgs/clan-app/ui-2d/src/api/index.tsx | 68 +++++++-- pkgs/clan-app/ui-2d/src/api_test.tsx | 2 +- .../src/components/Button/Button-Dark.css | 2 +- .../src/components/Button/Button-Ghost.css | 2 +- .../src/components/Button/Button-Light.css | 2 +- .../src/components/RemoteForm.stories.tsx | 6 +- .../ui-2d/src/components/RemoteForm.tsx | 72 +++------- .../ui-2d/src/components/SimpleModal.tsx | 39 +++++ .../ui-2d/src/components/inputBase/index.tsx | 4 +- .../components/machine-list-item/index.tsx | 20 +-- .../ui-2d/src/components/modal/index.tsx | 134 ------------------ pkgs/clan-app/ui-2d/src/index.css | 2 +- pkgs/clan-app/ui-2d/src/routes/flash/view.tsx | 11 +- .../machines/components/InstallMachine.tsx | 1 - .../machines/components/MachineForm.tsx | 9 +- .../routes/machines/install/hardware-step.tsx | 2 +- .../src/routes/machines/install/vars-step.tsx | 4 +- .../src/routes/machines/machines-list.tsx | 2 + pkgs/clan-app/ui/src/api/index.tsx | 114 +++++++++------ 21 files changed, 220 insertions(+), 365 deletions(-) mode change 100644 => 120000 pkgs/clan-app/ui-2d/package.json create mode 100644 pkgs/clan-app/ui-2d/src/components/SimpleModal.tsx delete mode 100644 pkgs/clan-app/ui-2d/src/components/modal/index.tsx diff --git a/pkgs/clan-app/ui-2d/package.json b/pkgs/clan-app/ui-2d/package.json deleted file mode 100644 index d1d91551f..000000000 --- a/pkgs/clan-app/ui-2d/package.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "name": "@clan/ui", - "version": "0.0.1", - "description": "", - "type": "module", - "scripts": { - "start": "vite", - "dev": "vite", - "build": "npm run check && npm run test && vite build && npm run convert-html", - "convert-html": "node gtk.webview.js", - "serve": "vite preview", - "check": "tsc --noEmit --skipLibCheck && eslint ./src --fix", - "knip": "knip --fix", - "test": "vitest run --project unit --typecheck", - "storybook": "storybook", - "storybook-build": "storybook build", - "storybook-dev": "storybook dev -p 6006", - "test-storybook": "vitest run --project storybook", - "test-storybook-update-snapshots": "vitest run --project storybook --update", - "test-storybook-static": "npm run storybook-build && concurrently -k -s first -n 'SB,TEST' -c 'magenta,blue' 'npx http-server storybook-static --port 6006 --silent' 'npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook'" - }, - "license": "MIT", - "devDependencies": { - "@eslint/js": "^9.3.0", - "@kachurun/storybook-solid": "^9.0.11", - "@kachurun/storybook-solid-vite": "^9.0.11", - "@storybook/addon-a11y": "^9.0.8", - "@storybook/addon-docs": "^9.0.8", - "@storybook/addon-links": "^9.0.8", - "@storybook/addon-onboarding": "^9.0.8", - "@storybook/addon-vitest": "^9.0.8", - "@tailwindcss/typography": "^0.5.13", - "@types/json-schema": "^7.0.15", - "@types/node": "^22.15.19", - "@vitest/browser": "^3.2.3", - "autoprefixer": "^10.4.19", - "classnames": "^2.5.1", - "concurrently": "^9.1.2", - "eslint": "^9.27.0", - "eslint-plugin-tailwindcss": "^3.17.0", - "jsdom": "^26.1.0", - "knip": "^5.61.2", - "postcss": "^8.4.38", - "prettier": "^3.2.5", - "solid-devtools": "^0.34.0", - "storybook": "^9.0.8", - "tailwindcss": "^4.0.0", - "typescript": "^5.4.5", - "typescript-eslint": "^8.32.1", - "vite": "^7.0.0", - "vite-plugin-solid": "^2.8.2", - "vite-plugin-solid-svg": "^0.8.1", - "vitest": "^3.2.3" - }, - "dependencies": { - "@floating-ui/dom": "^1.6.8", - "@kobalte/core": "^0.13.10", - "@kobalte/tailwindcss": "^0.9.0", - "@modular-forms/solid": "^0.25.1", - "@solid-primitives/storage": "^4.3.2", - "@solidjs/router": "^0.15.3", - "@tanstack/eslint-plugin-query": "^5.51.12", - "@tanstack/solid-query": "^5.76.0", - "corvu": "^0.7.1", - "nanoid": "^5.0.7", - "solid-js": "^1.9.7", - "solid-toast": "^0.5.0" - }, - "optionalDependencies": { - "@esbuild/darwin-arm64": "^0.25.4", - "@esbuild/darwin-x64": "^0.25.4", - "@esbuild/linux-arm64": "^0.25.4", - "@esbuild/linux-x64": "^0.25.4" - }, - "overrides": { - "vite": { - "rollup": "npm:@rollup/wasm-node@^4.34.9" - }, - "@rollup/rollup-darwin-x64": "npm:@rollup/wasm-node@^4.34.9", - "@rollup/rollup-linux-x64": "npm:@rollup/wasm-node@^4.34.9", - "@rollup/rollup-darwin-arm64": "npm:@rollup/wasm-node@^4.34.9", - "@rollup/rollup-linux-arm64": "npm:@rollup/wasm-node@^4.34.9" - } -} diff --git a/pkgs/clan-app/ui-2d/package.json b/pkgs/clan-app/ui-2d/package.json new file mode 120000 index 000000000..c606d535c --- /dev/null +++ b/pkgs/clan-app/ui-2d/package.json @@ -0,0 +1 @@ +../ui/package.json \ No newline at end of file diff --git a/pkgs/clan-app/ui-2d/src/Form/fields/Select.tsx b/pkgs/clan-app/ui-2d/src/Form/fields/Select.tsx index bc9d33c72..a6a58089c 100644 --- a/pkgs/clan-app/ui-2d/src/Form/fields/Select.tsx +++ b/pkgs/clan-app/ui-2d/src/Form/fields/Select.tsx @@ -19,7 +19,6 @@ import { } from "@/src/components/inputBase"; import { FieldLayout } from "./layout"; import Icon from "@/src/components/icon"; -import { useContext } from "corvu/dialog"; interface Option { value: string; @@ -51,9 +50,6 @@ interface SelectInputpProps { } export function SelectInput(props: SelectInputpProps) { - const dialogContext = (dialogContextId?: string) => - useContext(dialogContextId); - const _id = createUniqueId(); const [reference, setReference] = createSignal(); diff --git a/pkgs/clan-app/ui-2d/src/api/index.tsx b/pkgs/clan-app/ui-2d/src/api/index.tsx index ad429b585..73d9b8612 100644 --- a/pkgs/clan-app/ui-2d/src/api/index.tsx +++ b/pkgs/clan-app/ui-2d/src/api/index.tsx @@ -23,9 +23,37 @@ export type SuccessQuery = Extract< >; export type SuccessData = SuccessQuery["data"]; +function isMachine(obj: unknown): obj is Machine { + return ( + !!obj && + typeof obj === "object" && + typeof (obj as any).name === "string" && + typeof (obj as any).flake === "object" && + typeof (obj as any).flake.identifier === "string" + ); +} + +// Machine type with flake for API calls +interface Machine { + name: string; + flake: { + identifier: string; + }; +} + +interface BackendOpts { + logging?: { group: string | Machine }; +} + +interface BackendReturnType { + result: OperationResponse; + metadata: Record; +} + const _callApi = ( method: K, args: OperationArgs, + backendOpts?: BackendOpts, ): { promise: Promise>; op_key: string } => { // if window[method] does not exist, throw an error if (!(method in window)) { @@ -33,19 +61,30 @@ const _callApi = ( // return a rejected promise return { promise: Promise.resolve({ - status: "error", - errors: [ - { - message: `Method ${method} not found on window object`, - code: "method_not_found", - }, - ], - op_key: "noop", + status: "error", + errors: [ + { + message: `Method ${method} not found on window object`, + code: "method_not_found", + }, + ], + op_key: "noop", }), op_key: "noop", }; } + let metadata: BackendOpts | undefined = undefined; + if (backendOpts != undefined) { + metadata = { ...backendOpts }; + let group = backendOpts?.logging?.group; + if (group != undefined && isMachine(group)) { + metadata = { + logging: { group: group.flake.identifier + "#" + group.name }, + }; + } + } + const promise = ( window as unknown as Record< OperationNames, @@ -105,10 +144,11 @@ const handleCancel = async ( export const callApi = ( method: K, args: OperationArgs, + backendOpts?: BackendOpts, ): { promise: Promise>; op_key: string } => { - console.log("Calling API", method, args); + console.log("Calling API", method, args, backendOpts); - const { promise, op_key } = _callApi(method, args); + const { promise, op_key } = _callApi(method, args, backendOpts); promise.catch((error) => { toast.custom( (t) => ( @@ -146,13 +186,14 @@ export const callApi = ( console.log("Not printing toast because operation was cancelled"); } - if (response.status === "error" && !cancelled) { + const result = response; + if (result.status === "error" && !cancelled) { toast.remove(toastId); toast.custom( (t) => ( ), { @@ -162,7 +203,8 @@ export const callApi = ( } else { toast.remove(toastId); } - return response; + return result; }); + return { promise: new_promise, op_key: op_key }; }; diff --git a/pkgs/clan-app/ui-2d/src/api_test.tsx b/pkgs/clan-app/ui-2d/src/api_test.tsx index 49599f059..76ab8ac65 100644 --- a/pkgs/clan-app/ui-2d/src/api_test.tsx +++ b/pkgs/clan-app/ui-2d/src/api_test.tsx @@ -61,7 +61,7 @@ export const ApiTester = () => { return await callApi( values.endpoint as keyof API, JSON.parse(values.payload || "{}"), - ); + ).promise; }, staleTime: Infinity, enabled: false, diff --git a/pkgs/clan-app/ui-2d/src/components/Button/Button-Dark.css b/pkgs/clan-app/ui-2d/src/components/Button/Button-Dark.css index 07faf3f30..914fc74b9 100644 --- a/pkgs/clan-app/ui-2d/src/components/Button/Button-Dark.css +++ b/pkgs/clan-app/ui-2d/src/components/Button/Button-Dark.css @@ -27,5 +27,5 @@ } .button--dark-active:active { - @apply active:border-secondary-900 active:shadow-button-primary-active; + @apply active:border-secondary-900; } diff --git a/pkgs/clan-app/ui-2d/src/components/Button/Button-Ghost.css b/pkgs/clan-app/ui-2d/src/components/Button/Button-Ghost.css index dcbd77556..ad9881bb7 100644 --- a/pkgs/clan-app/ui-2d/src/components/Button/Button-Ghost.css +++ b/pkgs/clan-app/ui-2d/src/components/Button/Button-Ghost.css @@ -7,5 +7,5 @@ } .button--ghost-active:active { - @apply active:bg-secondary-200 active:text-secondary-900 active:shadow-button-primary-active; + @apply active:bg-secondary-200 active:text-secondary-900; } diff --git a/pkgs/clan-app/ui-2d/src/components/Button/Button-Light.css b/pkgs/clan-app/ui-2d/src/components/Button/Button-Light.css index dde186706..1ec847ac7 100644 --- a/pkgs/clan-app/ui-2d/src/components/Button/Button-Light.css +++ b/pkgs/clan-app/ui-2d/src/components/Button/Button-Light.css @@ -27,7 +27,7 @@ } .button--light-active:active { - @apply active:bg-secondary-200 border-secondary-600 active:text-secondary-900 active:shadow-button-primary-active; + @apply active:bg-secondary-200 border-secondary-600 active:text-secondary-900; box-shadow: inset 2px 2px theme(backgroundColor.secondary.300); diff --git a/pkgs/clan-app/ui-2d/src/components/RemoteForm.stories.tsx b/pkgs/clan-app/ui-2d/src/components/RemoteForm.stories.tsx index 9df499beb..7c6086de4 100644 --- a/pkgs/clan-app/ui-2d/src/components/RemoteForm.stories.tsx +++ b/pkgs/clan-app/ui-2d/src/components/RemoteForm.stories.tsx @@ -17,7 +17,7 @@ const defaultRemoteData: RemoteData = { private_key: undefined, password: "", forward_agent: true, - host_key_check: 0, + host_key_check: "strict", verbose_ssh: false, ssh_options: {}, tor_socks: false, @@ -32,7 +32,7 @@ const sampleRemoteData: RemoteData = { private_key: undefined, password: "", forward_agent: true, - host_key_check: 1, + host_key_check: "ask", verbose_ssh: false, ssh_options: { StrictHostKeyChecking: "no", @@ -238,7 +238,7 @@ const advancedRemoteData: RemoteData = { private_key: undefined, password: "", forward_agent: false, - host_key_check: 2, + host_key_check: "none", verbose_ssh: true, ssh_options: { ConnectTimeout: "10", diff --git a/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx b/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx index 4a751f478..fb54b1a69 100644 --- a/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx +++ b/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx @@ -11,13 +11,6 @@ import { Loader } from "@/src/components/v2/Loader/Loader"; import { Button } from "@/src/components/v2/Button/Button"; import Accordion from "@/src/components/accordion"; -// Define the HostKeyCheck enum values with proper API mapping -export enum HostKeyCheck { - ASK = 0, - TOFU = 1, - IGNORE = 2, -} - // Export the API types for use in other components export type { RemoteData, Machine, RemoteDataSource }; @@ -185,40 +178,6 @@ export function RemoteForm(props: RemoteFormProps) { const [formData, setFormData] = createSignal(null); const [isSaving, setIsSaving] = createSignal(false); - const hostKeyCheckOptions = [ - { value: "ASK", label: "Ask" }, - { value: "TOFU", label: "TOFU (Trust On First Use)" }, - { value: "IGNORE", label: "Ignore" }, - ]; - - // Helper function to convert enum name to numeric value - const getHostKeyCheckValue = (name: string): number => { - switch (name) { - case "ASK": - return HostKeyCheck.ASK; - case "TOFU": - return HostKeyCheck.TOFU; - case "IGNORE": - return HostKeyCheck.IGNORE; - default: - return HostKeyCheck.ASK; - } - }; - - // Helper function to convert numeric value to enum name - const getHostKeyCheckName = (value: number | undefined): string => { - switch (value) { - case HostKeyCheck.ASK: - return "ASK"; - case HostKeyCheck.TOFU: - return "TOFU"; - case HostKeyCheck.IGNORE: - return "IGNORE"; - default: - return "ASK"; - } - }; - // Query host data when machine is provided const hostQuery = useQuery(() => ({ queryKey: [ @@ -241,11 +200,15 @@ export function RemoteForm(props: RemoteFormProps) { }); } - const result = await callApi("get_host", { - name: props.machine.name, - flake: props.machine.flake, - field: props.field || "targetHost", - }).promise; + const result = await callApi( + "get_host", + { + name: props.machine.name, + flake: props.machine.flake, + field: props.field || "targetHost", + }, + {logging: { group: { name: props.machine.name, flake: props.machine.flake } },}, + ).promise; if (result.status === "error") throw new Error("Failed to fetch host data"); @@ -372,16 +335,13 @@ export function RemoteForm(props: RemoteFormProps) { - updateFormData({ - host_key_check: getHostKeyCheckValue( - e.currentTarget.value, - ) as 0 | 1 | 2 | 3, - }), - }} + value={formData()?.host_key_check || "ask"} + options={[ + { value: "ask", label: "Ask" }, + { value: "none", label: "None" }, + { value: "strict", label: "Strict" }, + { value: "tofu", label: "Trust on First Use" }, + ]} disabled={computedDisabled} helperText="How to handle host key verification" /> diff --git a/pkgs/clan-app/ui-2d/src/components/SimpleModal.tsx b/pkgs/clan-app/ui-2d/src/components/SimpleModal.tsx new file mode 100644 index 000000000..eb0395d43 --- /dev/null +++ b/pkgs/clan-app/ui-2d/src/components/SimpleModal.tsx @@ -0,0 +1,39 @@ +import { JSX, Show } from "solid-js"; + +interface SimpleModalProps { + open: boolean; + onClose: () => void; + title?: string; + children: JSX.Element; +} + +export const SimpleModal = (props: SimpleModalProps) => { + return ( + +
+ {/* Backdrop */} +
+ + {/* Modal Content */} +
+ {/* Header */} + +
+

{props.title}

+ +
+
+ + {/* Body */} +
{props.children}
+
+
+ + ); +}; diff --git a/pkgs/clan-app/ui-2d/src/components/inputBase/index.tsx b/pkgs/clan-app/ui-2d/src/components/inputBase/index.tsx index b0c1d50fe..cce3e5435 100644 --- a/pkgs/clan-app/ui-2d/src/components/inputBase/index.tsx +++ b/pkgs/clan-app/ui-2d/src/components/inputBase/index.tsx @@ -125,7 +125,7 @@ export const InputLabel = (props: InputLabelProps) => { weight="bold" class="inline-flex gap-1 align-middle !fg-def-1" classList={{ - [cx("!fg-semantic-1")]: !!props.error, + [cx("!text-red-600")]: !!props.error, }} aria-invalid={props.error} > @@ -185,7 +185,7 @@ export const InputError = (props: InputErrorProps) => { // @ts-expect-error: Dependent type is to complex to check how it is coupled to the override for now size="xxs" weight="medium" - class={cx("col-span-full px-1 !fg-semantic-4", typoClasses)} + class={cx("col-span-full px-1 !text-red-500", typoClasses)} {...rest} > {props.error} diff --git a/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx b/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx index b0881bf79..221ff81a6 100644 --- a/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx +++ b/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx @@ -47,11 +47,14 @@ export const MachineListItem = (props: MachineListItemProps) => { ); return; } - const target_host = await callApi("get_host", { - field: "targetHost", - flake: { identifier: active_clan }, - name: name, - }).promise; + const target_host = await callApi( + "get_host", + { + field: "targetHost", + flake: { identifier: active_clan }, + name: name, + }, {logging: { group: { name, flake: { identifier: active_clan } } }} + ).promise; if (target_host.status == "error") { console.error("No target host found for the machine"); @@ -79,7 +82,6 @@ export const MachineListItem = (props: MachineListItemProps) => { }, no_reboot: true, debug: true, - nix_options: [], password: null, }, target_host: target_host.data!.data, @@ -108,6 +110,8 @@ export const MachineListItem = (props: MachineListItemProps) => { field: "targetHost", flake: { identifier: active_clan }, name: name, + }, { + logging: { group: { name, flake: { identifier: active_clan } } }, }).promise; if (target_host.status == "error") { @@ -129,7 +133,7 @@ export const MachineListItem = (props: MachineListItemProps) => { field: "buildHost", flake: { identifier: active_clan }, name: name, - }).promise; + }, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise; if (build_host.status == "error") { console.error("No target host found for the machine"); @@ -150,7 +154,7 @@ export const MachineListItem = (props: MachineListItemProps) => { }, target_host: target_host.data!.data, build_host: build_host.data?.data || null, - }).promise; + }, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise; setUpdating(false); }; diff --git a/pkgs/clan-app/ui-2d/src/components/modal/index.tsx b/pkgs/clan-app/ui-2d/src/components/modal/index.tsx deleted file mode 100644 index b568acf9f..000000000 --- a/pkgs/clan-app/ui-2d/src/components/modal/index.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import Dialog from "corvu/dialog"; -import { createSignal, JSX } from "solid-js"; -import { Button } from "../Button/Button"; -import Icon from "../icon"; -import cx from "classnames"; - -interface ModalProps { - open: boolean | undefined; - handleClose: () => void; - title: string; - children: JSX.Element; - class?: string; -} -export const Modal = (props: ModalProps) => { - const [dragging, setDragging] = createSignal(false); - const [startOffset, setStartOffset] = createSignal({ x: 0, y: 0 }); - - let dialogRef: HTMLDivElement; - - const handleMouseDown = (e: MouseEvent) => { - setDragging(true); - const rect = dialogRef.getBoundingClientRect(); - setStartOffset({ - x: e.clientX - rect.left, - y: e.clientY - rect.top, - }); - }; - - const handleMouseMove = (e: MouseEvent) => { - if (dragging()) { - let newTop = e.clientY - startOffset().y; - let newLeft = e.clientX - startOffset().x; - - if (newTop < 0) { - newTop = 0; - } - if (newLeft < 0) { - newLeft = 0; - } - dialogRef.style.top = `${newTop}px`; - dialogRef.style.left = `${newLeft}px`; - - // dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`; - // dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`; - } - }; - - const handleMouseUp = () => setDragging(false); - - return ( - - - - - { - dialogRef = el; - }} - onMouseMove={handleMouseMove} - onMouseUp={handleMouseUp} - onMouseDown={(e: MouseEvent) => { - e.stopPropagation(); // Prevent backdrop drag conflict - }} - onClick={(e: MouseEvent) => e.stopPropagation()} // Prevent backdrop click closing - > - -
-
-
-
-
-
-
-
- - {props.title} - -
-
-
-
-
-
-
-
- -
-
-
- - {props.children} - -
-
-
- ); -}; diff --git a/pkgs/clan-app/ui-2d/src/index.css b/pkgs/clan-app/ui-2d/src/index.css index e645ad29d..909b198bf 100644 --- a/pkgs/clan-app/ui-2d/src/index.css +++ b/pkgs/clan-app/ui-2d/src/index.css @@ -1,4 +1,4 @@ -@import "material-icons/iconfont/filled.css"; +/* @import "material-icons/iconfont/filled.css"; */ /* List of icons: https://marella.me/material-icons/demo/ */ /* @import url(./components/Typography/css/typography.css); */ diff --git a/pkgs/clan-app/ui-2d/src/routes/flash/view.tsx b/pkgs/clan-app/ui-2d/src/routes/flash/view.tsx index ec91d3d51..c5d0a645d 100644 --- a/pkgs/clan-app/ui-2d/src/routes/flash/view.tsx +++ b/pkgs/clan-app/ui-2d/src/routes/flash/view.tsx @@ -19,10 +19,12 @@ import { createEffect, createSignal } from "solid-js"; // For, Show might not be import toast from "solid-toast"; import { FieldLayout } from "@/src/Form/fields/layout"; import { InputLabel } from "@/src/components/inputBase"; -import { Modal } from "@/src/components/modal"; + import Fieldset from "@/src/Form/fieldset"; // Still used for other fieldsets import Accordion from "@/src/components/accordion"; +import { SimpleModal } from "@/src/components/SimpleModal"; + // Import the new generic component import { FileSelectorField, @@ -192,12 +194,11 @@ export const Flash = () => { return ( <>
- !isFlashing() && setConfirmOpen(false)} + onClose={() => !isFlashing() && setConfirmOpen(false)} title="Confirm" > - {/* ... Modal content as before ... */}
{
-
+
{ + }, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }}).promise.finally(() => { setIsUpdating(false); }); }; diff --git a/pkgs/clan-app/ui-2d/src/routes/machines/install/hardware-step.tsx b/pkgs/clan-app/ui-2d/src/routes/machines/install/hardware-step.tsx index c22691ac3..291937e21 100644 --- a/pkgs/clan-app/ui-2d/src/routes/machines/install/hardware-step.tsx +++ b/pkgs/clan-app/ui-2d/src/routes/machines/install/hardware-step.tsx @@ -44,7 +44,7 @@ export const HWStep = (props: StepProps) => { command_prefix: "sudo", port: 22, forward_agent: false, - host_key_check: 1, // 0 = ASK + host_key_check: "ask", // 0 = ASK verbose_ssh: false, ssh_options: {}, tor_socks: false, diff --git a/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx b/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx index 6045bfb78..0b64af9e0 100644 --- a/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx +++ b/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx @@ -153,10 +153,10 @@ export const VarsStep = (props: VarsStepProps) => { base_dir: props.dir, machine_name: props.machine_id, full_closure: props.fullClosure, - }).promise; + }, {logging: {group: { name: props.machine_id, flake: {identifier: props.dir} }}}).promise; if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; - }, + }, })); const handleSubmit: SubmitHandler = async (values, event) => { diff --git a/pkgs/clan-app/ui-2d/src/routes/machines/machines-list.tsx b/pkgs/clan-app/ui-2d/src/routes/machines/machines-list.tsx index 88919659c..fd043494d 100644 --- a/pkgs/clan-app/ui-2d/src/routes/machines/machines-list.tsx +++ b/pkgs/clan-app/ui-2d/src/routes/machines/machines-list.tsx @@ -8,6 +8,7 @@ import Icon from "@/src/components/icon"; import { Header } from "@/src/layout/header"; import { makePersisted } from "@solid-primitives/storage"; import { useClanContext } from "@/src/contexts/clan"; +import { debug } from "console"; type MachinesModel = Extract< OperationResponse<"list_machines">, @@ -38,6 +39,7 @@ export const MachineListView: Component = () => { }, }).promise; console.log("response", response); + if (response.status === "error") { console.error("Failed to fetch data"); } else { diff --git a/pkgs/clan-app/ui/src/api/index.tsx b/pkgs/clan-app/ui/src/api/index.tsx index 59139290d..bbc40bd16 100644 --- a/pkgs/clan-app/ui/src/api/index.tsx +++ b/pkgs/clan-app/ui/src/api/index.tsx @@ -1,26 +1,19 @@ -import { API, Error as ApiError } from "@/api/API"; +import { API } from "@/api/API"; import { Schema as Inventory } from "@/api/Inventory"; import { toast } from "solid-toast"; import { ErrorToastComponent, CancelToastComponent, } from "@/src/components/toast"; + type OperationNames = keyof API; -type OperationArgs = API[T]["arguments"]; -export type OperationResponse = API[T]["return"]; - -type ApiEnvelope = - | { - status: "success"; - data: T; - op_key: string; - } - | ApiError; - type Services = NonNullable; type ServiceNames = keyof Services; -type ClanService = Services[T]; -type ClanServiceInstance = NonNullable< + +export type OperationArgs = API[T]["arguments"]; +export type OperationResponse = API[T]["return"]; + +export type ClanServiceInstance = NonNullable< Services[T] >[string]; @@ -28,51 +21,83 @@ export type SuccessQuery = Extract< OperationResponse, { status: "success" } >; -type SuccessData = SuccessQuery["data"]; +export type SuccessData = SuccessQuery["data"]; -type ErrorQuery = Extract< - OperationResponse, - { status: "error" } ->; -type ErrorData = ErrorQuery["errors"]; - -type ClanOperations = Record void>; - -interface GtkResponse { - result: T; - op_key: string; +function isMachine(obj: unknown): obj is Machine { + return ( + !!obj && + typeof obj === "object" && + typeof (obj as Machine).name === "string" && + typeof (obj as Machine).flake === "object" && + typeof (obj as Machine).flake.identifier === "string" + ); } + +// Machine type with flake for API calls +interface Machine { + name: string; + flake: { + identifier: string; + }; +} + +interface BackendOpts { + logging?: { group: string | Machine }; +} + +interface BackendReturnType { + result: OperationResponse; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata: Record; +} + const _callApi = ( method: K, args: OperationArgs, -): { promise: Promise>; op_key: string } => { + backendOpts?: BackendOpts, +): { promise: Promise>; op_key: string } => { // if window[method] does not exist, throw an error if (!(method in window)) { console.error(`Method ${method} not found on window object`); // return a rejected promise return { promise: Promise.resolve({ - status: "error", - errors: [ - { - message: `Method ${method} not found on window object`, - code: "method_not_found", - }, - ], - op_key: "noop", + result: { + status: "error", + errors: [ + { + message: `Method ${method} not found on window object`, + code: "method_not_found", + }, + ], + op_key: "noop", + }, + metadata: {}, }), op_key: "noop", }; } + let metadata: BackendOpts | undefined = undefined; + if (backendOpts != undefined) { + metadata = { ...backendOpts }; + const group = backendOpts?.logging?.group; + if (group != undefined && isMachine(group)) { + metadata = { + logging: { group: group.flake.identifier + "#" + group.name }, + }; + } + } + const promise = ( window as unknown as Record< OperationNames, ( args: OperationArgs, - ) => Promise> + metadata?: BackendOpts, + ) => Promise> > - )[method](args) as Promise>; + )[method](args, metadata) as Promise>; // eslint-disable-next-line @typescript-eslint/no-explicit-any const op_key = (promise as any)._webviewMessageId as string; @@ -82,7 +107,7 @@ const _callApi = ( const handleCancel = async ( ops_key: string, - orig_task: Promise>, + orig_task: Promise>, ) => { console.log("Canceling operation: ", ops_key); const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); @@ -102,7 +127,7 @@ const handleCancel = async ( }); const resp = await promise; - if (resp.status === "error") { + if (resp.result.status === "error") { toast.custom( (t) => ( ( export const callApi = ( method: K, args: OperationArgs, + backendOpts?: BackendOpts, ): { promise: Promise>; op_key: string } => { console.log("Calling API", method, args); - const { promise, op_key } = _callApi(method, args); + const { promise, op_key } = _callApi(method, args, backendOpts); promise.catch((error) => { toast.custom( (t) => ( @@ -165,13 +191,14 @@ export const callApi = ( console.log("Not printing toast because operation was cancelled"); } - if (response.status === "error" && !cancelled) { + const result = response.result; + if (result.status === "error" && !cancelled) { toast.remove(toastId); toast.custom( (t) => ( ), { @@ -181,7 +208,8 @@ export const callApi = ( } else { toast.remove(toastId); } - return response; + return result; }); + return { promise: new_promise, op_key: op_key }; }; From 9080e7c7f6883c071d6f3a87beabeaa051199cb2 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 16:00:34 +0700 Subject: [PATCH 2/8] clan-app: Fix .local.env not being sourced --- pkgs/clan-app/.envrc | 2 +- pkgs/clan-app/shell.nix | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkgs/clan-app/.envrc b/pkgs/clan-app/.envrc index 856067707..30917370b 100644 --- a/pkgs/clan-app/.envrc +++ b/pkgs/clan-app/.envrc @@ -1,7 +1,7 @@ # shellcheck shell=bash source_up -watch_file flake-module.nix shell.nix webview-ui/flake-module.nix +watch_file .local.env flake-module.nix shell.nix webview-ui/flake-module.nix # Because we depend on nixpkgs sources, uploading to builders takes a long time use flake .#clan-app --builders '' diff --git a/pkgs/clan-app/shell.nix b/pkgs/clan-app/shell.nix index a87b5b0ff..5c03c4b2f 100644 --- a/pkgs/clan-app/shell.nix +++ b/pkgs/clan-app/shell.nix @@ -89,9 +89,10 @@ mkShell { popd # configure process-compose - if test -f "$GIT_ROOT/pkgs/clan-app/.local.env"; then - source "$GIT_ROOT/pkgs/clan-app/.local.env" + if test -f "$CLAN_CORE_PATH/pkgs/clan-app/.local.env"; then + source "$CLAN_CORE_PATH/pkgs/clan-app/.local.env" fi + export PC_CONFIG_FILES="$CLAN_CORE_PATH/pkgs/clan-app/process-compose.yaml" echo -e "${GREEN}To launch a qemu VM for testing, run:\n start-vm ${NC}" From d5064ce465f1ae9199223a585008806dbe32f592 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 16:00:55 +0700 Subject: [PATCH 3/8] clan-app: Add pygdb.sh for debugging crashes in webview-lib --- pkgs/clan-app/README.md | 60 +++++++++-------------------------------- pkgs/clan-app/pygdb.sh | 5 ++++ 2 files changed, 17 insertions(+), 48 deletions(-) create mode 100755 pkgs/clan-app/pygdb.sh diff --git a/pkgs/clan-app/README.md b/pkgs/clan-app/README.md index aff0d825d..14cce1aeb 100644 --- a/pkgs/clan-app/README.md +++ b/pkgs/clan-app/README.md @@ -103,6 +103,18 @@ GTK_DEBUG=interactive ./bin/clan-app --debug Appending `--debug` flag enables debug logging printed into the console. +Debugging crashes in the `webview` library can be done by executing: + +```bash +$ ./pygdb.sh ./bin/clan-app --content-uri http://localhost:3000/ --debug +``` + +I recommend creating the file `.local.env` with the content: +```bash +export WEBVIEW_LIB_DIR=$HOME/Projects/webview/build/core +``` +where `WEBVIEW_LIB_DIR` points to a local checkout of the webview lib source, that has been build by hand. The `.local.env` file will be automatically sourced if it exists and will be ignored by git. + ### Profiling To activate profiling you can run @@ -111,51 +123,3 @@ To activate profiling you can run CLAN_CLI_PERF=1 ./bin/clan-app ``` -### Library Components - -> Note: -> -> we recognized bugs when starting some cli-commands through the integrated vs-code terminal. -> If encountering issues make sure to run commands in a regular os-shell. - -lib-Adw has a demo application showing all widgets. You can run it by executing - -```bash -adwaita-1-demo -``` - -GTK4 has a demo application showing all widgets. You can run it by executing - -```bash -gtk4-widget-factory -``` - -To find available icons execute - -```bash -gtk4-icon-browser -``` - -### Links - -Here are some important documentation links related to the Clan App: - -- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0): This link provides the PyGObject reference documentation for GTK4, the toolkit used for building the user interface of the clan app. It includes information about GTK4 widgets, signals, and other features. - -- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html): This link showcases a widget gallery for Adw, allowing you to see the available widgets and their visual appearance. It can be helpful for designing the user interface of the clan app. - -- [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/): This link provides the GNOME Human Interface Guidelines, which offer design and usability recommendations for creating GNOME applications. It covers topics such as layout, navigation, and interaction patterns. - -## Error handling - -> Error dialogs should be avoided where possible, since they are disruptive. -> -> For simple non-critical errors, toasts can be a good alternative. - - -[direnv]: https://direnv.net/ -[process-compose]: https://f1bonacc1.github.io/process-compose/ -[vite]: https://vite.dev/ -[webview]: https://github.com/webview/webview -[Storybook]: https://storybook.js.org/ -[webkit]: https://webkit.org/ \ No newline at end of file diff --git a/pkgs/clan-app/pygdb.sh b/pkgs/clan-app/pygdb.sh new file mode 100755 index 000000000..a861b801d --- /dev/null +++ b/pkgs/clan-app/pygdb.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + + +PYTHON_DIR=$(dirname "$(which python3)")/.. +gdb --quiet -ex "source $PYTHON_DIR/share/gdb/libpython.py" --ex "sharedlib $WEBVIEW_LIB_DIR/libwebview.so" --ex "run" --args python "$@" From 1ec67ecfaf1a8966f16fa7f8cf2547ced0a412d5 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 16:16:37 +0700 Subject: [PATCH 4/8] webview-lib: Moved repo to gitea, updated revision. Removed set_icon --- .../clan_app/deps/webview/_webview_ffi.py | 7 ++- .../clan-app/clan_app/deps/webview/webview.py | 56 ++++++++----------- pkgs/clan-app/webview-lib/default.nix | 18 ++++-- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py index d5640fc89..d719aa15e 100644 --- a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py +++ b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py @@ -88,9 +88,6 @@ class _WebviewLibrary: self.webview_set_title = self.lib.webview_set_title self.webview_set_title.argtypes = [c_void_p, c_char_p] - self.webview_set_icon = self.lib.webview_set_icon - self.webview_set_icon.argtypes = [c_void_p, c_char_p] - self.webview_set_size = self.lib.webview_set_size self.webview_set_size.argtypes = [c_void_p, c_int, c_int, c_int] @@ -112,6 +109,10 @@ class _WebviewLibrary: self.webview_return = self.lib.webview_return self.webview_return.argtypes = [c_void_p, c_char_p, c_int, c_char_p] + self.binding_callback_t = CFUNCTYPE( + None, c_char_p, c_char_p, c_void_p + ) + self.CFUNCTYPE = CFUNCTYPE diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index 8a0a6946c..ad24fcfd7 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -1,11 +1,9 @@ -import ctypes import functools import io import json import logging import threading from collections.abc import Callable -from dataclasses import dataclass from enum import IntEnum from typing import Any @@ -16,6 +14,7 @@ from clan_lib.api import ( dataclass_to_dict, from_dict, ) +from clan_lib.api.tasks import WebThread from clan_lib.async_run import AsyncContext, get_async_ctx, set_async_ctx from clan_lib.custom_logger import setup_logging from clan_lib.log_manager import LogManager @@ -44,12 +43,6 @@ class Size: self.hint = hint -@dataclass -class WebThread: - thread: threading.Thread - stop_event: threading.Event - - class Webview: def __init__( self, debug: bool = False, size: Size | None = None, window: int | None = None @@ -74,20 +67,22 @@ class Webview: op_key = op_key_bytes.decode() args = json.loads(request_data.decode()) log.debug(f"Calling {method_name}({args[0]})") + metadata: dict[str, Any] = {} try: # Initialize dataclasses from the payload reconciled_arguments = {} - for k, v in args[0].items(): - # Some functions expect to be called with dataclass instances - # But the js api returns dictionaries. - # Introspect the function and create the expected dataclass from dict dynamically - # Depending on the introspected argument_type - arg_class = api.get_method_argtype(method_name, k) + if len(args) > 0: + for k, v in args[0].items(): + # Some functions expect to be called with dataclass instances + # But the js api returns dictionaries. + # Introspect the function and create the expected dataclass from dict dynamically + # Depending on the introspected argument_type + arg_class = api.get_method_argtype(method_name, k) - # TODO: rename from_dict into something like construct_checked_value - # from_dict really takes Anything and returns an instance of the type/class - reconciled_arguments[k] = from_dict(arg_class, v) + # TODO: rename from_dict into something like construct_checked_value + # from_dict really takes Anything and returns an instance of the type/class + reconciled_arguments[k] = from_dict(arg_class, v) reconciled_arguments["op_key"] = op_key except Exception as e: @@ -112,8 +107,16 @@ class Webview: def thread_task(stop_event: threading.Event) -> None: ctx: AsyncContext = get_async_ctx() ctx.should_cancel = lambda: stop_event.is_set() + # If the API call has set log_group in metadata, + # create the log file under that group. + log_group = metadata.get("logging", {}).get("group", None) + if log_group is not None: + log.warning( + f"Using log group {log_group} for {method_name} with op_key {op_key}" + ) + breakpoint() log_file = log_manager.create_log_file( - wrap_method, op_key=op_key + wrap_method, op_key=op_key, group=log_group ).get_file_path() with log_file.open("ab") as log_f: @@ -129,7 +132,6 @@ class Webview: handler = setup_logging( log.getEffectiveLevel(), log_file=handler_stream ) - log.info("Starting thread for webview API call") try: # Original logic: call the wrapped API method. @@ -204,14 +206,6 @@ class Webview: _webview_lib.webview_set_title(self._handle, _encode_c_string(value)) self._title = value - @property - def icon(self) -> str: - return self._icon - - @icon.setter - def icon(self, value: str) -> None: - _webview_lib.webview_set_icon(self._handle, _encode_c_string(value)) - self._icon = value def destroy(self) -> None: for name in list(self._callbacks.keys()): @@ -237,9 +231,7 @@ class Webview: name, method, ) - c_callback = _webview_lib.CFUNCTYPE( - None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p - )(wrapper) + c_callback = _webview_lib.binding_callback_t(wrapper) if name in self._callbacks: msg = f"Callback {name} already exists. Skipping binding." @@ -261,9 +253,7 @@ class Webview: success = False self.return_(seq.decode(), 0 if success else 1, json.dumps(result)) - c_callback = _webview_lib.CFUNCTYPE( - None, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p - )(wrapper) + c_callback = _webview_lib.binding_callback_t(wrapper) self._callbacks[name] = c_callback _webview_lib.webview_bind( self._handle, _encode_c_string(name), c_callback, None diff --git a/pkgs/clan-app/webview-lib/default.nix b/pkgs/clan-app/webview-lib/default.nix index afe166031..eace975ee 100644 --- a/pkgs/clan-app/webview-lib/default.nix +++ b/pkgs/clan-app/webview-lib/default.nix @@ -8,13 +8,23 @@ pkgs.clangStdenv.mkDerivation { # We disallow remote connections from the UI on Linux # TODO: Disallow remote connections on MacOS - src = pkgs.fetchFromGitHub { - owner = "clan-lol"; + src = pkgs.fetchFromGitea { + domain = "git.clan.lol"; + owner = "clan"; repo = "webview"; - rev = "7d24f0192765b7e08f2d712fae90c046d08f318e"; - hash = "sha256-yokVI9tFiEEU5M/S2xAeJOghqqiCvTelLo8WLKQZsSY="; + rev = "ef481aca8e531f6677258ca911c61aaaf71d2214"; + hash = "sha256-KF9ESpo40z6VXyYsZCLWJAIh0RFe1Zy/Qw4k7cTpoYU="; }; + # @Mic92: Where is this revision coming from? I can't see it in any of the branches. + # I removed the icon python code for now + # src = pkgs.fetchFromGitHub { + # owner = "clan-lol"; + # repo = "webview"; + # rev = "7d24f0192765b7e08f2d712fae90c046d08f318e"; + # hash = "sha256-yokVI9tFiEEU5M/S2xAeJOghqqiCvTelLo8WLKQZsSY="; + # }; + outputs = [ "out" "dev" From 5d99d0e1e776c52dfcd3764ae987821f12315b38 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 16:18:37 +0700 Subject: [PATCH 5/8] clan-app: simplified task function, moved them to a separate file --- pkgs/clan-app/clan_app/app.py | 37 ++++------------------------- pkgs/clan-cli/clan_lib/api/tasks.py | 31 ++++++++++++++++++++---- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/pkgs/clan-app/clan_app/app.py b/pkgs/clan-app/clan_app/app.py index 587b3a9b5..9e5b7dcd9 100644 --- a/pkgs/clan-app/clan_app/app.py +++ b/pkgs/clan-app/clan_app/app.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from pathlib import Path import clan_lib.machines.actions # noqa: F401 -from clan_lib.api import API, ErrorDataClass, SuccessDataClass +from clan_lib.api import API, tasks # TODO: We have to manually import python files to make the API.register be triggered. # We NEED to fix this, as this is super unintuitive and error-prone. @@ -46,45 +46,16 @@ def app_run(app_opts: ClanAppOptions) -> int: webview = Webview(debug=app_opts.debug) webview.title = "Clan App" # This seems to call the gtk api correctly but and gtk also seems to our icon, but somehow the icon is not loaded. - webview.icon = "clan-white" + # Init LogManager global in log_manager_api module log_manager_api.LOG_MANAGER_INSTANCE = LogManager( base_dir=user_data_dir() / "clan-app" / "logs" ) - def cancel_task( - task_id: str, *, op_key: str - ) -> SuccessDataClass[None] | ErrorDataClass: - """Cancel a task by its op_key.""" - log.debug(f"Cancelling task with op_key: {task_id}") - future = webview.threads.get(task_id) - if future: - future.stop_event.set() - log.debug(f"Task {task_id} cancelled.") - else: - log.warning(f"Task {task_id} not found.") - return SuccessDataClass( - op_key=op_key, - data=None, - status="success", - ) + # Init BAKEND_THREADS in tasks module + tasks.BAKEND_THREADS = webview.threads - def list_tasks( - *, - op_key: str, - ) -> SuccessDataClass[list[str]] | ErrorDataClass: - """List all tasks.""" - log.debug("Listing all tasks.") - tasks = list(webview.threads.keys()) - return SuccessDataClass( - op_key=op_key, - data=tasks, - status="success", - ) - - API.overwrite_fn(list_tasks) API.overwrite_fn(open_file) - API.overwrite_fn(cancel_task) webview.bind_jsonschema_api(API, log_manager=log_manager_api.LOG_MANAGER_INSTANCE) webview.size = Size(1280, 1024, SizeHint.NONE) webview.navigate(content_uri) diff --git a/pkgs/clan-cli/clan_lib/api/tasks.py b/pkgs/clan-cli/clan_lib/api/tasks.py index db8ac28cf..49fbbce0a 100644 --- a/pkgs/clan-cli/clan_lib/api/tasks.py +++ b/pkgs/clan-cli/clan_lib/api/tasks.py @@ -1,15 +1,36 @@ +import logging +import threading +from dataclasses import dataclass + from clan_lib.api import API +log = logging.getLogger(__name__) + + +@dataclass +class WebThread: + thread: threading.Thread + stop_event: threading.Event + + +BAKEND_THREADS: dict[str, WebThread] | None = None + @API.register_abstract def cancel_task(task_id: str) -> None: """Cancel a task by its op_key.""" - msg = "cancel_task() is not implemented" - raise NotImplementedError(msg) + assert BAKEND_THREADS is not None, "Backend threads not initialized" + future = BAKEND_THREADS.get(task_id) + if future: + future.stop_event.set() + log.debug(f"Task with id {task_id} has been cancelled.") + else: + msg = f"Task with id {task_id} not found." + raise ValueError(msg) -@API.register_abstract +@API.register def list_tasks() -> list[str]: """List all tasks.""" - msg = "list_tasks() is not implemented" - raise NotImplementedError(msg) + assert BAKEND_THREADS is not None, "Backend threads not initialized" + return list(BAKEND_THREADS.keys()) From db3e8b99844425bbaabfc22714bf613cc0807b25 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 17:59:05 +0700 Subject: [PATCH 6/8] clan-app: Add logging middleware --- .../clan_app/deps/webview/_webview_ffi.py | 4 +- .../clan-app/clan_app/deps/webview/webview.py | 17 +++--- pkgs/clan-app/ui-2d/src/api/index.tsx | 32 +++++----- .../ui-2d/src/components/RemoteForm.tsx | 6 +- .../components/machine-list-item/index.tsx | 55 ++++++++++------- .../machines/components/MachineForm.tsx | 59 ++++++++++++------- .../src/routes/machines/install/vars-step.tsx | 20 +++++-- pkgs/clan-app/ui/src/api/index.tsx | 39 ++++++------ .../clan-cli/clan_lib/log_manager/__init__.py | 5 +- 9 files changed, 144 insertions(+), 93 deletions(-) diff --git a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py index d719aa15e..051e1e762 100644 --- a/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py +++ b/pkgs/clan-app/clan_app/deps/webview/_webview_ffi.py @@ -109,9 +109,7 @@ class _WebviewLibrary: self.webview_return = self.lib.webview_return self.webview_return.argtypes = [c_void_p, c_char_p, c_int, c_char_p] - self.binding_callback_t = CFUNCTYPE( - None, c_char_p, c_char_p, c_void_p - ) + self.binding_callback_t = CFUNCTYPE(None, c_char_p, c_char_p, c_void_p) self.CFUNCTYPE = CFUNCTYPE diff --git a/pkgs/clan-app/clan_app/deps/webview/webview.py b/pkgs/clan-app/clan_app/deps/webview/webview.py index ad24fcfd7..f5cd80fa9 100644 --- a/pkgs/clan-app/clan_app/deps/webview/webview.py +++ b/pkgs/clan-app/clan_app/deps/webview/webview.py @@ -66,13 +66,14 @@ class Webview: ) -> None: op_key = op_key_bytes.decode() args = json.loads(request_data.decode()) - log.debug(f"Calling {method_name}({args[0]})") - metadata: dict[str, Any] = {} + log.debug(f"Calling {method_name}({args})") + header: dict[str, Any] try: # Initialize dataclasses from the payload reconciled_arguments = {} - if len(args) > 0: + if len(args) > 1: + header = args[1] for k, v in args[0].items(): # Some functions expect to be called with dataclass instances # But the js api returns dictionaries. @@ -83,6 +84,8 @@ class Webview: # TODO: rename from_dict into something like construct_checked_value # from_dict really takes Anything and returns an instance of the type/class reconciled_arguments[k] = from_dict(arg_class, v) + elif len(args) == 1: + header = args[0] reconciled_arguments["op_key"] = op_key except Exception as e: @@ -109,12 +112,12 @@ class Webview: ctx.should_cancel = lambda: stop_event.is_set() # If the API call has set log_group in metadata, # create the log file under that group. - log_group = metadata.get("logging", {}).get("group", None) + log_group = header.get("logging", {}).get("group", None) if log_group is not None: log.warning( f"Using log group {log_group} for {method_name} with op_key {op_key}" ) - breakpoint() + log_file = log_manager.create_log_file( wrap_method, op_key=op_key, group=log_group ).get_file_path() @@ -136,10 +139,11 @@ class Webview: try: # Original logic: call the wrapped API method. result = wrap_method(**reconciled_arguments) + wrapped_result = {"body": dataclass_to_dict(result), "header": {}} # Serialize the result to JSON. serialized = json.dumps( - dataclass_to_dict(result), indent=4, ensure_ascii=False + dataclass_to_dict(wrapped_result), indent=4, ensure_ascii=False ) # This log message will now also be written to log_f @@ -206,7 +210,6 @@ class Webview: _webview_lib.webview_set_title(self._handle, _encode_c_string(value)) self._title = value - def destroy(self) -> None: for name in list(self._callbacks.keys()): self.unbind(name) diff --git a/pkgs/clan-app/ui-2d/src/api/index.tsx b/pkgs/clan-app/ui-2d/src/api/index.tsx index 73d9b8612..5c72f2edd 100644 --- a/pkgs/clan-app/ui-2d/src/api/index.tsx +++ b/pkgs/clan-app/ui-2d/src/api/index.tsx @@ -46,21 +46,22 @@ interface BackendOpts { } interface BackendReturnType { - result: OperationResponse; - metadata: Record; + body: OperationResponse; + header: Record; } const _callApi = ( method: K, args: OperationArgs, backendOpts?: BackendOpts, -): { promise: Promise>; op_key: string } => { +): { promise: Promise>; op_key: string } => { // if window[method] does not exist, throw an error if (!(method in window)) { console.error(`Method ${method} not found on window object`); // return a rejected promise return { promise: Promise.resolve({ + body: { status: "error", errors: [ { @@ -69,17 +70,19 @@ const _callApi = ( }, ], op_key: "noop", + }, + header: {}, }), op_key: "noop", }; } - let metadata: BackendOpts | undefined = undefined; + let header: BackendOpts = {}; if (backendOpts != undefined) { - metadata = { ...backendOpts }; + header = { ...backendOpts }; let group = backendOpts?.logging?.group; if (group != undefined && isMachine(group)) { - metadata = { + header = { logging: { group: group.flake.identifier + "#" + group.name }, }; } @@ -90,9 +93,10 @@ const _callApi = ( OperationNames, ( args: OperationArgs, - ) => Promise> + metadata: BackendOpts, + ) => Promise> > - )[method](args) as Promise>; + )[method](args, header) as Promise>; // eslint-disable-next-line @typescript-eslint/no-explicit-any const op_key = (promise as any)._webviewMessageId as string; @@ -102,7 +106,7 @@ const _callApi = ( const handleCancel = async ( ops_key: string, - orig_task: Promise>, + orig_task: Promise>, ) => { console.log("Canceling operation: ", ops_key); const { promise, op_key } = _callApi("cancel_task", { task_id: ops_key }); @@ -122,7 +126,7 @@ const handleCancel = async ( }); const resp = await promise; - if (resp.status === "error") { + if (resp.body.status === "error") { toast.custom( (t) => ( ( console.log("Not printing toast because operation was cancelled"); } - const result = response; - if (result.status === "error" && !cancelled) { + const body = response.body; + if (body.status === "error" && !cancelled) { toast.remove(toastId); toast.custom( (t) => ( ), { @@ -203,7 +207,7 @@ export const callApi = ( } else { toast.remove(toastId); } - return result; + return body; }); return { promise: new_promise, op_key: op_key }; diff --git a/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx b/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx index fb54b1a69..6e4517798 100644 --- a/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx +++ b/pkgs/clan-app/ui-2d/src/components/RemoteForm.tsx @@ -207,7 +207,11 @@ export function RemoteForm(props: RemoteFormProps) { flake: props.machine.flake, field: props.field || "targetHost", }, - {logging: { group: { name: props.machine.name, flake: props.machine.flake } },}, + { + logging: { + group: { name: props.machine.name, flake: props.machine.flake }, + }, + }, ).promise; if (result.status === "error") diff --git a/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx b/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx index 221ff81a6..78f0312a4 100644 --- a/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx +++ b/pkgs/clan-app/ui-2d/src/components/machine-list-item/index.tsx @@ -53,7 +53,8 @@ export const MachineListItem = (props: MachineListItemProps) => { field: "targetHost", flake: { identifier: active_clan }, name: name, - }, {logging: { group: { name, flake: { identifier: active_clan } } }} + }, + { logging: { group: { name, flake: { identifier: active_clan } } } }, ).promise; if (target_host.status == "error") { @@ -106,13 +107,17 @@ export const MachineListItem = (props: MachineListItemProps) => { } setUpdating(true); - const target_host = await callApi("get_host", { - field: "targetHost", - flake: { identifier: active_clan }, - name: name, - }, { - logging: { group: { name, flake: { identifier: active_clan } } }, - }).promise; + const target_host = await callApi( + "get_host", + { + field: "targetHost", + flake: { identifier: active_clan }, + name: name, + }, + { + logging: { group: { name, flake: { identifier: active_clan } } }, + }, + ).promise; if (target_host.status == "error") { console.error("No target host found for the machine"); @@ -129,11 +134,15 @@ export const MachineListItem = (props: MachineListItemProps) => { return; } - const build_host = await callApi("get_host", { - field: "buildHost", - flake: { identifier: active_clan }, - name: name, - }, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise; + const build_host = await callApi( + "get_host", + { + field: "buildHost", + flake: { identifier: active_clan }, + name: name, + }, + { logging: { group: { name, flake: { identifier: active_clan } } } }, + ).promise; if (build_host.status == "error") { console.error("No target host found for the machine"); @@ -145,16 +154,20 @@ export const MachineListItem = (props: MachineListItemProps) => { return; } - await callApi("deploy_machine", { - machine: { - name: name, - flake: { - identifier: active_clan, + await callApi( + "deploy_machine", + { + machine: { + name: name, + flake: { + identifier: active_clan, + }, }, + target_host: target_host.data!.data, + build_host: build_host.data?.data || null, }, - target_host: target_host.data!.data, - build_host: build_host.data?.data || null, - }, {logging: { group: { name, flake: { identifier: active_clan } } }}).promise; + { logging: { group: { name, flake: { identifier: active_clan } } } }, + ).promise; setUpdating(false); }; diff --git a/pkgs/clan-app/ui-2d/src/routes/machines/components/MachineForm.tsx b/pkgs/clan-app/ui-2d/src/routes/machines/components/MachineForm.tsx index 4f6059d4e..0ab716aa6 100644 --- a/pkgs/clan-app/ui-2d/src/routes/machines/components/MachineForm.tsx +++ b/pkgs/clan-app/ui-2d/src/routes/machines/components/MachineForm.tsx @@ -55,7 +55,7 @@ export function MachineForm(props: MachineFormProps) { ...values.machine, tags: Array.from(values.machine.tags || detailed.machine.tags || []), }, - } ).promise; + }).promise; await queryClient.invalidateQueries({ queryKey: [ @@ -77,10 +77,18 @@ export function MachineForm(props: MachineFormProps) { if (!machine_name || !base_dir) { return []; } - const result = await callApi("get_generators_closure", { - base_dir: base_dir, - machine_name: machine_name, - }, {logging: {group: { name: machine_name, flake: {identifier: base_dir} }}}).promise; + const result = await callApi( + "get_generators_closure", + { + base_dir: base_dir, + machine_name: machine_name, + }, + { + logging: { + group: { name: machine_name, flake: { identifier: base_dir } }, + }, + }, + ).promise; if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; }, @@ -112,13 +120,18 @@ export function MachineForm(props: MachineFormProps) { return; } - const target = await callApi("get_host", { - field: "targetHost", - name: machine, - flake: { - identifier: curr_uri, + const target = await callApi( + "get_host", + { + field: "targetHost", + name: machine, + flake: { + identifier: curr_uri, + }, + }, + { + logging: { group: { name: machine, flake: { identifier: curr_uri } } }, }, - }, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }} ).promise; if (target.status === "error") { @@ -133,18 +146,24 @@ export function MachineForm(props: MachineFormProps) { const target_host = target.data.data; setIsUpdating(true); - const r = await callApi("deploy_machine", { - machine: { - name: machine, - flake: { - identifier: curr_uri, + const r = await callApi( + "deploy_machine", + { + machine: { + name: machine, + flake: { + identifier: curr_uri, + }, }, + target_host: { + ...target_host, + }, + build_host: null, }, - target_host: { - ...target_host, + { + logging: { group: { name: machine, flake: { identifier: curr_uri } } }, }, - build_host: null, - }, {logging: { group: { name: machine, flake: { identifier: curr_uri } } }}).promise.finally(() => { + ).promise.finally(() => { setIsUpdating(false); }); }; diff --git a/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx b/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx index 0b64af9e0..ddc136f59 100644 --- a/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx +++ b/pkgs/clan-app/ui-2d/src/routes/machines/install/vars-step.tsx @@ -149,14 +149,22 @@ export const VarsStep = (props: VarsStepProps) => { const generatorsQuery = createQuery(() => ({ queryKey: [props.dir, props.machine_id, "generators", props.fullClosure], queryFn: async () => { - const result = await callApi("get_generators_closure", { - base_dir: props.dir, - machine_name: props.machine_id, - full_closure: props.fullClosure, - }, {logging: {group: { name: props.machine_id, flake: {identifier: props.dir} }}}).promise; + const result = await callApi( + "get_generators_closure", + { + base_dir: props.dir, + machine_name: props.machine_id, + full_closure: props.fullClosure, + }, + { + logging: { + group: { name: props.machine_id, flake: { identifier: props.dir } }, + }, + }, + ).promise; if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; - }, + }, })); const handleSubmit: SubmitHandler = async (values, event) => { diff --git a/pkgs/clan-app/ui/src/api/index.tsx b/pkgs/clan-app/ui/src/api/index.tsx index bbc40bd16..5c72f2edd 100644 --- a/pkgs/clan-app/ui/src/api/index.tsx +++ b/pkgs/clan-app/ui/src/api/index.tsx @@ -27,9 +27,9 @@ function isMachine(obj: unknown): obj is Machine { return ( !!obj && typeof obj === "object" && - typeof (obj as Machine).name === "string" && - typeof (obj as Machine).flake === "object" && - typeof (obj as Machine).flake.identifier === "string" + typeof (obj as any).name === "string" && + typeof (obj as any).flake === "object" && + typeof (obj as any).flake.identifier === "string" ); } @@ -46,9 +46,8 @@ interface BackendOpts { } interface BackendReturnType { - result: OperationResponse; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - metadata: Record; + body: OperationResponse; + header: Record; } const _callApi = ( @@ -62,7 +61,7 @@ const _callApi = ( // return a rejected promise return { promise: Promise.resolve({ - result: { + body: { status: "error", errors: [ { @@ -72,18 +71,18 @@ const _callApi = ( ], op_key: "noop", }, - metadata: {}, + header: {}, }), op_key: "noop", }; } - let metadata: BackendOpts | undefined = undefined; + let header: BackendOpts = {}; if (backendOpts != undefined) { - metadata = { ...backendOpts }; - const group = backendOpts?.logging?.group; + header = { ...backendOpts }; + let group = backendOpts?.logging?.group; if (group != undefined && isMachine(group)) { - metadata = { + header = { logging: { group: group.flake.identifier + "#" + group.name }, }; } @@ -94,10 +93,10 @@ const _callApi = ( OperationNames, ( args: OperationArgs, - metadata?: BackendOpts, + metadata: BackendOpts, ) => Promise> > - )[method](args, metadata) as Promise>; + )[method](args, header) as Promise>; // eslint-disable-next-line @typescript-eslint/no-explicit-any const op_key = (promise as any)._webviewMessageId as string; @@ -127,7 +126,7 @@ const handleCancel = async ( }); const resp = await promise; - if (resp.result.status === "error") { + if (resp.body.status === "error") { toast.custom( (t) => ( ( args: OperationArgs, backendOpts?: BackendOpts, ): { promise: Promise>; op_key: string } => { - console.log("Calling API", method, args); + console.log("Calling API", method, args, backendOpts); const { promise, op_key } = _callApi(method, args, backendOpts); promise.catch((error) => { @@ -191,14 +190,14 @@ export const callApi = ( console.log("Not printing toast because operation was cancelled"); } - const result = response.result; - if (result.status === "error" && !cancelled) { + const body = response.body; + if (body.status === "error" && !cancelled) { toast.remove(toastId); toast.custom( (t) => ( ), { @@ -208,7 +207,7 @@ export const callApi = ( } else { toast.remove(toastId); } - return result; + return body; }); return { promise: new_promise, op_key: op_key }; diff --git a/pkgs/clan-cli/clan_lib/log_manager/__init__.py b/pkgs/clan-cli/clan_lib/log_manager/__init__.py index fcb7a9f9a..9f85b990e 100644 --- a/pkgs/clan-cli/clan_lib/log_manager/__init__.py +++ b/pkgs/clan-cli/clan_lib/log_manager/__init__.py @@ -310,10 +310,13 @@ class LogManager: base_dir: Path def create_log_file( - self, func: Callable, op_key: str, group: str = "default" + self, func: Callable, op_key: str, group: str | None = None ) -> LogFile: now_utc = datetime.datetime.now(tz=datetime.UTC) + if group is None: + group = "default" + log_file = LogFile( op_key=op_key, date_day=now_utc.strftime("%Y-%m-%d"), From b9a386c881f5fa65540fa8b119383e2e4c0369cf Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 17:59:24 +0700 Subject: [PATCH 7/8] clan-cli: api.py add python header --- pkgs/clan-cli/api.py | 2 ++ 1 file changed, 2 insertions(+) mode change 100644 => 100755 pkgs/clan-cli/api.py diff --git a/pkgs/clan-cli/api.py b/pkgs/clan-cli/api.py old mode 100644 new mode 100755 index 033f8360d..94f36b6bd --- a/pkgs/clan-cli/api.py +++ b/pkgs/clan-cli/api.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import importlib import json import pkgutil From 2fd6426f28120402d627574d8086b462b49ecf4a Mon Sep 17 00:00:00 2001 From: Qubasa Date: Wed, 2 Jul 2025 18:11:28 +0700 Subject: [PATCH 8/8] clan-app: whitelist necessary any usage in api./index.tsx --- pkgs/clan-app/ui-2d/src/api/index.tsx | 7 ++++++- pkgs/clan-app/ui/src/api/index.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-app/ui-2d/src/api/index.tsx b/pkgs/clan-app/ui-2d/src/api/index.tsx index 5c72f2edd..f75bc5942 100644 --- a/pkgs/clan-app/ui-2d/src/api/index.tsx +++ b/pkgs/clan-app/ui-2d/src/api/index.tsx @@ -27,8 +27,11 @@ function isMachine(obj: unknown): obj is Machine { return ( !!obj && typeof obj === "object" && + // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (obj as any).name === "string" && + // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (obj as any).flake === "object" && + // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (obj as any).flake.identifier === "string" ); } @@ -47,6 +50,8 @@ interface BackendOpts { interface BackendReturnType { body: OperationResponse; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any header: Record; } @@ -80,7 +85,7 @@ const _callApi = ( let header: BackendOpts = {}; if (backendOpts != undefined) { header = { ...backendOpts }; - let group = backendOpts?.logging?.group; + const group = backendOpts?.logging?.group; if (group != undefined && isMachine(group)) { header = { logging: { group: group.flake.identifier + "#" + group.name }, diff --git a/pkgs/clan-app/ui/src/api/index.tsx b/pkgs/clan-app/ui/src/api/index.tsx index 5c72f2edd..f75bc5942 100644 --- a/pkgs/clan-app/ui/src/api/index.tsx +++ b/pkgs/clan-app/ui/src/api/index.tsx @@ -27,8 +27,11 @@ function isMachine(obj: unknown): obj is Machine { return ( !!obj && typeof obj === "object" && + // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (obj as any).name === "string" && + // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (obj as any).flake === "object" && + // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof (obj as any).flake.identifier === "string" ); } @@ -47,6 +50,8 @@ interface BackendOpts { interface BackendReturnType { body: OperationResponse; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any header: Record; } @@ -80,7 +85,7 @@ const _callApi = ( let header: BackendOpts = {}; if (backendOpts != undefined) { header = { ...backendOpts }; - let group = backendOpts?.logging?.group; + const group = backendOpts?.logging?.group; if (group != undefined && isMachine(group)) { header = { logging: { group: group.flake.identifier + "#" + group.name },