palmier 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pwa/assets/index-Cvffaohh.js +120 -0
- package/dist/pwa/assets/{index-BsB1tIsn.css → index-DBgOYBrB.css} +1 -1
- package/dist/pwa/assets/{web-DdVpqhvX.js → web-ChtbM4nv.js} +1 -1
- package/dist/pwa/assets/{web-Dcldtodb.js → web-hExASsqW.js} +1 -1
- package/dist/pwa/assets/{web-Eg0A6HEi.js → web-qdLcAD7T.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +20 -0
- package/package.json +1 -1
- package/palmier-server/pwa/src/App.css +6 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +27 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -2
- package/palmier-server/pwa/src/components/SessionsView.tsx +2 -1
- package/palmier-server/pwa/src/components/TaskCard.tsx +14 -6
- package/palmier-server/pwa/src/components/TaskForm.tsx +9 -5
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +12 -0
- package/palmier-server/pwa/src/formatTime.ts +38 -4
- package/palmier-server/pwa/src/pages/Dashboard.tsx +4 -2
- package/palmier-server/pwa/src/types.ts +2 -0
- package/src/rpc-handler.ts +14 -0
- package/dist/pwa/assets/index-DX5qJgHZ.js +0 -120
|
@@ -15,6 +15,7 @@ interface HostStoreContextValue {
|
|
|
15
15
|
renamePairedHost(hostId: string, name: string): void;
|
|
16
16
|
setHostLanUrl(hostId: string, lanUrl: string | undefined): void;
|
|
17
17
|
setHostLastAgent(hostId: string, agent: string): void;
|
|
18
|
+
setHostTimezone(hostId: string, timezone: string | undefined): void;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const HostStoreContext = createContext<HostStoreContextValue | null>(null);
|
|
@@ -91,6 +92,16 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
|
|
|
91
92
|
);
|
|
92
93
|
}, []);
|
|
93
94
|
|
|
95
|
+
const setHostTimezone = useCallback((hostId: string, timezone: string | undefined) => {
|
|
96
|
+
setPairedHosts((prev) =>
|
|
97
|
+
prev.map((h) => {
|
|
98
|
+
if (h.hostId !== hostId) return h;
|
|
99
|
+
if (h.timezone === timezone) return h;
|
|
100
|
+
return { ...h, timezone };
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
94
105
|
return (
|
|
95
106
|
<HostStoreContext.Provider value={{
|
|
96
107
|
pairedHosts,
|
|
@@ -99,6 +110,7 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
|
|
|
99
110
|
renamePairedHost,
|
|
100
111
|
setHostLanUrl,
|
|
101
112
|
setHostLastAgent,
|
|
113
|
+
setHostTimezone,
|
|
102
114
|
}}>
|
|
103
115
|
{children}
|
|
104
116
|
</HostStoreContext.Provider>
|
|
@@ -1,10 +1,44 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useHostConnection } from "./contexts/HostConnectionContext";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Format a timestamp for display. Shows time only if today, otherwise includes the date.
|
|
6
|
+
* If `timeZone` is provided, renders the wall-clock time in that IANA zone.
|
|
3
7
|
*/
|
|
4
|
-
export function formatTime(ms: number): string {
|
|
8
|
+
export function formatTime(ms: number, timeZone?: string): string {
|
|
5
9
|
const d = new Date(ms);
|
|
6
10
|
const now = new Date();
|
|
7
|
-
const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", timeZone });
|
|
12
|
+
// toDateString() would compare in the viewer's local zone, masking day
|
|
13
|
+
// boundaries in the host's zone. Format both dates in the target zone and
|
|
14
|
+
// compare the strings instead.
|
|
15
|
+
const dayOpts: Intl.DateTimeFormatOptions = { year: "numeric", month: "2-digit", day: "2-digit", timeZone };
|
|
16
|
+
const sameDay = d.toLocaleDateString(undefined, dayOpts) === now.toLocaleDateString(undefined, dayOpts);
|
|
17
|
+
if (sameDay) return time;
|
|
18
|
+
return `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", timeZone })} ${time}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns a `formatTime` bound to the active host's timezone. */
|
|
22
|
+
export function useFormatTime(): (ms: number) => string {
|
|
23
|
+
const { activeHost } = useHostConnection();
|
|
24
|
+
const tz = activeHost.timezone;
|
|
25
|
+
return useCallback((ms: number) => formatTime(ms, tz), [tz]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Current wall-clock in the host timezone, as ISO-shaped date + HH:MM strings.
|
|
30
|
+
* Used for "not in the past" validation on scheduled triggers so the floor is
|
|
31
|
+
* the host's now, not the viewer's now.
|
|
32
|
+
*/
|
|
33
|
+
export function hostNowParts(timeZone?: string): { date: string; time: string } {
|
|
34
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
35
|
+
timeZone,
|
|
36
|
+
year: "numeric", month: "2-digit", day: "2-digit",
|
|
37
|
+
hour: "2-digit", minute: "2-digit", hour12: false,
|
|
38
|
+
}).formatToParts(new Date());
|
|
39
|
+
const pick = (t: string) => parts.find((p) => p.type === t)?.value ?? "";
|
|
40
|
+
return {
|
|
41
|
+
date: `${pick("year")}-${pick("month")}-${pick("day")}`,
|
|
42
|
+
time: `${pick("hour")}:${pick("minute")}`,
|
|
43
|
+
};
|
|
10
44
|
}
|
|
@@ -46,7 +46,7 @@ interface PermissionPrompt { permissions: RequiredPermission[]; sessionName?: st
|
|
|
46
46
|
interface InputPrompt { questions: string[]; description?: string; sessionName?: string }
|
|
47
47
|
|
|
48
48
|
export default function Dashboard() {
|
|
49
|
-
const { removePairedHost, setHostLanUrl } = useHostStore();
|
|
49
|
+
const { removePairedHost, setHostLanUrl, setHostTimezone } = useHostStore();
|
|
50
50
|
const { connected, request, subscribeEvents, unauthorized, activeHost } = useHostConnection();
|
|
51
51
|
const hostId = activeHost.hostId;
|
|
52
52
|
const activeClientToken = activeHost.clientToken || null;
|
|
@@ -100,6 +100,7 @@ export default function Dashboard() {
|
|
|
100
100
|
agents?: AgentInfo[];
|
|
101
101
|
version?: string | null;
|
|
102
102
|
host_platform?: string;
|
|
103
|
+
host_timezone?: string;
|
|
103
104
|
linked_client_token?: string | null;
|
|
104
105
|
pending_prompts?: PendingPrompt[];
|
|
105
106
|
lan_url?: string | null;
|
|
@@ -113,6 +114,7 @@ export default function Dashboard() {
|
|
|
113
114
|
setDaemonVersion(version);
|
|
114
115
|
setUpdateRequired(!!version && isOlderThan(version, MIN_HOST_VERSION));
|
|
115
116
|
setHostLanUrl(hostId, result.lan_url ?? undefined);
|
|
117
|
+
setHostTimezone(hostId, result.host_timezone);
|
|
116
118
|
|
|
117
119
|
// Seed modal state from already-pending prompts.
|
|
118
120
|
const confirms = new Map<string, ConfirmPrompt>();
|
|
@@ -146,7 +148,7 @@ export default function Dashboard() {
|
|
|
146
148
|
setInputValues(inputVals);
|
|
147
149
|
})
|
|
148
150
|
.catch(() => { /* silent — update-required prompt guards the broken case */ });
|
|
149
|
-
}, [connected, hostId, request, setHostLanUrl]);
|
|
151
|
+
}, [connected, hostId, request, setHostLanUrl, setHostTimezone]);
|
|
150
152
|
|
|
151
153
|
// Always-on event subscription for modal lifecycle. Independent of which tab
|
|
152
154
|
// is active. Task-card status updates happen inside TasksView while mounted.
|
|
@@ -70,4 +70,6 @@ export interface PairedHost {
|
|
|
70
70
|
lanUrl?: string;
|
|
71
71
|
/** Last-used agent key for this host. Seeds the agent picker on the session composer and task form. */
|
|
72
72
|
lastAgent?: string;
|
|
73
|
+
/** IANA timezone from the host, refreshed on each `host.info`. Drives all time rendering. */
|
|
74
|
+
timezone?: string;
|
|
73
75
|
}
|
package/src/rpc-handler.ts
CHANGED
|
@@ -148,6 +148,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
148
148
|
agents: config.agents ?? [],
|
|
149
149
|
version: currentVersion,
|
|
150
150
|
host_platform: process.platform,
|
|
151
|
+
host_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
151
152
|
linked_client_token: getLinkedDevice()?.clientToken ?? null,
|
|
152
153
|
pending_prompts: listPending(),
|
|
153
154
|
lan_url: buildLanUrl(config.httpPort ?? 7256, config.defaultInterface),
|
|
@@ -634,10 +635,23 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
|
|
|
634
635
|
if (!params.task_id || !params.run_id) {
|
|
635
636
|
return { error: "task_id and run_id are required" };
|
|
636
637
|
}
|
|
638
|
+
const deleteTaskDir = getTaskDir(config.projectRoot, params.task_id);
|
|
639
|
+
let isOneOff = false;
|
|
640
|
+
try { isOneOff = !!parseTaskFile(deleteTaskDir).frontmatter.one_off; } catch { /* ignore */ }
|
|
641
|
+
|
|
637
642
|
const deleted = deleteHistoryEntry(config.projectRoot, params.task_id, params.run_id);
|
|
638
643
|
if (!deleted) {
|
|
639
644
|
return { error: "History entry not found" };
|
|
640
645
|
}
|
|
646
|
+
|
|
647
|
+
// A one-off task exists only to hold its single run — no run means no
|
|
648
|
+
// reason for the task to stick around.
|
|
649
|
+
if (isOneOff) {
|
|
650
|
+
try { getPlatform().removeTaskTimer(params.task_id); } catch { /* best-effort */ }
|
|
651
|
+
clearTaskQueue(params.task_id);
|
|
652
|
+
try { fs.rmSync(deleteTaskDir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
653
|
+
}
|
|
654
|
+
|
|
641
655
|
return { ok: true, task_id: params.task_id, run_id: params.run_id };
|
|
642
656
|
}
|
|
643
657
|
|