palmier 0.9.6 → 0.9.8
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/README.md +28 -13
- package/dist/agents/agent.d.ts +0 -1
- package/dist/agents/agent.js +0 -1
- package/dist/agents/aider.d.ts +0 -1
- package/dist/agents/aider.js +0 -1
- package/dist/agents/claude.d.ts +0 -1
- package/dist/agents/claude.js +0 -1
- package/dist/agents/cline.d.ts +0 -1
- package/dist/agents/cline.js +0 -1
- package/dist/agents/codex.d.ts +0 -1
- package/dist/agents/codex.js +0 -1
- package/dist/agents/copilot.d.ts +0 -1
- package/dist/agents/copilot.js +0 -1
- package/dist/agents/cursor.d.ts +0 -1
- package/dist/agents/cursor.js +0 -1
- package/dist/agents/deepagents.d.ts +0 -1
- package/dist/agents/deepagents.js +0 -1
- package/dist/agents/droid.d.ts +0 -1
- package/dist/agents/droid.js +0 -1
- package/dist/agents/gemini.d.ts +0 -1
- package/dist/agents/gemini.js +0 -1
- package/dist/agents/goose.d.ts +0 -1
- package/dist/agents/goose.js +0 -1
- package/dist/agents/hermes.d.ts +0 -1
- package/dist/agents/hermes.js +0 -1
- package/dist/agents/kimi.d.ts +0 -1
- package/dist/agents/kimi.js +0 -1
- package/dist/agents/kiro.d.ts +0 -1
- package/dist/agents/kiro.js +0 -1
- package/dist/agents/openclaw.d.ts +0 -1
- package/dist/agents/openclaw.js +0 -1
- package/dist/agents/opencode.d.ts +0 -1
- package/dist/agents/opencode.js +0 -1
- package/dist/agents/qoder.d.ts +0 -1
- package/dist/agents/qoder.js +0 -1
- package/dist/agents/qwen.d.ts +0 -1
- package/dist/agents/qwen.js +0 -1
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/client-store.d.ts +0 -1
- package/dist/client-store.js +0 -1
- package/dist/commands/clients.d.ts +0 -1
- package/dist/commands/clients.js +0 -1
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/pair.d.ts +0 -1
- package/dist/commands/pair.js +0 -1
- package/dist/commands/restart.d.ts +0 -1
- package/dist/commands/restart.js +0 -1
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +19 -3
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/serve.js +0 -1
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/event-queues.d.ts +0 -1
- package/dist/event-queues.js +0 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/linked-device.d.ts +0 -1
- package/dist/linked-device.js +0 -1
- package/dist/mcp-handler.d.ts +0 -1
- package/dist/mcp-handler.js +0 -1
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +0 -1
- package/dist/nats-client.d.ts +0 -1
- package/dist/nats-client.js +0 -1
- package/dist/network.d.ts +0 -1
- package/dist/network.js +0 -1
- package/dist/notification-store.d.ts +0 -1
- package/dist/notification-store.js +0 -1
- package/dist/pending-requests.d.ts +0 -1
- package/dist/pending-requests.js +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -1
- package/dist/platform/linux.d.ts +0 -1
- package/dist/platform/linux.js +0 -1
- package/dist/platform/macos.d.ts +0 -1
- package/dist/platform/macos.js +0 -1
- package/dist/platform/platform.d.ts +0 -1
- package/dist/platform/platform.js +0 -1
- package/dist/platform/windows.d.ts +0 -1
- package/dist/platform/windows.js +0 -1
- package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
- package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
- package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
- package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.d.ts +0 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/sms-store.d.ts +0 -1
- package/dist/sms-store.js +0 -1
- package/dist/spawn-command.d.ts +0 -1
- package/dist/spawn-command.js +0 -1
- package/dist/task.d.ts +0 -1
- package/dist/task.js +0 -1
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +0 -1
- package/dist/transports/nats-transport.d.ts +0 -1
- package/dist/transports/nats-transport.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/update-checker.d.ts +0 -1
- package/dist/update-checker.js +0 -1
- package/package.json +11 -1
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -37
- package/CLAUDE.md +0 -22
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/manifest.webmanifest +0 -1
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +0 -1
- package/dist/pwa/service-worker.js +0 -2
- package/palmier-server/.github/workflows/ci.yml +0 -21
- package/palmier-server/.github/workflows/deploy.yml +0 -38
- package/palmier-server/CLAUDE.md +0 -17
- package/palmier-server/PRODUCTION.md +0 -358
- package/palmier-server/README.md +0 -231
- package/palmier-server/nats.conf +0 -19
- package/palmier-server/package.json +0 -15
- package/palmier-server/pnpm-lock.yaml +0 -7639
- package/palmier-server/pnpm-workspace.yaml +0 -3
- package/palmier-server/pwa/index.html +0 -16
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/package.json +0 -34
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +0 -3012
- package/palmier-server/pwa/src/App.tsx +0 -59
- package/palmier-server/pwa/src/agentLabels.ts +0 -11
- package/palmier-server/pwa/src/api.ts +0 -67
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
- package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
- package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
- package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
- package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
- package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
- package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
- package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
- package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
- package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
- package/palmier-server/pwa/src/constants.ts +0 -2
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
- package/palmier-server/pwa/src/draftGuard.ts +0 -24
- package/palmier-server/pwa/src/formatTime.ts +0 -44
- package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
- package/palmier-server/pwa/src/main.tsx +0 -14
- package/palmier-server/pwa/src/native/Device.ts +0 -49
- package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
- package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
- package/palmier-server/pwa/src/service-worker.ts +0 -142
- package/palmier-server/pwa/src/types.ts +0 -75
- package/palmier-server/pwa/src/vite-env.d.ts +0 -11
- package/palmier-server/pwa/tsconfig.json +0 -21
- package/palmier-server/pwa/tsconfig.node.json +0 -19
- package/palmier-server/pwa/vite.config.ts +0 -47
- package/palmier-server/server/.env.example +0 -20
- package/palmier-server/server/package.json +0 -36
- package/palmier-server/server/src/db.ts +0 -44
- package/palmier-server/server/src/fcm.ts +0 -74
- package/palmier-server/server/src/index.ts +0 -688
- package/palmier-server/server/src/nats-jwt.ts +0 -299
- package/palmier-server/server/src/nats-setup.ts +0 -48
- package/palmier-server/server/src/nats.ts +0 -33
- package/palmier-server/server/src/notify.ts +0 -34
- package/palmier-server/server/src/push.ts +0 -68
- package/palmier-server/server/src/routes/device.ts +0 -224
- package/palmier-server/server/src/routes/fcm.ts +0 -64
- package/palmier-server/server/src/routes/hosts.ts +0 -56
- package/palmier-server/server/src/routes/push.ts +0 -101
- package/palmier-server/server/tsconfig.json +0 -20
- package/palmier-server/spec.md +0 -533
- package/src/agents/agent-instructions.md +0 -28
- package/src/agents/agent.ts +0 -114
- package/src/agents/aider.ts +0 -35
- package/src/agents/claude.ts +0 -39
- package/src/agents/cline.ts +0 -35
- package/src/agents/codex.ts +0 -40
- package/src/agents/copilot.ts +0 -37
- package/src/agents/cursor.ts +0 -36
- package/src/agents/deepagents.ts +0 -36
- package/src/agents/droid.ts +0 -35
- package/src/agents/gemini.ts +0 -43
- package/src/agents/goose.ts +0 -33
- package/src/agents/hermes.ts +0 -36
- package/src/agents/kimi.ts +0 -35
- package/src/agents/kiro.ts +0 -36
- package/src/agents/openclaw.ts +0 -29
- package/src/agents/opencode.ts +0 -36
- package/src/agents/qoder.ts +0 -36
- package/src/agents/qwen.ts +0 -32
- package/src/agents/shared-prompt.ts +0 -30
- package/src/client-store.ts +0 -68
- package/src/commands/clients.ts +0 -29
- package/src/commands/info.ts +0 -29
- package/src/commands/init.ts +0 -165
- package/src/commands/pair.ts +0 -137
- package/src/commands/restart.ts +0 -6
- package/src/commands/run.ts +0 -608
- package/src/commands/serve.ts +0 -211
- package/src/commands/uninstall.ts +0 -9
- package/src/config.ts +0 -36
- package/src/cross-spawn.d.ts +0 -5
- package/src/event-queues.ts +0 -41
- package/src/events.ts +0 -29
- package/src/index.ts +0 -111
- package/src/linked-device.ts +0 -52
- package/src/mcp-handler.ts +0 -200
- package/src/mcp-tools.ts +0 -839
- package/src/nats-client.ts +0 -19
- package/src/network.ts +0 -96
- package/src/notification-store.ts +0 -30
- package/src/pending-requests.ts +0 -73
- package/src/platform/index.ts +0 -20
- package/src/platform/linux.ts +0 -296
- package/src/platform/macos.ts +0 -329
- package/src/platform/platform.ts +0 -31
- package/src/platform/windows.ts +0 -299
- package/src/rpc-handler.ts +0 -691
- package/src/sms-store.ts +0 -28
- package/src/spawn-command.ts +0 -123
- package/src/task.ts +0 -343
- package/src/transports/http-transport.ts +0 -478
- package/src/transports/nats-transport.ts +0 -76
- package/src/types.ts +0 -89
- package/src/update-checker.ts +0 -40
- package/test/agent-instructions.test.ts +0 -209
- package/test/agent-output-parsing.test.ts +0 -74
- package/test/linux-cron.test.ts +0 -41
- package/test/macos-plist.test.ts +0 -112
- package/test/notification-store.test.ts +0 -57
- package/test/pairing.test.ts +0 -35
- package/test/result-state.test.ts +0 -110
- package/test/task-parsing.test.ts +0 -82
- package/test/taskrun-messages.test.ts +0 -224
- package/test/tsconfig.json +0 -9
- package/test/windows-xml.test.ts +0 -89
- package/tsconfig.json +0 -19
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
useState,
|
|
5
|
-
useCallback,
|
|
6
|
-
useEffect,
|
|
7
|
-
type ReactNode,
|
|
8
|
-
} from "react";
|
|
9
|
-
import type { PairedHost } from "../types";
|
|
10
|
-
|
|
11
|
-
interface HostStoreContextValue {
|
|
12
|
-
pairedHosts: PairedHost[];
|
|
13
|
-
addPairedHost(host: PairedHost): void;
|
|
14
|
-
removePairedHost(hostId: string): void;
|
|
15
|
-
renamePairedHost(hostId: string, name: string): void;
|
|
16
|
-
setHostLanUrl(hostId: string, lanUrl: string | undefined): void;
|
|
17
|
-
setHostLastAgent(hostId: string, agent: string): void;
|
|
18
|
-
setHostTimezone(hostId: string, timezone: string | undefined): void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const HostStoreContext = createContext<HostStoreContextValue | null>(null);
|
|
22
|
-
|
|
23
|
-
const STORAGE_KEY = "palmier_paired_hosts";
|
|
24
|
-
|
|
25
|
-
function loadHosts(): PairedHost[] {
|
|
26
|
-
try {
|
|
27
|
-
const raw = localStorage.getItem(STORAGE_KEY);
|
|
28
|
-
if (raw) {
|
|
29
|
-
return JSON.parse(raw) as PairedHost[];
|
|
30
|
-
}
|
|
31
|
-
} catch { /* ignore */ }
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function saveHosts(hosts: PairedHost[]) {
|
|
36
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(hosts));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const LOCAL_HOST_ID = "local";
|
|
40
|
-
|
|
41
|
-
/** Local mode: served by palmier serve on localhost — auto-inject a local host entry. */
|
|
42
|
-
const isLocalMode = !!(window as any).__PALMIER_SERVE__
|
|
43
|
-
&& (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1");
|
|
44
|
-
|
|
45
|
-
export function HostStoreProvider({ children }: { children: ReactNode }) {
|
|
46
|
-
const [pairedHosts, setPairedHosts] = useState<PairedHost[]>(() => {
|
|
47
|
-
const hosts = loadHosts();
|
|
48
|
-
if (isLocalMode && !hosts.some((h) => h.hostId === LOCAL_HOST_ID)) {
|
|
49
|
-
hosts.push({ hostId: LOCAL_HOST_ID, clientToken: "", directUrl: window.location.origin });
|
|
50
|
-
}
|
|
51
|
-
return hosts;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
saveHosts(pairedHosts);
|
|
56
|
-
}, [pairedHosts]);
|
|
57
|
-
|
|
58
|
-
const addPairedHost = useCallback((host: PairedHost) => {
|
|
59
|
-
setPairedHosts((prev) => {
|
|
60
|
-
const filtered = prev.filter((h) => h.hostId !== host.hostId);
|
|
61
|
-
return [...filtered, host];
|
|
62
|
-
});
|
|
63
|
-
}, []);
|
|
64
|
-
|
|
65
|
-
const removePairedHost = useCallback((hostId: string) => {
|
|
66
|
-
setPairedHosts((prev) => prev.filter((h) => h.hostId !== hostId));
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const renamePairedHost = useCallback((hostId: string, name: string) => {
|
|
70
|
-
setPairedHosts((prev) =>
|
|
71
|
-
prev.map((h) => (h.hostId === hostId ? { ...h, name } : h))
|
|
72
|
-
);
|
|
73
|
-
}, []);
|
|
74
|
-
|
|
75
|
-
const setHostLanUrl = useCallback((hostId: string, lanUrl: string | undefined) => {
|
|
76
|
-
setPairedHosts((prev) =>
|
|
77
|
-
prev.map((h) => {
|
|
78
|
-
if (h.hostId !== hostId) return h;
|
|
79
|
-
if (h.lanUrl === lanUrl) return h;
|
|
80
|
-
return { ...h, lanUrl };
|
|
81
|
-
})
|
|
82
|
-
);
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
const setHostLastAgent = useCallback((hostId: string, agent: string) => {
|
|
86
|
-
setPairedHosts((prev) =>
|
|
87
|
-
prev.map((h) => {
|
|
88
|
-
if (h.hostId !== hostId) return h;
|
|
89
|
-
if (h.lastAgent === agent) return h;
|
|
90
|
-
return { ...h, lastAgent: agent };
|
|
91
|
-
})
|
|
92
|
-
);
|
|
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
|
-
|
|
105
|
-
return (
|
|
106
|
-
<HostStoreContext.Provider value={{
|
|
107
|
-
pairedHosts,
|
|
108
|
-
addPairedHost,
|
|
109
|
-
removePairedHost,
|
|
110
|
-
renamePairedHost,
|
|
111
|
-
setHostLanUrl,
|
|
112
|
-
setHostLastAgent,
|
|
113
|
-
setHostTimezone,
|
|
114
|
-
}}>
|
|
115
|
-
{children}
|
|
116
|
-
</HostStoreContext.Provider>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function useHostStore(): HostStoreContextValue {
|
|
121
|
-
const ctx = useContext(HostStoreContext);
|
|
122
|
-
if (!ctx) throw new Error("useHostStore must be used within HostStoreProvider");
|
|
123
|
-
return ctx;
|
|
124
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
let currentDraftMessage: string | null = null;
|
|
2
|
-
|
|
3
|
-
function handleBeforeUnload(e: BeforeUnloadEvent) {
|
|
4
|
-
if (currentDraftMessage) {
|
|
5
|
-
e.preventDefault();
|
|
6
|
-
e.returnValue = "";
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function setDraftMessage(message: string | null) {
|
|
11
|
-
const wasActive = !!currentDraftMessage;
|
|
12
|
-
currentDraftMessage = message;
|
|
13
|
-
const nowActive = !!currentDraftMessage;
|
|
14
|
-
if (nowActive && !wasActive) {
|
|
15
|
-
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
16
|
-
} else if (!nowActive && wasActive) {
|
|
17
|
-
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function confirmLeaveDraft(): boolean {
|
|
22
|
-
if (!currentDraftMessage) return true;
|
|
23
|
-
return window.confirm(currentDraftMessage);
|
|
24
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import { useHostConnection } from "./contexts/HostConnectionContext";
|
|
3
|
-
|
|
4
|
-
/**
|
|
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.
|
|
7
|
-
*/
|
|
8
|
-
export function formatTime(ms: number, timeZone?: string): string {
|
|
9
|
-
const d = new Date(ms);
|
|
10
|
-
const now = new Date();
|
|
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
|
-
};
|
|
44
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useCallback } from "react";
|
|
2
|
-
|
|
3
|
-
interface StackEntry {
|
|
4
|
-
close: () => void;
|
|
5
|
-
pushed: React.RefObject<boolean>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Global stack so only the top-most modal handles a back gesture.
|
|
10
|
-
* Each entry is a close callback + its pushed ref; popstate pops the top one.
|
|
11
|
-
*/
|
|
12
|
-
const stack: StackEntry[] = [];
|
|
13
|
-
|
|
14
|
-
function globalPopHandler() {
|
|
15
|
-
const top = stack.pop();
|
|
16
|
-
if (top) {
|
|
17
|
-
// Mark pushed=false BEFORE calling close so the effect
|
|
18
|
-
// knows the back navigation was already handled by the browser.
|
|
19
|
-
top.pushed.current = false;
|
|
20
|
-
top.close();
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Single global listener, added once
|
|
25
|
-
let listening = false;
|
|
26
|
-
function ensureListener() {
|
|
27
|
-
if (listening) return;
|
|
28
|
-
listening = true;
|
|
29
|
-
window.addEventListener("popstate", globalPopHandler);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Pushes a history entry when `open` becomes true.
|
|
34
|
-
* When the user swipes back / presses back, calls `onClose` instead of navigating away.
|
|
35
|
-
* When `open` becomes false programmatically, pops the history entry.
|
|
36
|
-
* Supports nesting: only the innermost (most recent) modal responds to back.
|
|
37
|
-
*/
|
|
38
|
-
export function useBackClose(open: boolean, onClose: () => void) {
|
|
39
|
-
const pushed = useRef(false);
|
|
40
|
-
const onCloseRef = useRef(onClose);
|
|
41
|
-
onCloseRef.current = onClose;
|
|
42
|
-
|
|
43
|
-
const stableClose = useCallback(() => onCloseRef.current(), []);
|
|
44
|
-
const entry = useRef<StackEntry>({ close: stableClose, pushed });
|
|
45
|
-
entry.current.close = stableClose;
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (open && !pushed.current) {
|
|
49
|
-
ensureListener();
|
|
50
|
-
history.pushState({ modal: true }, "");
|
|
51
|
-
stack.push(entry.current);
|
|
52
|
-
pushed.current = true;
|
|
53
|
-
} else if (!open && pushed.current) {
|
|
54
|
-
// Closed programmatically — browser hasn't gone back yet, so we must.
|
|
55
|
-
pushed.current = false;
|
|
56
|
-
const idx = stack.indexOf(entry.current);
|
|
57
|
-
if (idx !== -1) stack.splice(idx, 1);
|
|
58
|
-
history.back();
|
|
59
|
-
}
|
|
60
|
-
// If !open && !pushed.current, the popstate handler already went back — nothing to do.
|
|
61
|
-
}, [open, stableClose]);
|
|
62
|
-
|
|
63
|
-
// Cleanup on unmount if still pushed
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
const e = entry.current;
|
|
66
|
-
return () => {
|
|
67
|
-
if (pushed.current) {
|
|
68
|
-
pushed.current = false;
|
|
69
|
-
const idx = stack.indexOf(e);
|
|
70
|
-
if (idx !== -1) stack.splice(idx, 1);
|
|
71
|
-
history.back();
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}, []);
|
|
75
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
|
|
3
|
-
export function useMediaQuery(query: string): boolean {
|
|
4
|
-
const [matches, setMatches] = useState(
|
|
5
|
-
() => window.matchMedia(query).matches
|
|
6
|
-
);
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
const mql = window.matchMedia(query);
|
|
10
|
-
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
11
|
-
mql.addEventListener("change", handler);
|
|
12
|
-
setMatches(mql.matches);
|
|
13
|
-
return () => mql.removeEventListener("change", handler);
|
|
14
|
-
}, [query]);
|
|
15
|
-
|
|
16
|
-
return matches;
|
|
17
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
interface Options {
|
|
4
|
-
onRefresh: () => Promise<unknown> | void;
|
|
5
|
-
/** Pixels the user must pull past before a release triggers a refresh. */
|
|
6
|
-
threshold?: number;
|
|
7
|
-
/** Maximum visual pull distance after resistance dampening. */
|
|
8
|
-
maxPull?: number;
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Window-level pull-to-refresh for the tasks/sessions lists. Activates only
|
|
14
|
-
* when the user starts a touch at `scrollY === 0` and drags downward.
|
|
15
|
-
* Returns `pullDistance` (dampened pixels to visually translate an indicator)
|
|
16
|
-
* and `refreshing` (true while the consumer's onRefresh promise is in flight).
|
|
17
|
-
*/
|
|
18
|
-
export function usePullToRefresh({
|
|
19
|
-
onRefresh,
|
|
20
|
-
threshold = 70,
|
|
21
|
-
maxPull = 110,
|
|
22
|
-
enabled = true,
|
|
23
|
-
}: Options) {
|
|
24
|
-
const [pullDistance, setPullDistance] = useState(0);
|
|
25
|
-
const [refreshing, setRefreshing] = useState(false);
|
|
26
|
-
|
|
27
|
-
const startY = useRef<number | null>(null);
|
|
28
|
-
const pulling = useRef(false);
|
|
29
|
-
const distance = useRef(0);
|
|
30
|
-
const refreshingRef = useRef(false);
|
|
31
|
-
const onRefreshRef = useRef(onRefresh);
|
|
32
|
-
useEffect(() => { onRefreshRef.current = onRefresh; });
|
|
33
|
-
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!enabled) return;
|
|
36
|
-
|
|
37
|
-
const onTouchStart = (e: TouchEvent) => {
|
|
38
|
-
if (refreshingRef.current || window.scrollY > 0) {
|
|
39
|
-
startY.current = null;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
startY.current = e.touches[0].clientY;
|
|
43
|
-
pulling.current = false;
|
|
44
|
-
distance.current = 0;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const onTouchMove = (e: TouchEvent) => {
|
|
48
|
-
if (refreshingRef.current || startY.current == null) return;
|
|
49
|
-
const dy = e.touches[0].clientY - startY.current;
|
|
50
|
-
if (dy <= 0) {
|
|
51
|
-
pulling.current = false;
|
|
52
|
-
distance.current = 0;
|
|
53
|
-
setPullDistance(0);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
pulling.current = true;
|
|
57
|
-
const adjusted = Math.min(maxPull, dy * 0.5);
|
|
58
|
-
distance.current = adjusted;
|
|
59
|
-
setPullDistance(adjusted);
|
|
60
|
-
if (e.cancelable) e.preventDefault();
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const onTouchEnd = () => {
|
|
64
|
-
if (!pulling.current) {
|
|
65
|
-
startY.current = null;
|
|
66
|
-
setPullDistance(0);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
const pulled = distance.current;
|
|
70
|
-
startY.current = null;
|
|
71
|
-
pulling.current = false;
|
|
72
|
-
distance.current = 0;
|
|
73
|
-
if (pulled >= threshold) {
|
|
74
|
-
refreshingRef.current = true;
|
|
75
|
-
setRefreshing(true);
|
|
76
|
-
setPullDistance(threshold);
|
|
77
|
-
Promise.resolve(onRefreshRef.current())
|
|
78
|
-
.catch(() => { /* swallow — indicator still closes */ })
|
|
79
|
-
.finally(() => {
|
|
80
|
-
refreshingRef.current = false;
|
|
81
|
-
setRefreshing(false);
|
|
82
|
-
setPullDistance(0);
|
|
83
|
-
});
|
|
84
|
-
} else {
|
|
85
|
-
setPullDistance(0);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
window.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
90
|
-
window.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
91
|
-
window.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
92
|
-
window.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
93
|
-
return () => {
|
|
94
|
-
window.removeEventListener("touchstart", onTouchStart);
|
|
95
|
-
window.removeEventListener("touchmove", onTouchMove);
|
|
96
|
-
window.removeEventListener("touchend", onTouchEnd);
|
|
97
|
-
window.removeEventListener("touchcancel", onTouchEnd);
|
|
98
|
-
};
|
|
99
|
-
}, [enabled, threshold, maxPull]);
|
|
100
|
-
|
|
101
|
-
return { pullDistance, refreshing, threshold };
|
|
102
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
import { Capacitor } from "@capacitor/core";
|
|
3
|
-
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
4
|
-
import { apiPost, apiGet } from "../api";
|
|
5
|
-
|
|
6
|
-
export function usePushSubscription() {
|
|
7
|
-
const { activeHost } = useHostConnection();
|
|
8
|
-
const subscribedRef = useRef<string | null>(null);
|
|
9
|
-
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
// Native app uses FCM for notifications, not Web Push
|
|
12
|
-
if (Capacitor.isNativePlatform()) return;
|
|
13
|
-
// Skip push subscription for direct-only (LAN) hosts — no cloud server to relay through
|
|
14
|
-
if (activeHost.directUrl || subscribedRef.current === activeHost.hostId) return;
|
|
15
|
-
|
|
16
|
-
async function subscribe() {
|
|
17
|
-
try {
|
|
18
|
-
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
|
|
19
|
-
console.warn("[Push] Push notifications not supported");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const registration = await navigator.serviceWorker.ready;
|
|
24
|
-
|
|
25
|
-
// Send hostId to SW so it can include it in push responses
|
|
26
|
-
registration.active?.postMessage({
|
|
27
|
-
type: "set-host-id",
|
|
28
|
-
hostId: activeHost.hostId,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
let subscription = await registration.pushManager.getSubscription();
|
|
32
|
-
|
|
33
|
-
if (!subscription) {
|
|
34
|
-
// Get VAPID public key from server
|
|
35
|
-
const { publicKey } = await apiGet<{ publicKey: string }>(
|
|
36
|
-
"/api/push/vapid-key"
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (!publicKey) {
|
|
40
|
-
console.warn("[Push] No VAPID public key configured on server");
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Request permission
|
|
45
|
-
const permission = await Notification.requestPermission();
|
|
46
|
-
if (permission !== "granted") {
|
|
47
|
-
console.log("[Push] Permission denied");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Subscribe
|
|
52
|
-
subscription = await registration.pushManager.subscribe({
|
|
53
|
-
userVisibleOnly: true,
|
|
54
|
-
applicationServerKey: publicKey,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Always ensure subscription is saved to server
|
|
59
|
-
const sub = subscription.toJSON();
|
|
60
|
-
await apiPost("/api/push/subscribe", {
|
|
61
|
-
hostId: activeHost.hostId,
|
|
62
|
-
endpoint: sub.endpoint,
|
|
63
|
-
keys: {
|
|
64
|
-
p256dh: sub.keys!.p256dh,
|
|
65
|
-
auth: sub.keys!.auth,
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
subscribedRef.current = activeHost.hostId;
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.error("[Push] Subscription failed:", err);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
subscribe();
|
|
76
|
-
}, [activeHost]);
|
|
77
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { StrictMode } from "react";
|
|
2
|
-
import { createRoot } from "react-dom/client";
|
|
3
|
-
import { BrowserRouter } from "react-router-dom";
|
|
4
|
-
import "@fontsource-variable/plus-jakarta-sans";
|
|
5
|
-
import App from "./App";
|
|
6
|
-
import "./App.css";
|
|
7
|
-
|
|
8
|
-
createRoot(document.getElementById("root")!).render(
|
|
9
|
-
<StrictMode>
|
|
10
|
-
<BrowserRouter>
|
|
11
|
-
<App />
|
|
12
|
-
</BrowserRouter>
|
|
13
|
-
</StrictMode>
|
|
14
|
-
);
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { Capacitor, registerPlugin, type PluginListenerHandle } from "@capacitor/core";
|
|
2
|
-
|
|
3
|
-
export interface DeepLinkEvent {
|
|
4
|
-
path: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface InstalledApp {
|
|
8
|
-
packageName: string;
|
|
9
|
-
appName: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface CapabilityStatus {
|
|
13
|
-
name: string;
|
|
14
|
-
enabled: boolean;
|
|
15
|
-
supported: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type SetCapabilityReason = "denied" | "no-email-client" | "unsupported";
|
|
19
|
-
|
|
20
|
-
export interface SetCapabilityResult {
|
|
21
|
-
enabled: boolean;
|
|
22
|
-
reason?: SetCapabilityReason;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface DevicePlugin {
|
|
26
|
-
getFcmToken(): Promise<{ token: string }>;
|
|
27
|
-
/** Returns one entry per capability the APK knows about. The PWA renders only these. */
|
|
28
|
-
getCapabilityStatus(): Promise<{ capabilities: CapabilityStatus[] }>;
|
|
29
|
-
/**
|
|
30
|
-
* Atomically gates a single capability. When `enabled: true`, the plugin drives
|
|
31
|
-
* any required permission dialog or system-Settings round-trip and writes the
|
|
32
|
-
* native kill-switch only on grant. When `enabled: false`, the kill-switch is
|
|
33
|
-
* removed (no OS dialog). The returned `enabled` reflects the actual stored
|
|
34
|
-
* state; `reason` distinguishes denial from preconditions like
|
|
35
|
-
* "no email app installed".
|
|
36
|
-
*/
|
|
37
|
-
setCapabilityEnabled(opts: { capability: string; enabled: boolean }): Promise<SetCapabilityResult>;
|
|
38
|
-
/** Returns user-visible (launcher) apps on the device. */
|
|
39
|
-
getInstalledApps(): Promise<{ apps: InstalledApp[] }>;
|
|
40
|
-
addListener(
|
|
41
|
-
event: "deepLink",
|
|
42
|
-
handler: (ev: DeepLinkEvent) => void
|
|
43
|
-
): Promise<PluginListenerHandle>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Null on web — callers must guard with Capacitor.isNativePlatform(). */
|
|
47
|
-
export const Device = Capacitor.isNativePlatform()
|
|
48
|
-
? registerPlugin<DevicePlugin>("Device")
|
|
49
|
-
: null;
|