clan-app: Implement dynamic log groups into javascript callApi

nix fmt
This commit is contained in:
Qubasa
2025-07-04 17:46:29 +07:00
parent aef1edf8e3
commit 52aaad272f
11 changed files with 111 additions and 112 deletions

View File

@@ -11,7 +11,7 @@ import clan_lib.machines.actions # noqa: F401
from clan_lib.api import API, load_in_all_api_functions, tasks from clan_lib.api import API, load_in_all_api_functions, tasks
from clan_lib.custom_logger import setup_logging from clan_lib.custom_logger import setup_logging
from clan_lib.dirs import user_data_dir from clan_lib.dirs import user_data_dir
from clan_lib.log_manager import LogManager from clan_lib.log_manager import LogGroupConfig, LogManager
from clan_lib.log_manager import api as log_manager_api from clan_lib.log_manager import api as log_manager_api
from clan_app.api.file_gtk import open_file from clan_app.api.file_gtk import open_file
@@ -41,12 +41,15 @@ def app_run(app_opts: ClanAppOptions) -> int:
webview = Webview(debug=app_opts.debug) webview = Webview(debug=app_opts.debug)
webview.title = "Clan App" 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.
# Init LogManager global in log_manager_api module # Add a log group ["clans", <dynamic_name>, "machines", <dynamic_name>]
log_manager_api.LOG_MANAGER_INSTANCE = LogManager( log_manager = LogManager(base_dir=user_data_dir() / "clan-app" / "logs")
base_dir=user_data_dir() / "clan-app" / "logs" clan_log_group = LogGroupConfig("clans", "Clans").add_child(
LogGroupConfig("machines", "Machines")
) )
log_manager = log_manager.add_root_group_config(clan_log_group)
# Init LogManager global in log_manager_api module
log_manager_api.LOG_MANAGER_INSTANCE = log_manager
# Init BAKEND_THREADS global in tasks module # Init BAKEND_THREADS global in tasks module
tasks.BAKEND_THREADS = webview.threads tasks.BAKEND_THREADS = webview.threads

View File

@@ -1,3 +1,4 @@
# ruff: noqa: TRY301
import functools import functools
import io import io
import json import json
@@ -66,15 +67,24 @@ class Webview:
) -> None: ) -> None:
op_key = op_key_bytes.decode() op_key = op_key_bytes.decode()
args = json.loads(request_data.decode()) args = json.loads(request_data.decode())
log.debug(f"Calling {method_name}({args})") log.debug(f"Calling {method_name}({json.dumps(args, indent=4)})")
header: dict[str, Any] header: dict[str, Any]
try: try:
# Initialize dataclasses from the payload # Initialize dataclasses from the payload
reconciled_arguments = {} reconciled_arguments = {}
if len(args) > 1: if len(args) == 1:
header = args[1] request = args[0]
for k, v in args[0].items(): header = request.get("header", {})
msg = f"Expected header to be a dict, got {type(header)}"
if not isinstance(header, dict):
raise TypeError(msg)
body = request.get("body", {})
msg = f"Expected body to be a dict, got {type(body)}"
if not isinstance(body, dict):
raise TypeError(msg)
for k, v in body.items():
# Some functions expect to be called with dataclass instances # Some functions expect to be called with dataclass instances
# But the js api returns dictionaries. # But the js api returns dictionaries.
# Introspect the function and create the expected dataclass from dict dynamically # Introspect the function and create the expected dataclass from dict dynamically
@@ -84,8 +94,11 @@ class Webview:
# TODO: rename from_dict into something like construct_checked_value # TODO: rename from_dict into something like construct_checked_value
# from_dict really takes Anything and returns an instance of the type/class # from_dict really takes Anything and returns an instance of the type/class
reconciled_arguments[k] = from_dict(arg_class, v) reconciled_arguments[k] = from_dict(arg_class, v)
elif len(args) == 1: elif len(args) > 1:
header = args[0] msg = (
"Expected a single argument, got multiple arguments to api_wrapper"
)
raise ValueError(msg)
reconciled_arguments["op_key"] = op_key reconciled_arguments["op_key"] = op_key
except Exception as e: except Exception as e:
@@ -114,11 +127,11 @@ class Webview:
try: try:
# If the API call has set log_group in metadata, # If the API call has set log_group in metadata,
# create the log file under that group. # create the log file under that group.
log_group: list[str] = header.get("logging", {}).get("group", None) log_group: list[str] = header.get("logging", {}).get("group_path", None)
if log_group is not None: if log_group is not None:
if not isinstance(log_group, list): if not isinstance(log_group, list):
msg = f"Expected log_group to be a list, got {type(log_group)}" msg = f"Expected log_group to be a list, got {type(log_group)}"
raise TypeError(msg) # noqa: TRY301 raise TypeError(msg)
log.warning( log.warning(
f"Using log group {log_group} for {method_name} with op_key {op_key}" f"Using log group {log_group} for {method_name} with op_key {op_key}"
) )

