@kwirthmagnify/kwirth-sender-timed 0.1.0

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.
Files changed (3) hide show
  1. package/back.js +193 -0
  2. package/front.js +401 -0
  3. package/package.json +7 -0
package/back.js ADDED
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/back/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ TimedSender: () => TimedSender,
24
+ default: () => index_default
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+ var TIMEZONES = [
28
+ "UTC",
29
+ // Europe
30
+ "Europe/London",
31
+ "Europe/Dublin",
32
+ "Europe/Lisbon",
33
+ "Europe/Madrid",
34
+ "Europe/Paris",
35
+ "Europe/Berlin",
36
+ "Europe/Rome",
37
+ "Europe/Amsterdam",
38
+ "Europe/Brussels",
39
+ "Europe/Vienna",
40
+ "Europe/Zurich",
41
+ "Europe/Stockholm",
42
+ "Europe/Oslo",
43
+ "Europe/Copenhagen",
44
+ "Europe/Helsinki",
45
+ "Europe/Warsaw",
46
+ "Europe/Prague",
47
+ "Europe/Budapest",
48
+ "Europe/Bucharest",
49
+ "Europe/Athens",
50
+ "Europe/Istanbul",
51
+ "Europe/Kiev",
52
+ "Europe/Moscow",
53
+ // Americas
54
+ "America/New_York",
55
+ "America/Toronto",
56
+ "America/Montreal",
57
+ "America/Chicago",
58
+ "America/Winnipeg",
59
+ "America/Denver",
60
+ "America/Edmonton",
61
+ "America/Los_Angeles",
62
+ "America/Vancouver",
63
+ "America/Phoenix",
64
+ "America/Anchorage",
65
+ "America/Honolulu",
66
+ "America/Mexico_City",
67
+ "America/Bogota",
68
+ "America/Lima",
69
+ "America/Santiago",
70
+ "America/Buenos_Aires",
71
+ "America/Sao_Paulo",
72
+ "America/Caracas",
73
+ "America/Halifax",
74
+ "America/St_Johns",
75
+ // Asia
76
+ "Asia/Jerusalem",
77
+ "Asia/Beirut",
78
+ "Asia/Riyadh",
79
+ "Asia/Dubai",
80
+ "Asia/Tehran",
81
+ "Asia/Karachi",
82
+ "Asia/Kolkata",
83
+ "Asia/Colombo",
84
+ "Asia/Dhaka",
85
+ "Asia/Bangkok",
86
+ "Asia/Jakarta",
87
+ "Asia/Singapore",
88
+ "Asia/Kuala_Lumpur",
89
+ "Asia/Hong_Kong",
90
+ "Asia/Shanghai",
91
+ "Asia/Taipei",
92
+ "Asia/Seoul",
93
+ "Asia/Tokyo",
94
+ // Africa
95
+ "Africa/Casablanca",
96
+ "Africa/Lagos",
97
+ "Africa/Nairobi",
98
+ "Africa/Johannesburg",
99
+ "Africa/Cairo",
100
+ // Pacific / Australia
101
+ "Australia/Perth",
102
+ "Australia/Adelaide",
103
+ "Australia/Darwin",
104
+ "Australia/Brisbane",
105
+ "Australia/Sydney",
106
+ "Australia/Melbourne",
107
+ "Pacific/Auckland",
108
+ "Pacific/Fiji",
109
+ "Pacific/Honolulu"
110
+ ];
111
+ function tzOffset(tz) {
112
+ try {
113
+ const part = new Intl.DateTimeFormat("en", { timeZone: tz, timeZoneName: "shortOffset" }).formatToParts(/* @__PURE__ */ new Date()).find((p) => p.type === "timeZoneName")?.value ?? "";
114
+ return part.replace("GMT", "UTC");
115
+ } catch {
116
+ return "";
117
+ }
118
+ }
119
+ function parseMinutes(hhmm) {
120
+ const [h, m] = hhmm.split(":").map(Number);
121
+ return (h ?? 0) * 60 + (m ?? 0);
122
+ }
123
+ function currentContext(timezone) {
124
+ const now = timezone ? new Date((/* @__PURE__ */ new Date()).toLocaleString("en-US", { timeZone: timezone })) : /* @__PURE__ */ new Date();
125
+ return { minutes: now.getHours() * 60 + now.getMinutes(), day: now.getDay() };
126
+ }
127
+ function matchesWindow(rule, minutes, day) {
128
+ if (rule.days && rule.days.length > 0 && !rule.days.includes(day)) return false;
129
+ const from = parseMinutes(rule.from);
130
+ const to = parseMinutes(rule.to);
131
+ return from <= to ? minutes >= from && minutes < to : minutes >= from || minutes < to;
132
+ }
133
+ var TimedSender = class {
134
+ constructor() {
135
+ this.id = "timed";
136
+ this.configs = /* @__PURE__ */ new Map();
137
+ }
138
+ addConfig(config) {
139
+ const tc = config;
140
+ if (!Array.isArray(tc.rules)) tc.rules = [];
141
+ this.configs.set(tc.name, tc);
142
+ }
143
+ removeConfig(name) {
144
+ this.configs.delete(name);
145
+ }
146
+ hasConfig(name) {
147
+ return this.configs.has(name);
148
+ }
149
+ getConfigNames() {
150
+ return Array.from(this.configs.keys());
151
+ }
152
+ async send(configName, message) {
153
+ const config = this.configs.get(configName);
154
+ if (!config) throw new Error(`TimedSender: config '${configName}' not found`);
155
+ if (!this.senderAccess) throw new Error(`TimedSender: senderAccess not initialized`);
156
+ const { minutes, day } = currentContext(config.timezone);
157
+ for (const rule of config.rules) {
158
+ if (!matchesWindow(rule, minutes, day)) continue;
159
+ if (rule.action === "send" && rule.senderId && rule.configName) {
160
+ await this.senderAccess.send(rule.senderId, rule.configName, message);
161
+ }
162
+ return;
163
+ }
164
+ const defAction = config.defaultAction ?? "drop";
165
+ if (defAction === "send" && config.defaultSenderId && config.defaultConfigName) {
166
+ await this.senderAccess.send(config.defaultSenderId, config.defaultConfigName, message);
167
+ }
168
+ }
169
+ getConfigSchema() {
170
+ return [
171
+ { name: "name", label: "Name", required: true },
172
+ { name: "timezone", label: "Timezone", type: "select", options: TIMEZONES, labels: TIMEZONES.map((tz) => {
173
+ const off = tzOffset(tz);
174
+ return off ? `${tz} (${off})` : tz;
175
+ }) },
176
+ { name: "rules", label: "Rules (JSON)", type: "json" },
177
+ { name: "defaultAction", label: "Default action", type: "select", options: ["drop", "send"] },
178
+ { name: "defaultSenderId", label: "Default sender ID" },
179
+ { name: "defaultConfigName", label: "Default config name" }
180
+ ];
181
+ }
182
+ async startSender(senders) {
183
+ this.senderAccess = senders;
184
+ }
185
+ async stopSender() {
186
+ this.senderAccess = void 0;
187
+ }
188
+ };
189
+ var index_default = TimedSender;
190
+ // Annotate the CommonJS export names for ESM import in node:
191
+ 0 && (module.exports = {
192
+ TimedSender
193
+ });
package/front.js ADDED
@@ -0,0 +1,401 @@
1
+ "use strict";
2
+ (() => {
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+
29
+ // kwirth-globals:react
30
+ var require_react = __commonJS({
31
+ "kwirth-globals:react"(exports, module) {
32
+ module.exports = window.__kwirth__.React;
33
+ }
34
+ });
35
+
36
+ // kwirth-globals:@mui/material
37
+ var require_material = __commonJS({
38
+ "kwirth-globals:@mui/material"(exports, module) {
39
+ module.exports = window.__kwirth__.MUI.material;
40
+ }
41
+ });
42
+
43
+ // kwirth-globals:@mui/icons-material
44
+ var require_icons_material = __commonJS({
45
+ "kwirth-globals:@mui/icons-material"(exports, module) {
46
+ module.exports = window.__kwirth__.MUI.icons;
47
+ }
48
+ });
49
+
50
+ // src/front/TimedConfigDialog.tsx
51
+ var import_react = __toESM(require_react(), 1);
52
+ var import_material = __toESM(require_material(), 1);
53
+ var import_icons_material = __toESM(require_icons_material(), 1);
54
+ function authGet(accessString) {
55
+ return { headers: { Authorization: accessString ? `Bearer ${accessString}` : "", "X-Kwirth-App": "true" } };
56
+ }
57
+ function authDelete(accessString) {
58
+ return { method: "DELETE", headers: { Authorization: accessString ? `Bearer ${accessString}` : "", "X-Kwirth-App": "true" } };
59
+ }
60
+ function authPost(accessString, body) {
61
+ return {
62
+ method: "POST",
63
+ body,
64
+ headers: { Authorization: accessString ? `Bearer ${accessString}` : "", "X-Kwirth-App": "true", "Content-Type": "application/json" }
65
+ };
66
+ }
67
+ var TIMEZONES = [
68
+ "UTC",
69
+ "Europe/London",
70
+ "Europe/Dublin",
71
+ "Europe/Lisbon",
72
+ "Europe/Madrid",
73
+ "Europe/Paris",
74
+ "Europe/Berlin",
75
+ "Europe/Rome",
76
+ "Europe/Amsterdam",
77
+ "Europe/Brussels",
78
+ "Europe/Vienna",
79
+ "Europe/Zurich",
80
+ "Europe/Stockholm",
81
+ "Europe/Oslo",
82
+ "Europe/Copenhagen",
83
+ "Europe/Helsinki",
84
+ "Europe/Warsaw",
85
+ "Europe/Prague",
86
+ "Europe/Budapest",
87
+ "Europe/Bucharest",
88
+ "Europe/Athens",
89
+ "Europe/Istanbul",
90
+ "Europe/Kiev",
91
+ "Europe/Moscow",
92
+ "America/New_York",
93
+ "America/Toronto",
94
+ "America/Montreal",
95
+ "America/Chicago",
96
+ "America/Winnipeg",
97
+ "America/Denver",
98
+ "America/Edmonton",
99
+ "America/Los_Angeles",
100
+ "America/Vancouver",
101
+ "America/Phoenix",
102
+ "America/Anchorage",
103
+ "America/Honolulu",
104
+ "America/Mexico_City",
105
+ "America/Bogota",
106
+ "America/Lima",
107
+ "America/Santiago",
108
+ "America/Buenos_Aires",
109
+ "America/Sao_Paulo",
110
+ "America/Caracas",
111
+ "America/Halifax",
112
+ "America/St_Johns",
113
+ "Asia/Jerusalem",
114
+ "Asia/Beirut",
115
+ "Asia/Riyadh",
116
+ "Asia/Dubai",
117
+ "Asia/Tehran",
118
+ "Asia/Karachi",
119
+ "Asia/Kolkata",
120
+ "Asia/Colombo",
121
+ "Asia/Dhaka",
122
+ "Asia/Bangkok",
123
+ "Asia/Jakarta",
124
+ "Asia/Singapore",
125
+ "Asia/Kuala_Lumpur",
126
+ "Asia/Hong_Kong",
127
+ "Asia/Shanghai",
128
+ "Asia/Taipei",
129
+ "Asia/Seoul",
130
+ "Asia/Tokyo",
131
+ "Africa/Casablanca",
132
+ "Africa/Lagos",
133
+ "Africa/Nairobi",
134
+ "Africa/Johannesburg",
135
+ "Africa/Cairo",
136
+ "Australia/Perth",
137
+ "Australia/Adelaide",
138
+ "Australia/Darwin",
139
+ "Australia/Brisbane",
140
+ "Australia/Sydney",
141
+ "Australia/Melbourne",
142
+ "Pacific/Auckland",
143
+ "Pacific/Fiji",
144
+ "Pacific/Honolulu"
145
+ ];
146
+ function tzOffset(tz) {
147
+ try {
148
+ const part = new Intl.DateTimeFormat("en", { timeZone: tz, timeZoneName: "shortOffset" }).formatToParts(/* @__PURE__ */ new Date()).find((p) => p.type === "timeZoneName")?.value ?? "";
149
+ return part.replace("GMT", "UTC");
150
+ } catch {
151
+ return "";
152
+ }
153
+ }
154
+ var DAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
155
+ function emptyRule() {
156
+ return { from: "09:00", to: "18:00", action: "send" };
157
+ }
158
+ function emptyConfig() {
159
+ return { name: "", rules: [], defaultAction: "drop" };
160
+ }
161
+ var RuleRow = ({ rule, senders, onChange, onDelete }) => {
162
+ const linkedSender = senders.find((s) => s.id === rule.senderId);
163
+ const configNames = linkedSender?.configNames ?? [];
164
+ const toggleDay = (day) => {
165
+ const days = rule.days ?? [];
166
+ const next = days.includes(day) ? days.filter((d) => d !== day) : [...days, day].sort();
167
+ onChange({ ...rule, days: next.length === 0 ? void 0 : next });
168
+ };
169
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { border: "1px solid", borderColor: "divider", borderRadius: 1, p: 1.5, mb: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", useFlexGap: true }, /* @__PURE__ */ import_react.default.createElement(
170
+ import_material.TextField,
171
+ {
172
+ size: "small",
173
+ label: "From",
174
+ type: "time",
175
+ sx: { width: 110 },
176
+ value: rule.from,
177
+ onChange: (e) => onChange({ ...rule, from: e.target.value }),
178
+ InputLabelProps: { shrink: true }
179
+ }
180
+ ), /* @__PURE__ */ import_react.default.createElement(
181
+ import_material.TextField,
182
+ {
183
+ size: "small",
184
+ label: "To",
185
+ type: "time",
186
+ sx: { width: 110 },
187
+ value: rule.to,
188
+ onChange: (e) => onChange({ ...rule, to: e.target.value }),
189
+ InputLabelProps: { shrink: true }
190
+ }
191
+ ), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 90 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Action"), /* @__PURE__ */ import_react.default.createElement(
192
+ import_material.Select,
193
+ {
194
+ label: "Action",
195
+ value: rule.action,
196
+ onChange: (e) => {
197
+ const action = e.target.value;
198
+ onChange({ ...rule, action, senderId: action === "drop" ? void 0 : rule.senderId, configName: action === "drop" ? void 0 : rule.configName });
199
+ }
200
+ },
201
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, "send"),
202
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, "drop")
203
+ )), rule.action === "send" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 130 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Sender"), /* @__PURE__ */ import_react.default.createElement(
204
+ import_material.Select,
205
+ {
206
+ label: "Sender",
207
+ value: rule.senderId ?? "",
208
+ onChange: (e) => onChange({ ...rule, senderId: e.target.value || void 0, configName: void 0 })
209
+ },
210
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
211
+ senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName ?? s.id))
212
+ )), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 130 }, disabled: !rule.senderId || configNames.length === 0 }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Config"), /* @__PURE__ */ import_react.default.createElement(
213
+ import_material.Select,
214
+ {
215
+ label: "Config",
216
+ value: rule.configName ?? "",
217
+ onChange: (e) => onChange({ ...rule, configName: e.target.value || void 0 })
218
+ },
219
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
220
+ configNames.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c))
221
+ ))), /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", onClick: onDelete }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { fontSize: "small" }))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 0, sx: { mt: 1 } }, DAY_LABELS.map((label, i) => /* @__PURE__ */ import_react.default.createElement(
222
+ import_material.FormControlLabel,
223
+ {
224
+ key: i,
225
+ control: /* @__PURE__ */ import_react.default.createElement(
226
+ import_material.Checkbox,
227
+ {
228
+ size: "small",
229
+ checked: !rule.days || rule.days.includes(i),
230
+ onChange: () => toggleDay(i),
231
+ sx: { p: 0.25 }
232
+ }
233
+ ),
234
+ label: /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, label),
235
+ sx: { mr: 0.5 }
236
+ }
237
+ )), rule.days && rule.days.length > 0 && /* @__PURE__ */ import_react.default.createElement(
238
+ import_material.Button,
239
+ {
240
+ size: "small",
241
+ sx: { fontSize: 10, p: 0.25, minWidth: 0 },
242
+ onClick: () => onChange({ ...rule, days: void 0 })
243
+ },
244
+ "all"
245
+ )));
246
+ };
247
+ var ConfigForm = ({ config, senders, onChange }) => {
248
+ const defSender = senders.find((s) => s.id === config.defaultSenderId);
249
+ const defConfigNames = defSender?.configNames ?? [];
250
+ const updateRule = (i, rule) => {
251
+ const rules = [...config.rules];
252
+ rules[i] = rule;
253
+ onChange({ ...config, rules });
254
+ };
255
+ const deleteRule = (i) => {
256
+ onChange({ ...config, rules: config.rules.filter((_, idx) => idx !== i) });
257
+ };
258
+ const addRule = () => {
259
+ onChange({ ...config, rules: [...config.rules, emptyRule()] });
260
+ };
261
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { spacing: 2 }, /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1.5, flexWrap: "wrap", useFlexGap: true }, /* @__PURE__ */ import_react.default.createElement(
262
+ import_material.TextField,
263
+ {
264
+ size: "small",
265
+ label: "Name *",
266
+ value: config.name,
267
+ onChange: (e) => onChange({ ...config, name: e.target.value }),
268
+ sx: { flex: 1, minWidth: 180 }
269
+ }
270
+ ), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 240 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Timezone"), /* @__PURE__ */ import_react.default.createElement(
271
+ import_material.Select,
272
+ {
273
+ label: "Timezone",
274
+ value: config.timezone ?? "",
275
+ onChange: (e) => onChange({ ...config, timezone: e.target.value || void 0 })
276
+ },
277
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "Server local")),
278
+ TIMEZONES.map((tz) => {
279
+ const off = tzOffset(tz);
280
+ return /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: tz, value: tz }, tz, off ? ` (${off})` : "");
281
+ })
282
+ ))), /* @__PURE__ */ import_react.default.createElement(import_material.Divider, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, "Rules (evaluated in order \u2014 first match wins)")), config.rules.length === 0 && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary" }, "No rules yet. Add one below."), config.rules.map((rule, i) => /* @__PURE__ */ import_react.default.createElement(
283
+ RuleRow,
284
+ {
285
+ key: i,
286
+ rule,
287
+ senders,
288
+ onChange: (r) => updateRule(i, r),
289
+ onDelete: () => deleteRule(i)
290
+ }
291
+ )), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null), onClick: addRule, sx: { alignSelf: "flex-start" } }, "Add rule"), /* @__PURE__ */ import_react.default.createElement(import_material.Divider, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption" }, "Default (when no rule matches)")), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1.5, flexWrap: "wrap", useFlexGap: true, alignItems: "center" }, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 110 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default action"), /* @__PURE__ */ import_react.default.createElement(
292
+ import_material.Select,
293
+ {
294
+ label: "Default action",
295
+ value: config.defaultAction ?? "drop",
296
+ onChange: (e) => {
297
+ const action = e.target.value;
298
+ onChange({ ...config, defaultAction: action, defaultSenderId: action === "drop" ? void 0 : config.defaultSenderId, defaultConfigName: action === "drop" ? void 0 : config.defaultConfigName });
299
+ }
300
+ },
301
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "drop" }, "drop"),
302
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "send" }, "send")
303
+ )), config.defaultAction === "send" && /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 160 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default sender"), /* @__PURE__ */ import_react.default.createElement(
304
+ import_material.Select,
305
+ {
306
+ label: "Default sender",
307
+ value: config.defaultSenderId ?? "",
308
+ onChange: (e) => onChange({ ...config, defaultSenderId: e.target.value || void 0, defaultConfigName: void 0 })
309
+ },
310
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
311
+ senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName ?? s.id))
312
+ )), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { minWidth: 160 }, disabled: !config.defaultSenderId || defConfigNames.length === 0 }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Default config"), /* @__PURE__ */ import_react.default.createElement(
313
+ import_material.Select,
314
+ {
315
+ label: "Default config",
316
+ value: config.defaultConfigName ?? "",
317
+ onChange: (e) => onChange({ ...config, defaultConfigName: e.target.value || void 0 })
318
+ },
319
+ /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { value: "" }, /* @__PURE__ */ import_react.default.createElement("em", null, "\u2014")),
320
+ defConfigNames.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c))
321
+ )))));
322
+ };
323
+ var TimedConfigDialog = ({ onClose, backendUrl, accessString }) => {
324
+ const [configs, setConfigs] = (0, import_react.useState)([]);
325
+ const [senders, setSenders] = (0, import_react.useState)([]);
326
+ const [loading, setLoading] = (0, import_react.useState)(true);
327
+ const [saving, setSaving] = (0, import_react.useState)(false);
328
+ const [deletingName, setDeletingName] = (0, import_react.useState)();
329
+ const [showForm, setShowForm] = (0, import_react.useState)(false);
330
+ const [formConfig, setFormConfig] = (0, import_react.useState)(emptyConfig());
331
+ const [error, setError] = (0, import_react.useState)();
332
+ (0, import_react.useEffect)(() => {
333
+ Promise.all([
334
+ fetch(`${backendUrl}/senders/timed/configs`, authGet(accessString)).then((r) => r.json()).then(setConfigs).catch(() => {
335
+ }),
336
+ fetch(`${backendUrl}/senders`, authGet(accessString)).then((r) => r.json()).then(setSenders).catch(() => {
337
+ })
338
+ ]).finally(() => setLoading(false));
339
+ }, []);
340
+ const saveConfig = async () => {
341
+ if (!formConfig.name.trim()) {
342
+ setError("Name is required");
343
+ return;
344
+ }
345
+ setSaving(true);
346
+ setError(void 0);
347
+ try {
348
+ const res = await fetch(`${backendUrl}/senders/timed/configs`, authPost(accessString, JSON.stringify(formConfig)));
349
+ if (!res.ok) throw new Error((await res.json()).error ?? `HTTP ${res.status}`);
350
+ setConfigs((prev) => {
351
+ const idx = prev.findIndex((c) => c.name === formConfig.name);
352
+ return idx >= 0 ? prev.map((c, i) => i === idx ? formConfig : c) : [...prev, formConfig];
353
+ });
354
+ setShowForm(false);
355
+ setFormConfig(emptyConfig());
356
+ } catch (err) {
357
+ setError(`Save failed: ${err}`);
358
+ } finally {
359
+ setSaving(false);
360
+ }
361
+ };
362
+ const deleteConfig = async (name) => {
363
+ setDeletingName(name);
364
+ try {
365
+ const res = await fetch(`${backendUrl}/senders/timed/configs/${encodeURIComponent(name)}`, authDelete(accessString));
366
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
367
+ setConfigs((prev) => prev.filter((c) => c.name !== name));
368
+ } catch (err) {
369
+ setError(`Delete failed: ${err}`);
370
+ } finally {
371
+ setDeletingName(void 0);
372
+ }
373
+ };
374
+ const startAdd = () => {
375
+ setFormConfig(emptyConfig());
376
+ setError(void 0);
377
+ setShowForm(true);
378
+ };
379
+ const startEdit = (config) => {
380
+ setFormConfig({ ...config });
381
+ setError(void 0);
382
+ setShowForm(true);
383
+ };
384
+ const configSummary = (c) => {
385
+ const parts = [];
386
+ if (c.timezone) parts.push(c.timezone);
387
+ if (c.rules.length > 0) parts.push(`${c.rules.length} rule(s)`);
388
+ parts.push(`default: ${c.defaultAction ?? "drop"}`);
389
+ return parts.join(" \xB7 ");
390
+ };
391
+ return /* @__PURE__ */ import_react.default.createElement(import_material.Dialog, { open: true, maxWidth: false, sx: { "& .MuiDialog-paper": { width: "760px" } } }, /* @__PURE__ */ import_react.default.createElement(import_material.DialogTitle, null, "Configure: Timed Sender"), /* @__PURE__ */ import_react.default.createElement(import_material.DialogContent, null, loading ? /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { display: "flex", justifyContent: "center", py: 4 } }, /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, null)) : /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { spacing: 1.5, sx: { mt: 1 } }, configs.length === 0 && !showForm && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary" }, "No configs yet."), configs.map((cfg) => /* @__PURE__ */ import_react.default.createElement(import_material.Box, { key: cfg.name, sx: { display: "flex", alignItems: "center", justifyContent: "space-between", px: 1.5, py: 0.75, border: "1px solid", borderColor: "divider", borderRadius: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Box, null, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", fontWeight: "bold" }, cfg.name), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary" }, configSummary(cfg))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 0.5 }, /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Edit" }, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", onClick: () => startEdit(cfg) }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, { fontSize: "small", sx: { transform: "rotate(45deg)" } }))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Delete" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", disabled: deletingName === cfg.name, onClick: () => deleteConfig(cfg.name) }, deletingName === cfg.name ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { fontSize: "small" }))))))), !showForm && /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", startIcon: /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null), onClick: startAdd, sx: { alignSelf: "flex-start" } }, "Add config"), showForm && /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { p: 2, border: "1px solid", borderColor: "divider", borderRadius: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "subtitle2", sx: { mb: 2 } }, formConfig.name && configs.some((c) => c.name === formConfig.name) ? `Edit: ${formConfig.name}` : "New config"), /* @__PURE__ */ import_react.default.createElement(ConfigForm, { config: formConfig, senders, onChange: setFormConfig }), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", spacing: 1, sx: { mt: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", onClick: () => {
392
+ setShowForm(false);
393
+ setError(void 0);
394
+ } }, "Cancel"), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !formConfig.name.trim(), onClick: saveConfig }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : "Save"))), error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error" }, error))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, { sx: { px: 2 } }, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { onClick: onClose }, "Close")));
395
+ };
396
+ var TimedConfigDialog_default = TimedConfigDialog;
397
+
398
+ // src/front/index.tsx
399
+ window.__kwirth_senders__ = window.__kwirth_senders__ ?? {};
400
+ window.__kwirth_senders__["timed"] = TimedConfigDialog_default;
401
+ })();
package/package.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "timed",
3
+ "name": "@kwirthmagnify/kwirth-sender-timed",
4
+ "displayName": "Timed Sender",
5
+ "version": "0.1.0",
6
+ "description": "Timed sender for Kwirth — routes or drops messages based on time-of-day windows and day-of-week rules"
7
+ }