Merge pull request 'api: init notification queue' (#4678) from ui-notify into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4678
This commit is contained in:
hsjobeki
2025-08-11 15:13:42 +00:00
13 changed files with 270 additions and 54 deletions

View File

@@ -1,12 +1,14 @@
import functools import functools
import json import json
import logging import logging
import threading
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import IntEnum from enum import IntEnum
from time import sleep
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from clan_lib.api import MethodRegistry from clan_lib.api import MethodRegistry, message_queue
from clan_lib.api.tasks import WebThread from clan_lib.api.tasks import WebThread
from clan_lib.log_manager import LogManager from clan_lib.log_manager import LogManager
@@ -69,6 +71,22 @@ class Webview:
if self.size: if self.size:
self.set_size(self.size) self.set_size(self.size)
def __post_init__(self) -> None:
self.setup_notify() # Start the notification loop
def setup_notify(self) -> None:
def loop() -> None:
while True:
try:
msg = message_queue.get() # Blocks until available
js_code = f"window.notifyBus({json.dumps(msg)});"
self.eval(js_code)
except Exception as e:
print("Bridge notify error:", e)
sleep(0.01) # avoid busy loop
threading.Thread(target=loop, daemon=True).start()
@property @property
def handle(self) -> Any: def handle(self) -> Any:
"""Get the webview handle, creating it if necessary.""" """Get the webview handle, creating it if necessary."""
@@ -129,6 +147,7 @@ class Webview:
webview=self, middleware_chain=tuple(self._middleware), threads={} webview=self, middleware_chain=tuple(self._middleware), threads={}
) )
self._bridge = bridge self._bridge = bridge
return bridge return bridge
# Legacy methods for compatibility # Legacy methods for compatibility

View File

@@ -25,7 +25,6 @@ class WebviewBridge(ApiBridge):
def send_api_response(self, response: BackendResponse) -> None: def send_api_response(self, response: BackendResponse) -> None:
"""Send response back to the webview client.""" """Send response back to the webview client."""
serialized = json.dumps( serialized = json.dumps(
dataclass_to_dict(response), indent=4, ensure_ascii=False dataclass_to_dict(response), indent=4, ensure_ascii=False
) )

9
pkgs/clan-app/ui/index.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import { ProcessMessage } from "./src/hooks/notify";
export {};
declare global {
interface Window {
notifyBus: (data: ProcessMessage) => void;
}
}

View File