View File

@@ -23,42 +23,25 @@ export type SuccessQuery<T extends OperationNames> = Extract<
>; >;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"]; export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
function isMachine(obj: unknown): obj is Machine { interface SendHeaderType {
return ( logging?: { group_path: string[] };
!!obj && }
typeof obj === "object" && interface BackendSendType<K extends OperationNames> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any body: OperationArgs<K>;
typeof (obj as any).name === "string" && header?: SendHeaderType;
// 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"
);
}
// Machine type with flake for API calls
interface Machine {
name: string;
flake: {
identifier: string;
};
}
interface BackendOpts {
logging?: { group: string | Machine };
} }
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ReceiveHeaderType {}
interface BackendReturnType<K extends OperationNames> { interface BackendReturnType<K extends OperationNames> {
body: OperationResponse<K>; body: OperationResponse<K>;
header: ReceiveHeaderType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
header: Record<string, any>;
} }
const _callApi = <K extends OperationNames>( const _callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts, backendOpts?: SendHeaderType,
): { promise: Promise<BackendReturnType<K>>; op_key: string } => { ): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
// if window[method] does not exist, throw an error // if window[method] does not exist, throw an error
if (!(method in window)) { if (!(method in window)) {
@@ -82,26 +65,19 @@ const _callApi = <K extends OperationNames>(
}; };
} }
let header: BackendOpts = {}; const message: BackendSendType<OperationNames> = {
if (backendOpts != undefined) { body: args,
header = { ...backendOpts }; header: backendOpts,
const group = backendOpts?.logging?.group; };
if (group != undefined && isMachine(group)) {
header = {
logging: { group: group.flake.identifier + "#" + group.name },
};
}
}
const promise = ( const promise = (
window as unknown as Record< window as unknown as Record<
OperationNames, OperationNames,
( (
args: OperationArgs<OperationNames>, args: BackendSendType<OperationNames>,
metadata: BackendOpts,
) => Promise<BackendReturnType<OperationNames>> ) => Promise<BackendReturnType<OperationNames>>
> >
)[method](args, header) as Promise<BackendReturnType<K>>; )[method](message) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (promise as any)._webviewMessageId as string; const op_key = (promise as any)._webviewMessageId as string;
@@ -153,7 +129,7 @@ const handleCancel = async <K extends OperationNames>(
export const callApi = <K extends OperationNames>( export const callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts, backendOpts?: SendHeaderType,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<OperationResponse<K>>; op_key: string } => {
console.log("Calling API", method, args, backendOpts); console.log("Calling API", method, args, backendOpts);

View File

@@ -186,6 +186,7 @@ export function RemoteForm(props: RemoteFormProps) {
props.queryFn, props.queryFn,
props.machine?.name, props.machine?.name,
props.machine?.flake, props.machine?.flake,
props.machine?.flake.identifier,
props.field || "targetHost", props.field || "targetHost",
], ],
queryFn: async () => { queryFn: async () => {
@@ -209,7 +210,12 @@ export function RemoteForm(props: RemoteFormProps) {
}, },
{ {
logging: { logging: {
group: { name: props.machine.name, flake: props.machine.flake }, group_path: [
"clans",
props.machine.flake.identifier,
"machines",
props.machine.name,
],
}, },
}, },
).promise; ).promise;

