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,688 +0,0 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import express from "express";
|
|
5
|
-
import helmet from "helmet";
|
|
6
|
-
import { pool, initDb } from "./db.js";
|
|
7
|
-
import { connectNats, getNatsConnection } from "./nats.js";
|
|
8
|
-
import { StringCodec } from "nats";
|
|
9
|
-
|
|
10
|
-
import hostsRoutes from "./routes/hosts.js";
|
|
11
|
-
import pushRoutes from "./routes/push.js";
|
|
12
|
-
import fcmRoutes from "./routes/fcm.js";
|
|
13
|
-
import deviceRoutes from "./routes/device.js";
|
|
14
|
-
import { notifyClients } from "./notify.js";
|
|
15
|
-
import { sendFcmToClients, sendFcmToDevice } from "./fcm.js";
|
|
16
|
-
import { createPairingCredentials, createPwaCredentials } from "./nats-jwt.js";
|
|
17
|
-
|
|
18
|
-
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
19
|
-
|
|
20
|
-
async function main(): Promise<void> {
|
|
21
|
-
// Initialize database tables
|
|
22
|
-
await initDb();
|
|
23
|
-
|
|
24
|
-
// Connect to NATS
|
|
25
|
-
await connectNats();
|
|
26
|
-
const sc = StringCodec();
|
|
27
|
-
|
|
28
|
-
// Subscribe to unified host-event pub/sub for push notifications
|
|
29
|
-
(async () => {
|
|
30
|
-
try {
|
|
31
|
-
const conn = await getNatsConnection();
|
|
32
|
-
const sub = conn.subscribe("host-event.>");
|
|
33
|
-
console.log("Listening for host-event notifications");
|
|
34
|
-
|
|
35
|
-
for await (const msg of sub) {
|
|
36
|
-
try {
|
|
37
|
-
// Subject: host-event.<host_id>.<task_id>
|
|
38
|
-
const tokens = msg.subject.split(".");
|
|
39
|
-
if (tokens.length < 3) continue;
|
|
40
|
-
const hostId = tokens[1];
|
|
41
|
-
const taskId = tokens.slice(2).join(".");
|
|
42
|
-
|
|
43
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
44
|
-
event_type: string;
|
|
45
|
-
running_state?: string;
|
|
46
|
-
name?: string;
|
|
47
|
-
run_id?: string;
|
|
48
|
-
session_id?: string;
|
|
49
|
-
description?: string;
|
|
50
|
-
agent_name?: string;
|
|
51
|
-
required_permissions?: Array<{ name: string; description: string }>;
|
|
52
|
-
input_questions?: string[];
|
|
53
|
-
result_file?: string;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
if (data.event_type === "confirm-request") {
|
|
57
|
-
await notifyClients(hostId, {
|
|
58
|
-
type: "confirm",
|
|
59
|
-
title: "Confirmation Required",
|
|
60
|
-
body: data.description || "A task requires confirmation to run.",
|
|
61
|
-
host_id: hostId,
|
|
62
|
-
session_id: data.session_id,
|
|
63
|
-
agent_name: data.agent_name,
|
|
64
|
-
});
|
|
65
|
-
} else if (data.event_type === "confirm-resolved") {
|
|
66
|
-
await notifyClients(hostId, {
|
|
67
|
-
type: "confirm-dismiss",
|
|
68
|
-
host_id: hostId,
|
|
69
|
-
session_id: data.session_id,
|
|
70
|
-
});
|
|
71
|
-
} else if (data.event_type === "permission-request") {
|
|
72
|
-
const taskLabel = data.name
|
|
73
|
-
? data.name.length > 60 ? data.name.slice(0, 60) + "…" : data.name
|
|
74
|
-
: "A task";
|
|
75
|
-
await notifyClients(hostId, {
|
|
76
|
-
type: "permission",
|
|
77
|
-
title: "Permission Required",
|
|
78
|
-
body: `${taskLabel} needs additional permissions to continue.`,
|
|
79
|
-
task_id: taskId,
|
|
80
|
-
host_id: hostId,
|
|
81
|
-
});
|
|
82
|
-
} else if (data.event_type === "permission-resolved") {
|
|
83
|
-
await notifyClients(hostId, {
|
|
84
|
-
type: "permission-dismiss",
|
|
85
|
-
task_id: taskId,
|
|
86
|
-
host_id: hostId,
|
|
87
|
-
});
|
|
88
|
-
} else if (data.event_type === "input-request") {
|
|
89
|
-
await notifyClients(hostId, {
|
|
90
|
-
type: "input",
|
|
91
|
-
title: "Input Required",
|
|
92
|
-
body: "A task needs your input to continue.",
|
|
93
|
-
host_id: hostId,
|
|
94
|
-
session_id: data.session_id,
|
|
95
|
-
agent_name: data.agent_name,
|
|
96
|
-
});
|
|
97
|
-
} else if (data.event_type === "input-resolved") {
|
|
98
|
-
await notifyClients(hostId, {
|
|
99
|
-
type: "input-dismiss",
|
|
100
|
-
host_id: hostId,
|
|
101
|
-
session_id: data.session_id,
|
|
102
|
-
});
|
|
103
|
-
} else if (data.event_type === "report-generated" || (data.event_type === "running-state" && data.running_state === "failed")) {
|
|
104
|
-
const label = data.name;
|
|
105
|
-
const taskLabel = label
|
|
106
|
-
? label.length > 60 ? label.slice(0, 60) + "…" : label
|
|
107
|
-
: "Task";
|
|
108
|
-
const isFailure = data.running_state === "failed";
|
|
109
|
-
const body = isFailure ? `${taskLabel} — failed` : `${taskLabel} — report ready`;
|
|
110
|
-
await notifyClients(hostId, {
|
|
111
|
-
type: isFailure ? "fail" : "complete",
|
|
112
|
-
title: "Palmier",
|
|
113
|
-
body,
|
|
114
|
-
task_id: taskId,
|
|
115
|
-
host_id: hostId,
|
|
116
|
-
run_id: data.run_id,
|
|
117
|
-
result_file: data.result_file,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
} catch (err) {
|
|
121
|
-
console.error("[host-event→Push] Error:", err);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error("Failed to subscribe to host-event:", err);
|
|
126
|
-
}
|
|
127
|
-
})();
|
|
128
|
-
|
|
129
|
-
// Subscribe to push notification requests from hosts
|
|
130
|
-
(async () => {
|
|
131
|
-
try {
|
|
132
|
-
const conn = await getNatsConnection();
|
|
133
|
-
const sub = conn.subscribe("host.*.push.send");
|
|
134
|
-
console.log("Listening for push notification requests");
|
|
135
|
-
|
|
136
|
-
for await (const msg of sub) {
|
|
137
|
-
try {
|
|
138
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
139
|
-
hostId: string;
|
|
140
|
-
title: string;
|
|
141
|
-
body: string;
|
|
142
|
-
session_id?: string;
|
|
143
|
-
agent_name?: string;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Validate hostId in subject matches payload
|
|
147
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
148
|
-
if (data.hostId !== subjectHostId) {
|
|
149
|
-
if (msg.reply) {
|
|
150
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
151
|
-
}
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// If no agent_name, session_id is a taskId — include as task_id for deep-linking
|
|
156
|
-
const isTask = data.session_id && !data.agent_name;
|
|
157
|
-
|
|
158
|
-
console.log(`[Push] Sending notification for host ${data.hostId}`);
|
|
159
|
-
await notifyClients(data.hostId, {
|
|
160
|
-
type: "notification",
|
|
161
|
-
host_id: data.hostId,
|
|
162
|
-
title: data.title,
|
|
163
|
-
body: data.body,
|
|
164
|
-
...(isTask ? { task_id: data.session_id } : {}),
|
|
165
|
-
agent_name: data.agent_name,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (msg.reply) {
|
|
169
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
170
|
-
}
|
|
171
|
-
} catch (err) {
|
|
172
|
-
console.error("[Push] Error sending notification:", err);
|
|
173
|
-
if (msg.reply) {
|
|
174
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
} catch (err) {
|
|
179
|
-
console.error("Failed to subscribe to push requests:", err);
|
|
180
|
-
}
|
|
181
|
-
})();
|
|
182
|
-
|
|
183
|
-
// Subscribe to FCM geolocation requests from hosts
|
|
184
|
-
(async () => {
|
|
185
|
-
try {
|
|
186
|
-
const conn = await getNatsConnection();
|
|
187
|
-
const sub = conn.subscribe("host.*.fcm.geolocation");
|
|
188
|
-
console.log("Listening for FCM geolocation requests");
|
|
189
|
-
|
|
190
|
-
for await (const msg of sub) {
|
|
191
|
-
try {
|
|
192
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
193
|
-
hostId: string;
|
|
194
|
-
requestId: string;
|
|
195
|
-
fcmToken?: string;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
199
|
-
if (data.hostId !== subjectHostId) {
|
|
200
|
-
if (msg.reply) {
|
|
201
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
202
|
-
}
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const fcmPayload = {
|
|
207
|
-
type: "geolocation-request",
|
|
208
|
-
requestId: data.requestId,
|
|
209
|
-
hostId: data.hostId,
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
console.log(`[FCM] Sending geolocation request for host ${data.hostId}`);
|
|
213
|
-
if (data.fcmToken) {
|
|
214
|
-
await sendFcmToDevice(data.fcmToken, fcmPayload);
|
|
215
|
-
} else {
|
|
216
|
-
await sendFcmToClients(data.hostId, fcmPayload);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (msg.reply) {
|
|
220
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
221
|
-
}
|
|
222
|
-
} catch (err) {
|
|
223
|
-
console.error("[FCM] Error handling geolocation request:", err);
|
|
224
|
-
if (msg.reply) {
|
|
225
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
console.error("Failed to subscribe to FCM geolocation requests:", err);
|
|
231
|
-
}
|
|
232
|
-
})();
|
|
233
|
-
|
|
234
|
-
// Subscribe to contacts requests from hosts
|
|
235
|
-
(async () => {
|
|
236
|
-
try {
|
|
237
|
-
const conn = await getNatsConnection();
|
|
238
|
-
const sub = conn.subscribe("host.*.fcm.contacts");
|
|
239
|
-
console.log("Listening for FCM contacts requests");
|
|
240
|
-
|
|
241
|
-
for await (const msg of sub) {
|
|
242
|
-
try {
|
|
243
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
244
|
-
hostId: string;
|
|
245
|
-
requestId: string;
|
|
246
|
-
fcmToken?: string;
|
|
247
|
-
action: string;
|
|
248
|
-
name?: string;
|
|
249
|
-
phone?: string;
|
|
250
|
-
email?: string;
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
254
|
-
if (data.hostId !== subjectHostId) {
|
|
255
|
-
if (msg.reply) {
|
|
256
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
257
|
-
}
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const fcmPayload: Record<string, string> = {
|
|
262
|
-
type: data.action === "create" ? "create-contact" : "read-contacts",
|
|
263
|
-
requestId: data.requestId,
|
|
264
|
-
hostId: data.hostId,
|
|
265
|
-
};
|
|
266
|
-
if (data.name) fcmPayload.name = data.name;
|
|
267
|
-
if (data.phone) fcmPayload.phone = data.phone;
|
|
268
|
-
if (data.email) fcmPayload.email = data.email;
|
|
269
|
-
|
|
270
|
-
console.log(`[FCM] Sending contacts ${data.action} request for host ${data.hostId}`);
|
|
271
|
-
if (data.fcmToken) {
|
|
272
|
-
await sendFcmToDevice(data.fcmToken, fcmPayload);
|
|
273
|
-
} else {
|
|
274
|
-
await sendFcmToClients(data.hostId, fcmPayload);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (msg.reply) {
|
|
278
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
279
|
-
}
|
|
280
|
-
} catch (err) {
|
|
281
|
-
console.error("[FCM] Error handling contacts request:", err);
|
|
282
|
-
if (msg.reply) {
|
|
283
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
} catch (err) {
|
|
288
|
-
console.error("Failed to subscribe to FCM contacts requests:", err);
|
|
289
|
-
}
|
|
290
|
-
})();
|
|
291
|
-
|
|
292
|
-
// Subscribe to calendar requests from hosts
|
|
293
|
-
(async () => {
|
|
294
|
-
try {
|
|
295
|
-
const conn = await getNatsConnection();
|
|
296
|
-
const sub = conn.subscribe("host.*.fcm.calendar");
|
|
297
|
-
console.log("Listening for FCM calendar requests");
|
|
298
|
-
|
|
299
|
-
for await (const msg of sub) {
|
|
300
|
-
try {
|
|
301
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
302
|
-
hostId: string;
|
|
303
|
-
requestId: string;
|
|
304
|
-
action: string;
|
|
305
|
-
[key: string]: string;
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
309
|
-
if (data.hostId !== subjectHostId) {
|
|
310
|
-
if (msg.reply) {
|
|
311
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
312
|
-
}
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const fcmPayload: Record<string, string> = {
|
|
317
|
-
type: data.action === "create" ? "create-calendar-event" : "read-calendar",
|
|
318
|
-
requestId: data.requestId,
|
|
319
|
-
hostId: data.hostId,
|
|
320
|
-
};
|
|
321
|
-
// Forward optional fields
|
|
322
|
-
for (const key of ["startDate", "endDate", "title", "startTime", "endTime", "location", "description"]) {
|
|
323
|
-
if (data[key]) fcmPayload[key] = data[key];
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
console.log(`[FCM] Sending calendar ${data.action} request for host ${data.hostId}`);
|
|
327
|
-
await sendFcmToClients(data.hostId, fcmPayload);
|
|
328
|
-
|
|
329
|
-
if (msg.reply) {
|
|
330
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
331
|
-
}
|
|
332
|
-
} catch (err) {
|
|
333
|
-
console.error("[FCM] Error handling calendar request:", err);
|
|
334
|
-
if (msg.reply) {
|
|
335
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
} catch (err) {
|
|
340
|
-
console.error("Failed to subscribe to FCM calendar requests:", err);
|
|
341
|
-
}
|
|
342
|
-
})();
|
|
343
|
-
|
|
344
|
-
// Subscribe to send-SMS requests from hosts
|
|
345
|
-
(async () => {
|
|
346
|
-
try {
|
|
347
|
-
const conn = await getNatsConnection();
|
|
348
|
-
const sub = conn.subscribe("host.*.fcm.sms");
|
|
349
|
-
console.log("Listening for FCM SMS requests");
|
|
350
|
-
|
|
351
|
-
for await (const msg of sub) {
|
|
352
|
-
try {
|
|
353
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
354
|
-
hostId: string;
|
|
355
|
-
requestId: string;
|
|
356
|
-
action: string;
|
|
357
|
-
to?: string;
|
|
358
|
-
body?: string;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
362
|
-
if (data.hostId !== subjectHostId) {
|
|
363
|
-
if (msg.reply) {
|
|
364
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
365
|
-
}
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const fcmPayload: Record<string, string> = {
|
|
370
|
-
type: "send-sms",
|
|
371
|
-
requestId: data.requestId,
|
|
372
|
-
hostId: data.hostId,
|
|
373
|
-
};
|
|
374
|
-
if (data.to) fcmPayload.to = data.to;
|
|
375
|
-
if (data.body) fcmPayload.body = data.body;
|
|
376
|
-
|
|
377
|
-
console.log(`[FCM] Sending SMS request for host ${data.hostId}`);
|
|
378
|
-
await sendFcmToClients(data.hostId, fcmPayload);
|
|
379
|
-
|
|
380
|
-
if (msg.reply) {
|
|
381
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
382
|
-
}
|
|
383
|
-
} catch (err) {
|
|
384
|
-
console.error("[FCM] Error handling SMS request:", err);
|
|
385
|
-
if (msg.reply) {
|
|
386
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
} catch (err) {
|
|
391
|
-
console.error("Failed to subscribe to FCM SMS requests:", err);
|
|
392
|
-
}
|
|
393
|
-
})();
|
|
394
|
-
|
|
395
|
-
// Subscribe to alarm requests from hosts
|
|
396
|
-
(async () => {
|
|
397
|
-
try {
|
|
398
|
-
const conn = await getNatsConnection();
|
|
399
|
-
const sub = conn.subscribe("host.*.fcm.alarm");
|
|
400
|
-
console.log("Listening for FCM alarm requests");
|
|
401
|
-
|
|
402
|
-
for await (const msg of sub) {
|
|
403
|
-
try {
|
|
404
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
405
|
-
hostId: string;
|
|
406
|
-
requestId: string;
|
|
407
|
-
fcmToken?: string;
|
|
408
|
-
title?: string;
|
|
409
|
-
description?: string;
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
413
|
-
if (data.hostId !== subjectHostId) {
|
|
414
|
-
if (msg.reply) {
|
|
415
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
416
|
-
}
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const fcmPayload: Record<string, string> = {
|
|
421
|
-
type: "send-alarm",
|
|
422
|
-
requestId: data.requestId,
|
|
423
|
-
hostId: data.hostId,
|
|
424
|
-
};
|
|
425
|
-
if (data.title) fcmPayload.title = data.title;
|
|
426
|
-
if (data.description) fcmPayload.description = data.description;
|
|
427
|
-
|
|
428
|
-
console.log(`[FCM] Sending alarm request for host ${data.hostId}`);
|
|
429
|
-
if (data.fcmToken) {
|
|
430
|
-
await sendFcmToDevice(data.fcmToken, fcmPayload);
|
|
431
|
-
} else {
|
|
432
|
-
await sendFcmToClients(data.hostId, fcmPayload);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (msg.reply) {
|
|
436
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
437
|
-
}
|
|
438
|
-
} catch (err) {
|
|
439
|
-
console.error("[FCM] Error handling alarm request:", err);
|
|
440
|
-
if (msg.reply) {
|
|
441
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
} catch (err) {
|
|
446
|
-
console.error("Failed to subscribe to FCM alarm requests:", err);
|
|
447
|
-
}
|
|
448
|
-
})();
|
|
449
|
-
|
|
450
|
-
// Subscribe to email requests from hosts
|
|
451
|
-
(async () => {
|
|
452
|
-
try {
|
|
453
|
-
const conn = await getNatsConnection();
|
|
454
|
-
const sub = conn.subscribe("host.*.fcm.email");
|
|
455
|
-
console.log("Listening for FCM email requests");
|
|
456
|
-
|
|
457
|
-
for await (const msg of sub) {
|
|
458
|
-
try {
|
|
459
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
460
|
-
hostId: string;
|
|
461
|
-
requestId: string;
|
|
462
|
-
fcmToken?: string;
|
|
463
|
-
[key: string]: string | undefined;
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
467
|
-
if (data.hostId !== subjectHostId) {
|
|
468
|
-
if (msg.reply) {
|
|
469
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
470
|
-
}
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const fcmPayload: Record<string, string> = {
|
|
475
|
-
type: "send-email",
|
|
476
|
-
requestId: data.requestId,
|
|
477
|
-
hostId: data.hostId,
|
|
478
|
-
};
|
|
479
|
-
for (const key of ["to", "subject", "body", "cc", "bcc"]) {
|
|
480
|
-
if (data[key]) fcmPayload[key] = data[key]!;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
console.log(`[FCM] Sending email request for host ${data.hostId}`);
|
|
484
|
-
if (data.fcmToken) {
|
|
485
|
-
await sendFcmToDevice(data.fcmToken, fcmPayload);
|
|
486
|
-
} else {
|
|
487
|
-
await sendFcmToClients(data.hostId, fcmPayload);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (msg.reply) {
|
|
491
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
492
|
-
}
|
|
493
|
-
} catch (err) {
|
|
494
|
-
console.error("[FCM] Error handling email request:", err);
|
|
495
|
-
if (msg.reply) {
|
|
496
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
} catch (err) {
|
|
501
|
-
console.error("Failed to subscribe to FCM email requests:", err);
|
|
502
|
-
}
|
|
503
|
-
})();
|
|
504
|
-
|
|
505
|
-
// Subscribe to battery requests from hosts
|
|
506
|
-
(async () => {
|
|
507
|
-
try {
|
|
508
|
-
const conn = await getNatsConnection();
|
|
509
|
-
const sub = conn.subscribe("host.*.fcm.battery");
|
|
510
|
-
console.log("Listening for FCM battery requests");
|
|
511
|
-
|
|
512
|
-
for await (const msg of sub) {
|
|
513
|
-
try {
|
|
514
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
515
|
-
hostId: string;
|
|
516
|
-
requestId: string;
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
520
|
-
if (data.hostId !== subjectHostId) {
|
|
521
|
-
if (msg.reply) {
|
|
522
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
523
|
-
}
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
console.log(`[FCM] Sending battery request for host ${data.hostId}`);
|
|
528
|
-
await sendFcmToClients(data.hostId, {
|
|
529
|
-
type: "read-battery",
|
|
530
|
-
requestId: data.requestId,
|
|
531
|
-
hostId: data.hostId,
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
if (msg.reply) {
|
|
535
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
536
|
-
}
|
|
537
|
-
} catch (err) {
|
|
538
|
-
console.error("[FCM] Error handling battery request:", err);
|
|
539
|
-
if (msg.reply) {
|
|
540
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
} catch (err) {
|
|
545
|
-
console.error("Failed to subscribe to FCM battery requests:", err);
|
|
546
|
-
}
|
|
547
|
-
})();
|
|
548
|
-
|
|
549
|
-
// Subscribe to ringer mode requests from hosts
|
|
550
|
-
(async () => {
|
|
551
|
-
try {
|
|
552
|
-
const conn = await getNatsConnection();
|
|
553
|
-
const sub = conn.subscribe("host.*.fcm.ringer");
|
|
554
|
-
console.log("Listening for FCM ringer requests");
|
|
555
|
-
|
|
556
|
-
for await (const msg of sub) {
|
|
557
|
-
try {
|
|
558
|
-
const data = JSON.parse(sc.decode(msg.data)) as {
|
|
559
|
-
hostId: string;
|
|
560
|
-
requestId: string;
|
|
561
|
-
mode: string;
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
const subjectHostId = msg.subject.split(".")[1];
|
|
565
|
-
if (data.hostId !== subjectHostId) {
|
|
566
|
-
if (msg.reply) {
|
|
567
|
-
msg.respond(sc.encode(JSON.stringify({ error: "hostId mismatch" })));
|
|
568
|
-
}
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
console.log(`[FCM] Sending ringer mode request for host ${data.hostId}`);
|
|
573
|
-
await sendFcmToClients(data.hostId, {
|
|
574
|
-
type: "set-ringer-mode",
|
|
575
|
-
requestId: data.requestId,
|
|
576
|
-
hostId: data.hostId,
|
|
577
|
-
mode: data.mode,
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
if (msg.reply) {
|
|
581
|
-
msg.respond(sc.encode(JSON.stringify({ ok: true })));
|
|
582
|
-
}
|
|
583
|
-
} catch (err) {
|
|
584
|
-
console.error("[FCM] Error handling ringer request:", err);
|
|
585
|
-
if (msg.reply) {
|
|
586
|
-
msg.respond(sc.encode(JSON.stringify({ error: String(err) })));
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
} catch (err) {
|
|
591
|
-
console.error("Failed to subscribe to FCM ringer requests:", err);
|
|
592
|
-
}
|
|
593
|
-
})();
|
|
594
|
-
|
|
595
|
-
// Create Express app
|
|
596
|
-
const app = express();
|
|
597
|
-
|
|
598
|
-
app.use(
|
|
599
|
-
helmet({
|
|
600
|
-
contentSecurityPolicy: false,
|
|
601
|
-
crossOriginResourcePolicy: false,
|
|
602
|
-
})
|
|
603
|
-
);
|
|
604
|
-
|
|
605
|
-
// CORS for Capacitor Android app (requests from capacitor://localhost)
|
|
606
|
-
app.use((req, res, next) => {
|
|
607
|
-
const origin = req.headers.origin;
|
|
608
|
-
if (origin && (origin.startsWith("capacitor://") || origin === "https://localhost" || origin.startsWith("http://localhost"))) {
|
|
609
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
610
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
|
|
611
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
612
|
-
}
|
|
613
|
-
if (req.method === "OPTIONS") {
|
|
614
|
-
res.sendStatus(204);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
next();
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
app.use(express.json());
|
|
621
|
-
|
|
622
|
-
// Mount routes
|
|
623
|
-
app.use("/api/hosts", hostsRoutes);
|
|
624
|
-
app.use("/api/push", pushRoutes);
|
|
625
|
-
app.use("/api/fcm", fcmRoutes);
|
|
626
|
-
app.use("/api/device", deviceRoutes);
|
|
627
|
-
|
|
628
|
-
// Public NATS config endpoint — returns pairing-only credentials.
|
|
629
|
-
// These can only publish to pair.* subjects (no RPC, no event subscriptions).
|
|
630
|
-
app.get("/api/config", (_req, res) => {
|
|
631
|
-
const accountSeed = process.env.NATS_ACCOUNT_SEED;
|
|
632
|
-
if (!accountSeed) {
|
|
633
|
-
res.status(500).json({ error: "Server NATS auth not configured" });
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
const creds = createPairingCredentials(accountSeed);
|
|
637
|
-
res.json({
|
|
638
|
-
natsWsUrl: process.env.NATS_WS_URL || "",
|
|
639
|
-
natsJwt: creds.jwt,
|
|
640
|
-
natsNkeySeed: creds.nkeySeed,
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
// Host-scoped NATS credentials — returns JWT scoped to a single host's subjects.
|
|
645
|
-
// Called by the PWA after pairing to get credentials for RPC + event subscriptions.
|
|
646
|
-
app.get("/api/nats-credentials/:hostId", (req, res) => {
|
|
647
|
-
const accountSeed = process.env.NATS_ACCOUNT_SEED;
|
|
648
|
-
if (!accountSeed) {
|
|
649
|
-
res.status(500).json({ error: "Server NATS auth not configured" });
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
const { hostId } = req.params;
|
|
653
|
-
const creds = createPwaCredentials(accountSeed, hostId);
|
|
654
|
-
res.json({
|
|
655
|
-
natsWsUrl: process.env.NATS_WS_URL || "",
|
|
656
|
-
natsJwt: creds.jwt,
|
|
657
|
-
natsNkeySeed: creds.nkeySeed,
|
|
658
|
-
});
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// Health check
|
|
662
|
-
app.get("/health", async (_req, res) => {
|
|
663
|
-
try {
|
|
664
|
-
await pool.query("SELECT 1");
|
|
665
|
-
res.json({ status: "ok" });
|
|
666
|
-
} catch {
|
|
667
|
-
res.status(503).json({ status: "unhealthy" });
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
// Serve built PWA static files (production only)
|
|
672
|
-
const pwaDistPath = path.resolve(process.cwd(), "../pwa/dist");
|
|
673
|
-
if (fs.existsSync(pwaDistPath)) {
|
|
674
|
-
app.use(express.static(pwaDistPath));
|
|
675
|
-
app.get("*", (_req, res) => {
|
|
676
|
-
res.sendFile(path.join(pwaDistPath, "index.html"));
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
app.listen(PORT, () => {
|
|
681
|
-
console.log(`Palmier server listening on port ${PORT}`);
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
main().catch((err) => {
|
|
686
|
-
console.error("Failed to start server:", err);
|
|
687
|
-
process.exit(1);
|
|
688
|
-
});
|