@@ -1,7 +1,9 @@
.modal_content { .modal_content {
@apply min-w-[320px] max-w-[512px]; @apply min-w-[320px] flex flex-col;
@apply rounded-md overflow-hidden; @apply rounded-md overflow-hidden;
max-height: calc(100vh - 2rem);
/* todo replace with a theme() color */ /* todo replace with a theme() color */
box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32); box-shadow: 0.1875rem 0.1875rem 0 0 rgba(145, 172, 175, 0.32);
@apply border border-def-3 rounded-bl-md rounded-br-md; @apply border border-def-3 rounded-bl-md rounded-br-md;
@@ -22,7 +24,8 @@
} }
.modal_body { .modal_body {
@apply rounded-b-md p-6 pt-4 bg-def-1; overflow-y: auto;
@apply rounded-b-md p-6 pt-4 bg-def-1 flex-grow;
&[data-no-padding] { &[data-no-padding] {
@apply p-0; @apply p-0;

View File

@@ -72,7 +72,7 @@ export const callApi = <K extends OperationNames>(
const op_key = backendOpts?.op_key ?? crypto.randomUUID(); const op_key = backendOpts?.op_key ?? crypto.randomUUID();
const req: BackendSendType<OperationNames> = { const req: BackendSendType<K> = {
body: args, body: args,
header: { header: {
...backendOpts, ...backendOpts,
@@ -83,11 +83,9 @@ export const callApi = <K extends OperationNames>(
const result = ( const result = (
window as unknown as Record< window as unknown as Record<
OperationNames, OperationNames,
( (args: BackendSendType<K>) => Promise<BackendReturnType<K>>
args: BackendSendType<OperationNames>,
) => Promise<BackendReturnType<OperationNames>>
> >
)[method](req) as Promise<BackendReturnType<K>>; )[method](req);
return { return {
uuid: op_key, uuid: op_key,

View File

@@ -0,0 +1,89 @@
import { createSignal, onCleanup } from "solid-js";
import { OperationNames } from "./api";
export interface ProcessMessage<
TData = unknown,
TTopic extends string = string,
> {
topic: TTopic;
data: TData;
origin: string | null;
}
interface Subscriber<T extends ProcessMessage> {
filter: (msg: T) => boolean;
callback: (msg: T) => void;
}
const subscribers: Subscriber<ProcessMessage>[] = [];
// Declare the global function on the window type
declare global {
interface Window {
notifyBus: (msg: ProcessMessage) => void;
}
}
window.notifyBus = (msg: ProcessMessage) => {
console.debug("notifyBus called with:", msg);
for (const sub of subscribers) {
try {
if (sub.filter(msg)) {
sub.callback(msg);
}
} catch (e) {
console.error("Subscriber threw an error:", e);
}
}
};
/**
* Subscribe to any message
*
* Returns a function to unsubsribe itself
*
* consider using useNotify for reactive usage on solidjs
*/
export function _subscribeNotify<T extends ProcessMessage>(
filter: (msg: T) => boolean,
callback: (msg: T) => void,
) {
// Cast to shared subscriber type for storage
const sub: Subscriber<ProcessMessage> = {
filter: filter as (msg: ProcessMessage) => boolean,
callback: callback as (msg: ProcessMessage) => void,
};
subscribers.push(sub);
return () => {
const idx = subscribers.indexOf(sub);
if (idx >= 0) subscribers.splice(idx, 1);
};
}
/**
* Returns a reactive signal that tracks a message by the given filter predicate
* The signal has the value of the last message where filter was true
* null in case no message was recieved yet
*/
export function useNotify<T extends ProcessMessage = ProcessMessage>(
filter: (msg: T) => boolean = () => true as boolean,
) {
const [message, setMessage] = createSignal<T | null>(null);
const unsubscribe = _subscribeNotify(filter, (msg) => setMessage(() => msg));
onCleanup(unsubscribe);
return message;
}
/**
* Tracks any message that was sent from this api origin
*
*/
export function useNotifyOrigin<
T extends ProcessMessage = ProcessMessage,
K extends OperationNames = OperationNames,
>(origin: K) {
return useNotify<T>((m) => m.origin === origin);
}

View File

@@ -5,8 +5,7 @@ import {
StepperProvider, StepperProvider,
useStepper, useStepper,
} from "@/src/hooks/stepper"; } from "@/src/hooks/stepper";
import { createForm, FieldValues, SubmitHandler } from "@modular-forms/solid"; import { Show } from "solid-js";
import { onMount, Show } from "solid-js";
import { Dynamic } from "solid-js/web"; import { Dynamic } from "solid-js/web";
import { initialSteps } from "./steps/Initial"; import { initialSteps } from "./steps/Initial";
import { createInstallerSteps } from "./steps/createInstaller"; import { createInstallerSteps } from "./steps/createInstaller";
@@ -20,11 +19,12 @@ const InstallStepper = (props: InstallStepperProps) => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
const [store, set] = getStepStore<InstallStoreType>(stepSignal); const [store, set] = getStepStore<InstallStoreType>(stepSignal);
onMount(() => { return (
set("done", props.onDone); <Dynamic
}); component={stepSignal.currentStep().content}
onDone={props.onDone}
return <Dynamic component={stepSignal.currentStep().content} />; />
);
}; };
export interface InstallModalProps { export interface InstallModalProps {
@@ -53,8 +53,8 @@ export interface InstallStoreType {
mainDisk: string; mainDisk: string;
// ...TODO Vars // ...TODO Vars
progress: ApiCall<"run_machine_install">; progress: ApiCall<"run_machine_install">;
promptValues: PromptValues; promptValues: PromptValues;
prepareStep: "disk" | "generators" | "install";
}; };
done: () => void; done: () => void;
} }
@@ -84,6 +84,7 @@ export const InstallModal = (props: InstallModalProps) => {
return ( return (
<StepperProvider stepper={stepper}> <StepperProvider stepper={stepper}>
<Modal <Modal
class="h-[30rem] w-screen max-w-3xl"
mount={props.mount} mount={props.mount}
title="Install machine" title="Install machine"
onClose={() => { onClose={() => {

View File

@@ -12,8 +12,8 @@ const ChoiceLocalOrRemote = () => {
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex flex-col justify-center gap-1 px-1"> <div class="flex flex-col justify-center gap-1 px-1">
<Typography <Typography
hierarchy="label" hierarchy="body"
size="xs" size="default"
weight="bold" weight="bold"
color="primary" color="primary"
> >
@@ -33,8 +33,8 @@ const ChoiceLocalOrRemote = () => {
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex flex-col justify-center gap-1 px-1"> <div class="flex flex-col justify-center gap-1 px-1">
<Typography <Typography
hierarchy="label" hierarchy="body"
size="xs" size="default"
weight="bold" weight="bold"
color="primary" color="primary"
> >
@@ -64,8 +64,8 @@ const ChoiceLocalInstaller = () => {
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex flex-col justify-center gap-1 px-1"> <div class="flex flex-col justify-center gap-1 px-1">
<Typography <Typography
hierarchy="label" hierarchy="body"
size="xs" size="default"
weight="bold" weight="bold"
color="primary" color="primary"
> >
@@ -85,8 +85,8 @@ const ChoiceLocalInstaller = () => {
<div class="flex justify-between gap-2"> <div class="flex justify-between gap-2">
<div class="flex flex-col justify-center gap-1 px-1"> <div class="flex flex-col justify-center gap-1 px-1">
<Typography <Typography
hierarchy="label" hierarchy="body"
size="xs" size="default"
weight="bold" weight="bold"
color="primary" color="primary"
> >

View File

@@ -149,7 +149,7 @@ const ConfigureImage = () => {
let content: Node; let content: Node;
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} class="h-full">
<StepLayout <StepLayout
body={ body={
<div <div
@@ -241,7 +241,7 @@ const ChooseDisk = () => {
const stripId = (s: string) => s.split("-")[1] ?? s; const stripId = (s: string) => s.split("-")[1] ?? s;
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} class="h-full">
<StepLayout <StepLayout
body={ body={
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -317,7 +317,7 @@ const FlashProgress = () => {
}; };
return ( return (
<div class="flex h-60 w-full flex-col items-center justify-end bg-inv-4"> <div class="flex size-full h-60 flex-col items-center justify-end bg-inv-4">
<div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1"> <div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1">
<Typography <Typography
hierarchy="title" hierarchy="title"
@@ -343,7 +343,7 @@ const FlashProgress = () => {
const FlashDone = () => { const FlashDone = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
return ( return (
<div class="flex w-full flex-col items-center bg-inv-4"> <div class="flex size-full flex-col items-center justify-between bg-inv-4">
<div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1"> <div class="flex w-full max-w-md flex-col items-center gap-3 py-6 fg-inv-1">
<div class="rounded-full bg-semantic-success-4"> <div class="rounded-full bg-semantic-success-4">
<Icon icon="Checkmark" class="size-9" /> <Icon icon="Checkmark" class="size-9" />
@@ -361,15 +361,15 @@ const FlashDone = () => {
title="Remove it and plug it into the machine that you want to install." title="Remove it and plug it into the machine that you want to install."
description="" description=""
/> />
<div class="mt-3 flex w-full justify-end"> </div>
<Button <div class="flex w-full justify-end p-6">
hierarchy="primary" <Button
endIcon="ArrowRight" hierarchy="primary"
onClick={() => stepSignal.next()} endIcon="ArrowRight"
> onClick={() => stepSignal.next()}
Next >
</Button> Next
</div> </Button>
</div> </div>
</div> </div>
); );

View File

@@ -2,6 +2,7 @@ import { Typography } from "@/src/components/Typography/Typography";
import { BackButton, NextButton, StepLayout } from "../../Steps"; import { BackButton, NextButton, StepLayout } from "../../Steps";
import { import {
createForm, createForm,
FieldValues,
getError, getError,
SubmitHandler, SubmitHandler,
valiForm, valiForm,
@@ -12,7 +13,7 @@ import { getStepStore, useStepper } from "@/src/hooks/stepper";
import { InstallSteps, InstallStoreType, PromptValues } from "../install"; import { InstallSteps, InstallStoreType, PromptValues } from "../install";
import { TextInput } from "@/src/components/Form/TextInput"; import { TextInput } from "@/src/components/Form/TextInput";
import { Alert } from "@/src/components/Alert/Alert"; import { Alert } from "@/src/components/Alert/Alert";
import { For, onMount, Show } from "solid-js"; import { For, Match, Show, Switch } from "solid-js";
import { Divider } from "@/src/components/Divider/Divider"; import { Divider } from "@/src/components/Divider/Divider";
import { Orienter } from "@/src/components/Form/Orienter"; import { Orienter } from "@/src/components/Form/Orienter";
import { Button } from "@/src/components/Button/Button"; import { Button } from "@/src/components/Button/Button";
@@ -27,6 +28,7 @@ import {
} from "@/src/hooks/queries"; } from "@/src/hooks/queries";
import { useClanURI } from "@/src/hooks/clan"; import { useClanURI } from "@/src/hooks/clan";
import { useApiClient } from "@/src/hooks/ApiClient"; import { useApiClient } from "@/src/hooks/ApiClient";
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
export const InstallHeader = (props: { machineName: string }) => { export const InstallHeader = (props: { machineName: string }) => {
return ( return (
@@ -72,7 +74,7 @@ const ConfigureAddress = () => {
}; };
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} class="h-full">
<StepLayout <StepLayout
body={ body={
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -230,7 +232,7 @@ const ConfigureDisk = () => {
); );
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} class="h-full">
<StepLayout <StepLayout
body={ body={
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -302,19 +304,19 @@ const ConfigureData = () => {
); );
}; };
type PromptGroup = { interface PromptGroup {
name: string; name: string;
fields: { fields: {
prompt: Prompt; prompt: Prompt;
generator: string; generator: string;
value?: string | null; value?: string | null;
}[]; }[];
}; }
type Prompt = NonNullable<MachineGenerators[number]["prompts"]>[number]; type Prompt = NonNullable<MachineGenerators[number]["prompts"]>[number];
type PromptForm = { interface PromptForm extends FieldValues {
promptValues: PromptValues; promptValues: PromptValues;
}; }
interface PromptsFieldsProps { interface PromptsFieldsProps {
generators: MachineGenerators; generators: MachineGenerators;
@@ -365,7 +367,7 @@ const PromptsFields = (props: PromptsFieldsProps) => {
}; };
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} class="h-full">
<StepLayout <StepLayout
body={ body={
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -449,6 +451,8 @@ const InstallSummary = () => {
// Here you would typically trigger the installation process // Here you would typically trigger the installation process
console.log("Installation started"); console.log("Installation started");
stepSignal.setActiveStep("install:progress");
const setDisk = client.fetch("set_machine_disk_schema", { const setDisk = client.fetch("set_machine_disk_schema", {
machine: { machine: {
flake: { flake: {
@@ -463,6 +467,10 @@ const InstallSummary = () => {
force: true, force: true,
}); });
set("install", (s) => ({
...s,
prepareStep: "disk",
}));
const diskResult = await setDisk.result; // Wait for the disk schema to be set const diskResult = await setDisk.result; // Wait for the disk schema to be set
if (diskResult.status === "error") { if (diskResult.status === "error") {
console.error("Error setting disk schema:", diskResult.errors); console.error("Error setting disk schema:", diskResult.errors);
@@ -474,8 +482,11 @@ const InstallSummary = () => {
base_dir: clanUri, base_dir: clanUri,
machine_name: store.install.machineName, machine_name: store.install.machineName,
}); });
stepSignal.setActiveStep("install:progress");
set("install", (s) => ({
...s,
prepareStep: "generators",
}));
await runGenerators.result; // Wait for the generators to run await runGenerators.result; // Wait for the generators to run
const runInstall = client.fetch("run_machine_install", { const runInstall = client.fetch("run_machine_install", {
@@ -493,6 +504,7 @@ const InstallSummary = () => {
}); });
set("install", (s) => ({ set("install", (s) => ({
...s, ...s,
prepareStep: "install",
progress: runInstall, progress: runInstall,
})); }));
@@ -533,6 +545,8 @@ const InstallSummary = () => {
); );
}; };
type InstallTopic = "generators" | "upload-secrets" | "nixos-anywhere";
const InstallProgress = () => { const InstallProgress = () => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
const [store, get] = getStepStore<InstallStoreType>(stepSignal); const [store, get] = getStepStore<InstallStoreType>(stepSignal);
@@ -542,10 +556,14 @@ const InstallProgress = () => {
if (progress) { if (progress) {
await progress.cancel(); await progress.cancel();
} }
store.done(); stepSignal.previous();
}; };
const installState = useNotifyOrigin<ProcessMessage<unknown, InstallTopic>>(
"run_machine_install",
);
return ( return (
<div class="flex h-60 w-full flex-col items-center justify-end bg-inv-4"> <div class="flex size-full h-60 flex-col items-center justify-end bg-inv-4">
<div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1"> <div class="mb-6 flex w-full max-w-md flex-col items-center gap-3 fg-inv-1">
<Typography <Typography
hierarchy="title" hierarchy="title"
@@ -555,6 +573,36 @@ const InstallProgress = () => {
> >
Machine is beeing installed Machine is beeing installed
</Typography> </Typography>
<Typography
hierarchy="label"
size="default"
class="py-2"
color="secondary"
inverted
>
<Switch fallback={"Waiting for preparation to start..."}>
<Match when={store.install.prepareStep === "disk"}>
Configuring disk schema ...
</Match>
<Match when={store.install.prepareStep === "generators"}>
Provisioning services ...
</Match>
<Match when={store.install.prepareStep === "install"}>
{/* Progress after the run_machine_install api call */}
<Switch fallback={"Waiting for installation to start..."}>
<Match when={installState()?.topic === "generators"}>
Checking services ...
</Match>
<Match when={installState()?.topic === "upload-secrets"}>
Uploading Credentials ...
</Match>
<Match when={installState()?.topic === "nixos-anywhere"}>
Running nixos-anywhere ...
</Match>
</Switch>
</Match>
</Switch>
</Typography>
<LoadingBar /> <LoadingBar />
<Button <Button
hierarchy="primary" hierarchy="primary"
@@ -569,7 +617,10 @@ const InstallProgress = () => {
); );
}; };
const FlashDone = () => { interface InstallDoneProps {
onDone: () => void;
}
const InstallDone = (props: InstallDoneProps) => {
const stepSignal = useStepper<InstallSteps>(); const stepSignal = useStepper<InstallSteps>();
const [store, get] = getStepStore<InstallStoreType>(stepSignal); const [store, get] = getStepStore<InstallStoreType>(stepSignal);
@@ -592,7 +643,7 @@ const FlashDone = () => {
hierarchy="primary" hierarchy="primary"
endIcon="Close" endIcon="Close"
size="s" size="s"
onClick={() => store.done()} onClick={() => props.onDone()}
> >
Close Close
</Button> </Button>
@@ -639,7 +690,7 @@ export const installSteps = [
}, },
{ {
id: "install:done", id: "install:done",
content: FlashDone, content: InstallDone,
isSplash: true, isSplash: true,
}, },
] as const; ] as const;

View File

@@ -9,7 +9,7 @@ interface StepLayoutProps {
} }
export const StepLayout = (props: StepLayoutProps) => { export const StepLayout = (props: StepLayoutProps) => {
return ( return (
<div class="flex flex-col gap-6"> <div class="flex size-full grow flex-col justify-between gap-6">
{props.body} {props.body}
{props.footer} {props.footer}
</div> </div>

View File

@@ -5,11 +5,13 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from functools import wraps from functools import wraps
from inspect import Parameter, Signature, signature from inspect import Parameter, Signature, signature
from queue import Queue
from types import ModuleType from types import ModuleType
from typing import ( from typing import (
Annotated, Annotated,
Any, Any,
Literal, Literal,
TypedDict,
TypeVar, TypeVar,
get_type_hints, get_type_hints,
) )
@@ -31,6 +33,30 @@ T = TypeVar("T")
ResponseDataType = TypeVar("ResponseDataType") ResponseDataType = TypeVar("ResponseDataType")
class ProcessMessage(TypedDict):
"""
Represents a message to be sent to the UI.
Attributes:
- topic: The topic of the message, used to identify the type of message.
- data: The data to be sent with the message.
- origin: The API operation that this message is related to, if applicable.
"""
topic: str
data: Any
origin: str | None
message_queue: Queue[ProcessMessage] = Queue()
"""
A global message queue for sending messages to the UI
This can be used to send notifications or messages to the UI. Before returning a response.
The clan-app imports the queue as clan_lib.api.message_queue and subscribes to it.
"""
@dataclass @dataclass
class ApiError: class ApiError:
message: str message: str

View File

@@ -9,7 +9,7 @@ from clan_cli.facts.generate import generate_facts
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
from clan_cli.vars.generate import generate_vars from clan_cli.vars.generate import generate_vars
from clan_lib.api import API from clan_lib.api import API, message_queue
from clan_lib.cmd import Log, RunOpts, run from clan_lib.cmd import Log, RunOpts, run
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_config, nix_shell from clan_lib.nix import nix_config, nix_shell
@@ -22,6 +22,20 @@ log = logging.getLogger(__name__)
BuildOn = Literal["auto", "local", "remote"] BuildOn = Literal["auto", "local", "remote"]
Step = Literal["generators", "upload-secrets", "nixos-anywhere"]
def notify_install_step(current: Step) -> None:
message_queue.put(
{
"topic": current,
"data": None,
# MUST be set the to api function name, while technically you can set any origin, this is a bad idea.
"origin": "run_machine_install",
}
)
@dataclass @dataclass
class InstallOptions: class InstallOptions:
machine: Machine machine: Machine
@@ -64,6 +78,8 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
] ]
) )
# Notify the UI about what we are doing
notify_install_step("generators")
generate_facts([machine]) generate_facts([machine])
generate_vars([machine]) generate_vars([machine])
@@ -74,6 +90,9 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
activation_secrets = base_directory / "activation_secrets" activation_secrets = base_directory / "activation_secrets"
upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/") upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/")
upload_dir.mkdir(parents=True) upload_dir.mkdir(parents=True)
# Notify the UI about what we are doing
notify_install_step("upload-secrets")
machine.secret_facts_store.upload(upload_dir) machine.secret_facts_store.upload(upload_dir)
machine.secret_vars_store.populate_dir( machine.secret_vars_store.populate_dir(
machine.name, upload_dir, phases=["activation", "users", "services"] machine.name, upload_dir, phases=["activation", "users", "services"]
@@ -174,4 +193,6 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
["nixos-anywhere"], ["nixos-anywhere"],
cmd, cmd,
) )
notify_install_step("nixos-anywhere")
run(cmd, RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True)) run(cmd, RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True))