View File

@@ -54,7 +54,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}, },
{ logging: { group: { name, flake: { identifier: active_clan } } } }, {
logging: { group_path: ["clans", active_clan, "machines", name] },
},
).promise; ).promise;
if (target_host.status == "error") { if (target_host.status == "error") {
@@ -115,7 +117,9 @@ export const MachineListItem = (props: MachineListItemProps) => {
name: name, name: name,
}, },
{ {
logging: { group: { name, flake: { identifier: active_clan } } }, logging: {
group_path: ["clans", active_clan, "machines", name],
},
}, },
).promise; ).promise;
@@ -141,7 +145,11 @@ export const MachineListItem = (props: MachineListItemProps) => {
flake: { identifier: active_clan }, flake: { identifier: active_clan },
name: name, name: name,
}, },
{ logging: { group: { name, flake: { identifier: active_clan } } } }, {
logging: {
group_path: ["clans", active_clan, "machines", name],
},
},
).promise; ).promise;
if (build_host.status == "error") { if (build_host.status == "error") {
@@ -166,7 +174,11 @@ export const MachineListItem = (props: MachineListItemProps) => {
target_host: target_host.data!.data, target_host: target_host.data!.data,
build_host: build_host.data?.data || null, build_host: build_host.data?.data || null,
}, },
{ logging: { group: { name, flake: { identifier: active_clan } } } }, {
logging: {
group_path: ["clans", active_clan, "machines", name],
},
},
).promise; ).promise;
setUpdating(false); setUpdating(false);

View File

@@ -85,7 +85,7 @@ export function MachineForm(props: MachineFormProps) {
}, },
{ {
logging: { logging: {
group: { name: machine_name, flake: { identifier: base_dir } }, group_path: ["clans", base_dir, "machines", machine_name],
}, },
}, },
).promise; ).promise;
@@ -130,7 +130,9 @@ export function MachineForm(props: MachineFormProps) {
}, },
}, },
{ {
logging: { group: { name: machine, flake: { identifier: curr_uri } } }, logging: {
group_path: ["clans", curr_uri, "machines", machine],
},
}, },
).promise; ).promise;
@@ -161,7 +163,9 @@ export function MachineForm(props: MachineFormProps) {
build_host: null, build_host: null,
}, },
{ {
logging: { group: { name: machine, flake: { identifier: curr_uri } } }, logging: {
group_path: ["clans", curr_uri, "machines", machine],
},
}, },
).promise.finally(() => { ).promise.finally(() => {
setIsUpdating(false); setIsUpdating(false);

View File

@@ -158,7 +158,7 @@ export const VarsStep = (props: VarsStepProps) => {
}, },
{ {
logging: { logging: {
group: { name: props.machine_id, flake: { identifier: props.dir } }, group_path: ["clans", props.dir, "machines", props.machine_id],
}, },
}, },
).promise; ).promise;

View File

@@ -16,14 +16,22 @@ export const MachineInstall = () => {
queryFn: async () => { queryFn: async () => {
const curr = activeClanURI(); const curr = activeClanURI();
if (curr) { if (curr) {
const result = await callApi("get_machine_details", { const result = await callApi(
machine: { "get_machine_details",
flake: { {
identifier: curr, machine: {
flake: {
identifier: curr,
},
name: params.id,
}, },
name: params.id,
}, },
}).promise; {
logging: {
group_path: ["clans", curr, "machines", params.id],
},
},
).promise;
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
} }

View File

@@ -8,7 +8,6 @@ import Icon from "@/src/components/icon";
import { Header } from "@/src/layout/header"; import { Header } from "@/src/layout/header";
import { makePersisted } from "@solid-primitives/storage"; import { makePersisted } from "@solid-primitives/storage";
import { useClanContext } from "@/src/contexts/clan"; import { useClanContext } from "@/src/contexts/clan";
import { debug } from "console";
type MachinesModel = Extract< type MachinesModel = Extract<
OperationResponse<"list_machines">, OperationResponse<"list_machines">,

View File

@@ -23,42 +23,25 @@ export type SuccessQuery<T extends OperationNames> = Extract<
>; >;
export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"]; export type SuccessData<T extends OperationNames> = SuccessQuery<T>["data"];
function isMachine(obj: unknown): obj is Machine { interface SendHeaderType {
return ( logging?: { group_path: string[] };
!!obj && }
typeof obj === "object" && interface BackendSendType<K extends OperationNames> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any body: OperationArgs<K>;
typeof (obj as any).name === "string" && header?: SendHeaderType;
// 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"
);
}
// Machine type with flake for API calls
interface Machine {
name: string;
flake: {
identifier: string;
};
}
interface BackendOpts {
logging?: { group: string | Machine };
} }
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ReceiveHeaderType {}
interface BackendReturnType<K extends OperationNames> { interface BackendReturnType<K extends OperationNames> {
body: OperationResponse<K>; body: OperationResponse<K>;
header: ReceiveHeaderType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
header: Record<string, any>;
} }
const _callApi = <K extends OperationNames>( const _callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts, backendOpts?: SendHeaderType,
): { promise: Promise<BackendReturnType<K>>; op_key: string } => { ): { promise: Promise<BackendReturnType<K>>; op_key: string } => {
// if window[method] does not exist, throw an error // if window[method] does not exist, throw an error
if (!(method in window)) { if (!(method in window)) {
@@ -82,26 +65,19 @@ const _callApi = <K extends OperationNames>(
}; };
} }
let header: BackendOpts = {}; const message: BackendSendType<OperationNames> = {
if (backendOpts != undefined) { body: args,
header = { ...backendOpts }; header: backendOpts,
const group = backendOpts?.logging?.group; };
if (group != undefined && isMachine(group)) {
header = {
logging: { group: group.flake.identifier + "#" + group.name },
};
}
}
const promise = ( const promise = (
window as unknown as Record< window as unknown as Record<
OperationNames, OperationNames,
( (
args: OperationArgs<OperationNames>, args: BackendSendType<OperationNames>,
metadata: BackendOpts,
) => Promise<BackendReturnType<OperationNames>> ) => Promise<BackendReturnType<OperationNames>>
> >
)[method](args, header) as Promise<BackendReturnType<K>>; )[method](message) as Promise<BackendReturnType<K>>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const op_key = (promise as any)._webviewMessageId as string; const op_key = (promise as any)._webviewMessageId as string;
@@ -153,7 +129,7 @@ const handleCancel = async <K extends OperationNames>(
export const callApi = <K extends OperationNames>( export const callApi = <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
backendOpts?: BackendOpts, backendOpts?: SendHeaderType,
): { promise: Promise<OperationResponse<K>>; op_key: string } => { ): { promise: Promise<OperationResponse<K>>; op_key: string } => {
console.log("Calling API", method, args, backendOpts); console.log("Calling API", method, args, backendOpts);

View File

@@ -20,7 +20,9 @@ def list_log_days() -> list[str]:
@API.register @API.register
def list_log_groups(selector: list[str], date_day: str | None = None) -> list[str]: def list_log_groups(
selector: list[str] | None, date_day: str | None = None
) -> list[str]:
"""List all log groups at the specified hierarchical path. """List all log groups at the specified hierarchical path.
Args: Args: