@kwirthmagnify/kwirth-sender-timed 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{dist/back.js → back.js} +29 -0
- package/{dist/front.js → front.js} +5 -1
- package/package.json +7 -25
- package/README.md +0 -84
- package/build.mjs +0 -65
- package/dist/package.json +0 -7
- package/src/back/index.ts +0 -123
- package/src/front/TimedConfigDialog.tsx +0 -383
- package/src/front/index.tsx +0 -6
- package/tsconfig.json +0 -13
- package/watch.mjs +0 -68
package/{dist/back.js → back.js}
RENAMED
|
@@ -115,9 +115,24 @@ function tzOffset(tz) {
|
|
|
115
115
|
return "";
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
function parseMinutes(hhmm) {
|
|
119
|
+
const [h, m] = hhmm.split(":").map(Number);
|
|
120
|
+
return (h ?? 0) * 60 + (m ?? 0);
|
|
121
|
+
}
|
|
122
|
+
function currentContext(timezone) {
|
|
123
|
+
const now = timezone ? new Date((/* @__PURE__ */ new Date()).toLocaleString("en-US", { timeZone: timezone })) : /* @__PURE__ */ new Date();
|
|
124
|
+
return { minutes: now.getHours() * 60 + now.getMinutes(), day: now.getDay() };
|
|
125
|
+
}
|
|
126
|
+
function matchesWindow(rule, minutes, day) {
|
|
127
|
+
if (rule.days && rule.days.length > 0 && !rule.days.includes(day)) return false;
|
|
128
|
+
const from = parseMinutes(rule.from);
|
|
129
|
+
const to = parseMinutes(rule.to);
|
|
130
|
+
return from <= to ? minutes >= from && minutes < to : minutes >= from || minutes < to;
|
|
131
|
+
}
|
|
118
132
|
var TimedSender = class {
|
|
119
133
|
constructor() {
|
|
120
134
|
this.id = "timed";
|
|
135
|
+
this.senderType = "filter";
|
|
121
136
|
this.configs = /* @__PURE__ */ new Map();
|
|
122
137
|
}
|
|
123
138
|
addConfig(config) {
|
|
@@ -134,8 +149,22 @@ var TimedSender = class {
|
|
|
134
149
|
getConfigNames() {
|
|
135
150
|
return Array.from(this.configs.keys());
|
|
136
151
|
}
|
|
152
|
+
getNodeMeta() {
|
|
153
|
+
return { label: "Timed filter", icon: "AccessTime", description: "Routes or drops messages based on time-of-day windows and day-of-week rules." };
|
|
154
|
+
}
|
|
137
155
|
async send(_configName, _message) {
|
|
138
156
|
}
|
|
157
|
+
async evalFilter(configName, _message, forward) {
|
|
158
|
+
const config = this.configs.get(configName);
|
|
159
|
+
if (!config) return;
|
|
160
|
+
const { minutes, day } = currentContext(config.timezone);
|
|
161
|
+
for (const rule of config.rules ?? []) {
|
|
162
|
+
if (!matchesWindow(rule, minutes, day)) continue;
|
|
163
|
+
if (rule.action === "send") await forward();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if ((config.defaultAction ?? "drop") === "send") await forward();
|
|
167
|
+
}
|
|
139
168
|
getConfigSchema() {
|
|
140
169
|
return [
|
|
141
170
|
{ name: "name", label: "Name", required: true },
|
|
@@ -363,5 +363,9 @@
|
|
|
363
363
|
|
|
364
364
|
// src/front/index.tsx
|
|
365
365
|
window.__kwirth_senders__ = window.__kwirth_senders__ ?? {};
|
|
366
|
-
window.__kwirth_senders__["timed"] =
|
|
366
|
+
window.__kwirth_senders__["timed"] = {
|
|
367
|
+
ConfigDialog: TimedConfigDialog_default,
|
|
368
|
+
nodeLabel: "Timed filter",
|
|
369
|
+
nodeDescription: "Routes or drops messages based on time-of-day windows and day-of-week rules."
|
|
370
|
+
};
|
|
367
371
|
})();
|
package/package.json
CHANGED
|
@@ -1,25 +1,7 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "timed",
|
|
3
|
-
"name": "@kwirthmagnify/kwirth-sender-timed",
|
|
4
|
-
"
|
|
5
|
-
"version": "0.1.
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"type": "module",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "node build.mjs",
|
|
11
|
-
"watch": "node watch.mjs"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@kwirthmagnify/kwirth-common-back": "^0.5.10"
|
|
15
|
-
},
|
|
16
|
-
"devDependencies": {
|
|
17
|
-
"@mui/icons-material": "^7.1.2",
|
|
18
|
-
"@mui/material": "^7.1.2",
|
|
19
|
-
"@types/node": "^20.12.13",
|
|
20
|
-
"@types/react": "^19.2.16",
|
|
21
|
-
"@types/react-dom": "^19.2.3",
|
|
22
|
-
"esbuild": "^0.27.2",
|
|
23
|
-
"typescript": "^5.4.0"
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "timed",
|
|
3
|
+
"name": "@kwirthmagnify/kwirth-sender-timed",
|
|
4
|
+
"displayName": "Timed Sender",
|
|
5
|
+
"version": "0.1.7",
|
|
6
|
+
"description": "Timed sender for Kwirth — routes or drops messages based on time-of-day windows and day-of-week rules"
|
|
7
|
+
}
|
package/README.md
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
# sender-timed
|
|
2
|
-
|
|
3
|
-
Routes or drops messages based on **time-of-day windows** and optional **day-of-week** filters. Rules are evaluated in order — first match wins. If no rule matches, the `defaultAction` applies.
|
|
4
|
-
|
|
5
|
-
## Use cases
|
|
6
|
-
|
|
7
|
-
- Only forward alerts to on-call during business hours
|
|
8
|
-
- Drop non-critical notifications at night
|
|
9
|
-
- Route differently on weekends vs weekdays
|
|
10
|
-
|
|
11
|
-
## Config fields
|
|
12
|
-
|
|
13
|
-
| Field | Type | Required | Description |
|
|
14
|
-
|---|---|---|---|
|
|
15
|
-
| `name` | string | ✓ | Config name |
|
|
16
|
-
| `timezone` | string | | IANA timezone (e.g. `Europe/Madrid`). Defaults to server local time. |
|
|
17
|
-
| `rules` | JSON array | | Ordered list of `ITimedSenderRule` (see below) |
|
|
18
|
-
| `defaultAction` | `send`\|`drop` | | Action when no rule matches (default: `drop`) |
|
|
19
|
-
| `defaultSenderId` | string | | Sender to use for default `send` |
|
|
20
|
-
| `defaultConfigName` | string | | Config name for default `send` |
|
|
21
|
-
|
|
22
|
-
## Rule fields (`ITimedSenderRule`)
|
|
23
|
-
|
|
24
|
-
| Field | Type | Required | Description |
|
|
25
|
-
|---|---|---|---|
|
|
26
|
-
| `from` | `"HH:mm"` | ✓ | Start of window (inclusive, 24h) |
|
|
27
|
-
| `to` | `"HH:mm"` | ✓ | End of window (exclusive, 24h). If `to < from`, the window spans midnight. |
|
|
28
|
-
| `days` | `number[]` | | Days of week: 0=Sun, 1=Mon … 6=Sat. All days if omitted. |
|
|
29
|
-
| `action` | `send`\|`drop` | ✓ | What to do on match |
|
|
30
|
-
| `senderId` | string | | Required when `action === "send"` |
|
|
31
|
-
| `configName` | string | | Required when `action === "send"` |
|
|
32
|
-
|
|
33
|
-
## Examples
|
|
34
|
-
|
|
35
|
-
### Business hours only (Mon–Fri 09:00–18:00 → email, otherwise drop)
|
|
36
|
-
|
|
37
|
-
```json
|
|
38
|
-
{
|
|
39
|
-
"name": "biz-hours",
|
|
40
|
-
"timezone": "Europe/Madrid",
|
|
41
|
-
"rules": [
|
|
42
|
-
{
|
|
43
|
-
"from": "09:00", "to": "18:00",
|
|
44
|
-
"days": [1,2,3,4,5],
|
|
45
|
-
"action": "send",
|
|
46
|
-
"senderId": "email-smtp", "configName": "ops-team"
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"defaultAction": "drop"
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Night silence (22:00–08:00 → drop, rest → send)
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"name": "no-nights",
|
|
58
|
-
"rules": [
|
|
59
|
-
{ "from": "22:00", "to": "08:00", "action": "drop" }
|
|
60
|
-
],
|
|
61
|
-
"defaultAction": "send",
|
|
62
|
-
"defaultSenderId": "console", "defaultConfigName": "default"
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Weekend routing (Sat+Sun → Slack, weekdays → email)
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"name": "weekend-routing",
|
|
71
|
-
"rules": [
|
|
72
|
-
{
|
|
73
|
-
"from": "00:00", "to": "00:00",
|
|
74
|
-
"days": [0, 6],
|
|
75
|
-
"action": "send",
|
|
76
|
-
"senderId": "slack", "configName": "oncall"
|
|
77
|
-
}
|
|
78
|
-
],
|
|
79
|
-
"defaultAction": "send",
|
|
80
|
-
"defaultSenderId": "email-smtp", "defaultConfigName": "team"
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
> **Note:** A window where `from === to` (e.g. `"00:00"` to `"00:00"`) matches **all times** of the specified days.
|
package/build.mjs
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import esbuild from 'esbuild'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
|
|
5
|
-
const kwirthGlobalsPlugin = {
|
|
6
|
-
name: 'kwirth-globals',
|
|
7
|
-
setup(build) {
|
|
8
|
-
const globals = {
|
|
9
|
-
'react': 'window.__kwirth__.React',
|
|
10
|
-
'@mui/material': 'window.__kwirth__.MUI.material',
|
|
11
|
-
'@mui/icons-material': 'window.__kwirth__.MUI.icons',
|
|
12
|
-
'@kwirthmagnify/kwirth-common': 'window.__kwirth__.kwirthCommon',
|
|
13
|
-
}
|
|
14
|
-
for (const pkg of Object.keys(globals)) {
|
|
15
|
-
build.onResolve({ filter: new RegExp(`^${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) }, () => ({
|
|
16
|
-
path: pkg,
|
|
17
|
-
namespace: 'kwirth-globals',
|
|
18
|
-
}))
|
|
19
|
-
}
|
|
20
|
-
build.onLoad({ filter: /.*/, namespace: 'kwirth-globals' }, (args) => ({
|
|
21
|
-
contents: `module.exports = ${globals[args.path]}`,
|
|
22
|
-
loader: 'js',
|
|
23
|
-
}))
|
|
24
|
-
},
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
fs.mkdirSync('dist', { recursive: true })
|
|
28
|
-
|
|
29
|
-
await esbuild.build({
|
|
30
|
-
entryPoints: ['src/back/index.ts'],
|
|
31
|
-
bundle: true,
|
|
32
|
-
format: 'cjs',
|
|
33
|
-
platform: 'node',
|
|
34
|
-
target: 'node20',
|
|
35
|
-
outfile: 'dist/back.js',
|
|
36
|
-
loader: { '.ts': 'ts' },
|
|
37
|
-
minify: false,
|
|
38
|
-
})
|
|
39
|
-
console.log('Built dist/back.js')
|
|
40
|
-
|
|
41
|
-
await esbuild.build({
|
|
42
|
-
entryPoints: ['src/front/index.tsx'],
|
|
43
|
-
bundle: true,
|
|
44
|
-
format: 'iife',
|
|
45
|
-
outfile: 'dist/front.js',
|
|
46
|
-
plugins: [kwirthGlobalsPlugin],
|
|
47
|
-
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
48
|
-
jsx: 'transform',
|
|
49
|
-
jsxFactory: 'React.createElement',
|
|
50
|
-
jsxFragment: 'React.Fragment',
|
|
51
|
-
target: 'es2020',
|
|
52
|
-
minify: false,
|
|
53
|
-
})
|
|
54
|
-
console.log('Built dist/front.js')
|
|
55
|
-
|
|
56
|
-
const meta = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
|
57
|
-
const distMeta = {
|
|
58
|
-
id: meta.id,
|
|
59
|
-
name: meta.name,
|
|
60
|
-
displayName: meta.displayName,
|
|
61
|
-
version: meta.version,
|
|
62
|
-
description: meta.description,
|
|
63
|
-
}
|
|
64
|
-
fs.writeFileSync(path.join('dist', 'package.json'), JSON.stringify(distMeta, null, 2))
|
|
65
|
-
console.log('Wrote dist/package.json')
|
package/dist/package.json
DELETED
package/src/back/index.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { ISender, ISenderAccess, ISenderConfig, ISenderFieldDef, ISenderMessage } from '@kwirthmagnify/kwirth-common-back'
|
|
2
|
-
|
|
3
|
-
const TIMEZONES = [
|
|
4
|
-
'UTC',
|
|
5
|
-
// Europe
|
|
6
|
-
'Europe/London', 'Europe/Dublin', 'Europe/Lisbon',
|
|
7
|
-
'Europe/Madrid', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Amsterdam',
|
|
8
|
-
'Europe/Brussels', 'Europe/Vienna', 'Europe/Zurich', 'Europe/Stockholm', 'Europe/Oslo',
|
|
9
|
-
'Europe/Copenhagen', 'Europe/Helsinki', 'Europe/Warsaw', 'Europe/Prague', 'Europe/Budapest',
|
|
10
|
-
'Europe/Bucharest', 'Europe/Athens', 'Europe/Istanbul', 'Europe/Kiev', 'Europe/Moscow',
|
|
11
|
-
// Americas
|
|
12
|
-
'America/New_York', 'America/Toronto', 'America/Montreal',
|
|
13
|
-
'America/Chicago', 'America/Winnipeg',
|
|
14
|
-
'America/Denver', 'America/Edmonton',
|
|
15
|
-
'America/Los_Angeles', 'America/Vancouver',
|
|
16
|
-
'America/Phoenix', 'America/Anchorage', 'America/Honolulu',
|
|
17
|
-
'America/Mexico_City', 'America/Bogota', 'America/Lima',
|
|
18
|
-
'America/Santiago', 'America/Buenos_Aires', 'America/Sao_Paulo',
|
|
19
|
-
'America/Caracas', 'America/Halifax', 'America/St_Johns',
|
|
20
|
-
// Asia
|
|
21
|
-
'Asia/Jerusalem', 'Asia/Beirut', 'Asia/Riyadh', 'Asia/Dubai', 'Asia/Tehran',
|
|
22
|
-
'Asia/Karachi', 'Asia/Kolkata', 'Asia/Colombo', 'Asia/Dhaka',
|
|
23
|
-
'Asia/Bangkok', 'Asia/Jakarta', 'Asia/Singapore', 'Asia/Kuala_Lumpur',
|
|
24
|
-
'Asia/Hong_Kong', 'Asia/Shanghai', 'Asia/Taipei',
|
|
25
|
-
'Asia/Seoul', 'Asia/Tokyo',
|
|
26
|
-
// Africa
|
|
27
|
-
'Africa/Casablanca', 'Africa/Lagos', 'Africa/Nairobi', 'Africa/Johannesburg', 'Africa/Cairo',
|
|
28
|
-
// Pacific / Australia
|
|
29
|
-
'Australia/Perth', 'Australia/Adelaide', 'Australia/Darwin',
|
|
30
|
-
'Australia/Brisbane', 'Australia/Sydney', 'Australia/Melbourne',
|
|
31
|
-
'Pacific/Auckland', 'Pacific/Fiji', 'Pacific/Honolulu',
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
export interface ITimedSenderRule {
|
|
37
|
-
from: string
|
|
38
|
-
to: string
|
|
39
|
-
days?: number[]
|
|
40
|
-
action: 'send' | 'drop'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ITimedSenderConfig extends ISenderConfig {
|
|
44
|
-
name: string
|
|
45
|
-
rules: ITimedSenderRule[]
|
|
46
|
-
defaultAction?: 'send' | 'drop'
|
|
47
|
-
timezone?: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
function tzOffset(tz: string): string {
|
|
53
|
-
try {
|
|
54
|
-
const part = new Intl.DateTimeFormat('en', { timeZone: tz, timeZoneName: 'shortOffset' })
|
|
55
|
-
.formatToParts(new Date()).find(p => p.type === 'timeZoneName')?.value ?? ''
|
|
56
|
-
return part.replace('GMT', 'UTC')
|
|
57
|
-
} catch { return '' }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseMinutes(hhmm: string): number {
|
|
61
|
-
const [h, m] = hhmm.split(':').map(Number)
|
|
62
|
-
return (h ?? 0) * 60 + (m ?? 0)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function currentContext(timezone?: string): { minutes: number; day: number } {
|
|
66
|
-
const now = timezone
|
|
67
|
-
? new Date(new Date().toLocaleString('en-US', { timeZone: timezone }))
|
|
68
|
-
: new Date()
|
|
69
|
-
return { minutes: now.getHours() * 60 + now.getMinutes(), day: now.getDay() }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function matchesWindow(rule: ITimedSenderRule, minutes: number, day: number): boolean {
|
|
73
|
-
if (rule.days && rule.days.length > 0 && !rule.days.includes(day)) return false
|
|
74
|
-
const from = parseMinutes(rule.from)
|
|
75
|
-
const to = parseMinutes(rule.to)
|
|
76
|
-
return from <= to
|
|
77
|
-
? minutes >= from && minutes < to // normal: 09:00–18:00
|
|
78
|
-
: minutes >= from || minutes < to // overnight: 22:00–06:00
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── Sender ────────────────────────────────────────────────────────────────────
|
|
82
|
-
|
|
83
|
-
export class TimedSender implements ISender {
|
|
84
|
-
readonly id = 'timed'
|
|
85
|
-
private configs = new Map<string, ITimedSenderConfig>()
|
|
86
|
-
|
|
87
|
-
addConfig(config: ISenderConfig): void {
|
|
88
|
-
const tc = config as ITimedSenderConfig
|
|
89
|
-
if (!Array.isArray(tc.rules)) tc.rules = []
|
|
90
|
-
this.configs.set(tc.name, tc)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
removeConfig(name: string): void {
|
|
94
|
-
this.configs.delete(name)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
hasConfig(name: string): boolean {
|
|
98
|
-
return this.configs.has(name)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getConfigNames(): string[] {
|
|
102
|
-
return Array.from(this.configs.keys())
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async send(_configName: string, _message: ISenderMessage): Promise<void> {
|
|
106
|
-
// timed sender is a filter used inside composite pipelines; standalone send is a no-op
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
getConfigSchema(): ISenderFieldDef[] {
|
|
110
|
-
return [
|
|
111
|
-
{ name: 'name', label: 'Name', required: true },
|
|
112
|
-
{ name: 'timezone', label: 'Timezone', type: 'select', options: TIMEZONES, labels: TIMEZONES.map(tz => { const off = tzOffset(tz); return off ? `${tz} (${off})` : tz }) } as unknown as ISenderFieldDef,
|
|
113
|
-
{ name: 'rules', label: 'Rules (JSON)', type: 'json' },
|
|
114
|
-
{ name: 'defaultAction', label: 'Default action', type: 'select', options: ['drop', 'send'] },
|
|
115
|
-
]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async startSender(_senders: ISenderAccess): Promise<void> {}
|
|
119
|
-
|
|
120
|
-
async stopSender(): Promise<void> {}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export default TimedSender
|
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
Box, Button, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent,
|
|
4
|
-
DialogTitle, Divider, FormControl, IconButton, InputLabel,
|
|
5
|
-
MenuItem, Select, Stack, TextField, Tooltip, Typography
|
|
6
|
-
} from '@mui/material'
|
|
7
|
-
import { ContentCopy } from '@mui/icons-material'
|
|
8
|
-
import { Add, Delete } from '@mui/icons-material'
|
|
9
|
-
|
|
10
|
-
// ─── Auth helpers ─────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
function authGet(accessString: string): RequestInit {
|
|
13
|
-
return { headers: { Authorization: accessString ? `Bearer ${accessString}` : '', 'X-Kwirth-App': 'true' } }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function authDelete(accessString: string): RequestInit {
|
|
17
|
-
return { method: 'DELETE', headers: { Authorization: accessString ? `Bearer ${accessString}` : '', 'X-Kwirth-App': 'true' } }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function authPost(accessString: string, body: string): RequestInit {
|
|
21
|
-
return {
|
|
22
|
-
method: 'POST', body,
|
|
23
|
-
headers: { Authorization: accessString ? `Bearer ${accessString}` : '', 'X-Kwirth-App': 'true', 'Content-Type': 'application/json' },
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
interface ITimedRule {
|
|
30
|
-
from: string
|
|
31
|
-
to: string
|
|
32
|
-
days?: number[]
|
|
33
|
-
action: 'send' | 'drop'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface ITimedConfig {
|
|
37
|
-
name: string
|
|
38
|
-
description?: string
|
|
39
|
-
timezone?: string
|
|
40
|
-
rules: ITimedRule[]
|
|
41
|
-
defaultAction?: 'send' | 'drop'
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface ITimedConfigDialogProps {
|
|
45
|
-
onClose: () => void
|
|
46
|
-
backendUrl: string
|
|
47
|
-
accessString: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
const TIMEZONES = [
|
|
53
|
-
'UTC',
|
|
54
|
-
'Europe/London', 'Europe/Dublin', 'Europe/Lisbon',
|
|
55
|
-
'Europe/Madrid', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Amsterdam',
|
|
56
|
-
'Europe/Brussels', 'Europe/Vienna', 'Europe/Zurich', 'Europe/Stockholm', 'Europe/Oslo',
|
|
57
|
-
'Europe/Copenhagen', 'Europe/Helsinki', 'Europe/Warsaw', 'Europe/Prague', 'Europe/Budapest',
|
|
58
|
-
'Europe/Bucharest', 'Europe/Athens', 'Europe/Istanbul', 'Europe/Kiev', 'Europe/Moscow',
|
|
59
|
-
'America/New_York', 'America/Toronto', 'America/Montreal',
|
|
60
|
-
'America/Chicago', 'America/Winnipeg',
|
|
61
|
-
'America/Denver', 'America/Edmonton',
|
|
62
|
-
'America/Los_Angeles', 'America/Vancouver',
|
|
63
|
-
'America/Phoenix', 'America/Anchorage', 'America/Honolulu',
|
|
64
|
-
'America/Mexico_City', 'America/Bogota', 'America/Lima',
|
|
65
|
-
'America/Santiago', 'America/Buenos_Aires', 'America/Sao_Paulo',
|
|
66
|
-
'America/Caracas', 'America/Halifax', 'America/St_Johns',
|
|
67
|
-
'Asia/Jerusalem', 'Asia/Beirut', 'Asia/Riyadh', 'Asia/Dubai', 'Asia/Tehran',
|
|
68
|
-
'Asia/Karachi', 'Asia/Kolkata', 'Asia/Colombo', 'Asia/Dhaka',
|
|
69
|
-
'Asia/Bangkok', 'Asia/Jakarta', 'Asia/Singapore', 'Asia/Kuala_Lumpur',
|
|
70
|
-
'Asia/Hong_Kong', 'Asia/Shanghai', 'Asia/Taipei',
|
|
71
|
-
'Asia/Seoul', 'Asia/Tokyo',
|
|
72
|
-
'Africa/Casablanca', 'Africa/Lagos', 'Africa/Nairobi', 'Africa/Johannesburg', 'Africa/Cairo',
|
|
73
|
-
'Australia/Perth', 'Australia/Adelaide', 'Australia/Darwin',
|
|
74
|
-
'Australia/Brisbane', 'Australia/Sydney', 'Australia/Melbourne',
|
|
75
|
-
'Pacific/Auckland', 'Pacific/Fiji', 'Pacific/Honolulu',
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
function tzOffset(tz: string): string {
|
|
79
|
-
try {
|
|
80
|
-
const part = new Intl.DateTimeFormat('en', { timeZone: tz, timeZoneName: 'shortOffset' })
|
|
81
|
-
.formatToParts(new Date()).find(p => p.type === 'timeZoneName')?.value ?? ''
|
|
82
|
-
return part.replace('GMT', 'UTC')
|
|
83
|
-
} catch { return '' }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
87
|
-
|
|
88
|
-
function emptyRule(): ITimedRule {
|
|
89
|
-
return { from: '09:00', to: '18:00', action: 'send' }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function emptyConfig(): ITimedConfig {
|
|
93
|
-
return { name: '', rules: [], defaultAction: 'drop' }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Rule editor row ──────────────────────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
const RuleRow: React.FC<{
|
|
99
|
-
rule: ITimedRule
|
|
100
|
-
onChange: (rule: ITimedRule) => void
|
|
101
|
-
onDelete: () => void
|
|
102
|
-
}> = ({ rule, onChange, onDelete }) => {
|
|
103
|
-
const allDays = [0, 1, 2, 3, 4, 5, 6]
|
|
104
|
-
const selectedDays = rule.days ?? allDays
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 1, py: 0.75, display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
108
|
-
<Stack direction='row' spacing={1} alignItems='center' sx={{ flex: 1 }}>
|
|
109
|
-
<TextField size='small' label='From' type='time' sx={{ width: 110 }}
|
|
110
|
-
value={rule.from} onChange={e => onChange({ ...rule, from: e.target.value })}
|
|
111
|
-
InputLabelProps={{ shrink: true }} />
|
|
112
|
-
<TextField size='small' label='To' type='time' sx={{ width: 110 }}
|
|
113
|
-
value={rule.to} onChange={e => onChange({ ...rule, to: e.target.value })}
|
|
114
|
-
InputLabelProps={{ shrink: true }} />
|
|
115
|
-
<FormControl size='small' sx={{ minWidth: 110, flexShrink: 0 }}>
|
|
116
|
-
<InputLabel>Action</InputLabel>
|
|
117
|
-
<Select label='Action' value={rule.action}
|
|
118
|
-
onChange={e => onChange({ ...rule, action: e.target.value as 'send' | 'drop' })}>
|
|
119
|
-
<MenuItem value='drop'><Chip label='drop' size='small' color='error' sx={{ fontSize: 10, height: 18 }} /></MenuItem>
|
|
120
|
-
<MenuItem value='send'><Chip label='send' size='small' color='success' sx={{ fontSize: 10, height: 18 }} /></MenuItem>
|
|
121
|
-
</Select>
|
|
122
|
-
</FormControl>
|
|
123
|
-
<FormControl size='small' sx={{ flex: 1, minWidth: 160 }}>
|
|
124
|
-
<InputLabel>Days</InputLabel>
|
|
125
|
-
<Select
|
|
126
|
-
multiple label='Days'
|
|
127
|
-
value={selectedDays}
|
|
128
|
-
onChange={e => {
|
|
129
|
-
const val = e.target.value as number[]
|
|
130
|
-
onChange({ ...rule, days: val.length === 0 || val.length === 7 ? undefined : [...val].sort((a, b) => a - b) })
|
|
131
|
-
}}
|
|
132
|
-
renderValue={sel => {
|
|
133
|
-
const days = sel as number[]
|
|
134
|
-
return days.length === 7 ? 'All days' : days.map(d => DAY_LABELS[d]).join(', ')
|
|
135
|
-
}}
|
|
136
|
-
>
|
|
137
|
-
{DAY_LABELS.map((label, i) => (
|
|
138
|
-
<MenuItem key={i} value={i} sx={{ py: 0.25 }}>
|
|
139
|
-
<Checkbox checked={selectedDays.includes(i)} size='small' sx={{ p: 0.25 }} />
|
|
140
|
-
<Typography variant='body2'>{label}</Typography>
|
|
141
|
-
</MenuItem>
|
|
142
|
-
))}
|
|
143
|
-
</Select>
|
|
144
|
-
</FormControl>
|
|
145
|
-
</Stack>
|
|
146
|
-
<IconButton size='small' color='error' onClick={onDelete} sx={{ alignSelf: 'center', flexShrink: 0 }}>
|
|
147
|
-
<Delete fontSize='small' />
|
|
148
|
-
</IconButton>
|
|
149
|
-
</Box>
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── Config form ──────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
const ConfigForm: React.FC<{
|
|
156
|
-
config: ITimedConfig
|
|
157
|
-
onChange: (config: ITimedConfig) => void
|
|
158
|
-
}> = ({ config, onChange }) => {
|
|
159
|
-
const updateRule = (i: number, rule: ITimedRule) => {
|
|
160
|
-
const rules = [...config.rules]
|
|
161
|
-
rules[i] = rule
|
|
162
|
-
onChange({ ...config, rules })
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const deleteRule = (i: number) => {
|
|
166
|
-
onChange({ ...config, rules: config.rules.filter((_, idx) => idx !== i) })
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const addRule = () => {
|
|
170
|
-
onChange({ ...config, rules: [...config.rules, emptyRule()] })
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<Stack spacing={2}>
|
|
175
|
-
<Stack direction='row' spacing={1} alignItems='center'>
|
|
176
|
-
<TextField
|
|
177
|
-
size='small' label='Name *' value={config.name}
|
|
178
|
-
onChange={e => onChange({ ...config, name: e.target.value })}
|
|
179
|
-
sx={{ flex: 2 }}
|
|
180
|
-
/>
|
|
181
|
-
<FormControl size='small' sx={{ flex: 2 }}>
|
|
182
|
-
<InputLabel>Timezone</InputLabel>
|
|
183
|
-
<Select label='Timezone' value={config.timezone ?? ''}
|
|
184
|
-
onChange={e => onChange({ ...config, timezone: e.target.value || undefined })}>
|
|
185
|
-
<MenuItem value=''><em>Server local</em></MenuItem>
|
|
186
|
-
{TIMEZONES.map(tz => {
|
|
187
|
-
const off = tzOffset(tz)
|
|
188
|
-
return <MenuItem key={tz} value={tz}>{tz}{off ? ` (${off})` : ''}</MenuItem>
|
|
189
|
-
})}
|
|
190
|
-
</Select>
|
|
191
|
-
</FormControl>
|
|
192
|
-
<FormControl size='small' sx={{ minWidth: 110, flexShrink: 0 }}>
|
|
193
|
-
<InputLabel>Default</InputLabel>
|
|
194
|
-
<Select label='Default' value={config.defaultAction ?? 'drop'}
|
|
195
|
-
onChange={e => onChange({ ...config, defaultAction: e.target.value as 'send' | 'drop' })}>
|
|
196
|
-
<MenuItem value='drop'><Chip label='drop' size='small' color='error' sx={{ fontSize: 10, height: 18 }} /></MenuItem>
|
|
197
|
-
<MenuItem value='send'><Chip label='send' size='small' color='success' sx={{ fontSize: 10, height: 18 }} /></MenuItem>
|
|
198
|
-
</Select>
|
|
199
|
-
</FormControl>
|
|
200
|
-
</Stack>
|
|
201
|
-
|
|
202
|
-
<TextField size='small' label='Description' value={config.description ?? ''}
|
|
203
|
-
onChange={e => onChange({ ...config, description: e.target.value || undefined })}
|
|
204
|
-
fullWidth multiline maxRows={2} />
|
|
205
|
-
|
|
206
|
-
<Divider><Typography variant='caption'>Rules (evaluated in order — first match wins)</Typography></Divider>
|
|
207
|
-
|
|
208
|
-
{config.rules.length === 0 && (
|
|
209
|
-
<Typography variant='body2' color='text.secondary'>No rules yet. Add one below.</Typography>
|
|
210
|
-
)}
|
|
211
|
-
{config.rules.map((rule, i) => (
|
|
212
|
-
<RuleRow key={i} rule={rule}
|
|
213
|
-
onChange={r => updateRule(i, r)}
|
|
214
|
-
onDelete={() => deleteRule(i)}
|
|
215
|
-
/>
|
|
216
|
-
))}
|
|
217
|
-
<Button size='small' startIcon={<Add />} onClick={addRule} sx={{ alignSelf: 'flex-start' }}>
|
|
218
|
-
Add rule
|
|
219
|
-
</Button>
|
|
220
|
-
|
|
221
|
-
</Stack>
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ─── Main dialog ──────────────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
const TimedConfigDialog: React.FC<ITimedConfigDialogProps> = ({ onClose, backendUrl, accessString }) => {
|
|
228
|
-
const [configs, setConfigs] = useState<ITimedConfig[]>([])
|
|
229
|
-
const [loading, setLoading] = useState(true)
|
|
230
|
-
const [saving, setSaving] = useState(false)
|
|
231
|
-
const [deletingName, setDeletingName] = useState<string | undefined>()
|
|
232
|
-
const [showForm, setShowForm] = useState(false)
|
|
233
|
-
const [formConfig, setFormConfig] = useState<ITimedConfig>(emptyConfig())
|
|
234
|
-
const [originalFormName, setOriginalFormName] = useState<string | undefined>()
|
|
235
|
-
const [error, setError] = useState<string | undefined>()
|
|
236
|
-
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
fetch(`${backendUrl}/senders/timed/configs`, authGet(accessString))
|
|
239
|
-
.then(r => r.json()).then(data => setConfigs(Array.isArray(data) ? data : (data.configs ?? []))).catch(() => {})
|
|
240
|
-
.finally(() => setLoading(false))
|
|
241
|
-
}, [])
|
|
242
|
-
|
|
243
|
-
const saveConfig = async () => {
|
|
244
|
-
const trimmed = formConfig.name.trim()
|
|
245
|
-
if (!trimmed) { setError('Name is required'); return }
|
|
246
|
-
setSaving(true)
|
|
247
|
-
setError(undefined)
|
|
248
|
-
try {
|
|
249
|
-
const payload = { ...formConfig, name: trimmed }
|
|
250
|
-
const res = await fetch(`${backendUrl}/senders/timed/configs`, authPost(accessString, JSON.stringify(payload)))
|
|
251
|
-
if (!res.ok) throw new Error((await res.json()).error ?? `HTTP ${res.status}`)
|
|
252
|
-
if (originalFormName && originalFormName !== trimmed) {
|
|
253
|
-
await fetch(`${backendUrl}/senders/timed/configs/${encodeURIComponent(originalFormName)}`, authDelete(accessString))
|
|
254
|
-
}
|
|
255
|
-
setConfigs(prev => {
|
|
256
|
-
const withoutOld = originalFormName && originalFormName !== trimmed
|
|
257
|
-
? prev.filter(c => c.name !== originalFormName)
|
|
258
|
-
: prev
|
|
259
|
-
const idx = withoutOld.findIndex(c => c.name === trimmed)
|
|
260
|
-
return idx >= 0 ? withoutOld.map((c, i) => i === idx ? payload : c) : [...withoutOld, payload]
|
|
261
|
-
})
|
|
262
|
-
setOriginalFormName(trimmed)
|
|
263
|
-
setFormConfig(payload)
|
|
264
|
-
} catch (err) {
|
|
265
|
-
setError(`Save failed: ${err}`)
|
|
266
|
-
} finally {
|
|
267
|
-
setSaving(false)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const deleteConfig = async (name: string) => {
|
|
272
|
-
setDeletingName(name)
|
|
273
|
-
try {
|
|
274
|
-
const res = await fetch(`${backendUrl}/senders/timed/configs/${encodeURIComponent(name)}`, authDelete(accessString))
|
|
275
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
276
|
-
setConfigs(prev => prev.filter(c => c.name !== name))
|
|
277
|
-
} catch (err) {
|
|
278
|
-
setError(`Delete failed: ${err}`)
|
|
279
|
-
} finally {
|
|
280
|
-
setDeletingName(undefined)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const startAdd = () => {
|
|
285
|
-
setFormConfig(emptyConfig())
|
|
286
|
-
setOriginalFormName(undefined)
|
|
287
|
-
setError(undefined)
|
|
288
|
-
setShowForm(true)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const startEdit = (config: ITimedConfig) => {
|
|
292
|
-
setFormConfig({ ...config })
|
|
293
|
-
setOriginalFormName(config.name)
|
|
294
|
-
setError(undefined)
|
|
295
|
-
setShowForm(true)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const startClone = (config: ITimedConfig) => {
|
|
299
|
-
setFormConfig({ ...config, name: `${config.name} (copy)` })
|
|
300
|
-
setOriginalFormName(undefined)
|
|
301
|
-
setError(undefined)
|
|
302
|
-
setShowForm(true)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const configSummary = (c: ITimedConfig) => {
|
|
306
|
-
const parts: string[] = []
|
|
307
|
-
if (c.timezone) parts.push(c.timezone)
|
|
308
|
-
if (c.rules.length > 0) parts.push(`${c.rules.length} rule(s)`)
|
|
309
|
-
parts.push(`default: ${c.defaultAction ?? 'drop'}`)
|
|
310
|
-
return parts.join(' · ')
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
<Dialog open={true} maxWidth={false} sx={{ '& .MuiDialog-paper': { width: '900px', height: '640px' } }}>
|
|
315
|
-
<DialogTitle>Configure: Timed Sender</DialogTitle>
|
|
316
|
-
<DialogContent sx={{ display: 'flex', gap: 2, p: '16px !important', overflow: 'hidden', height: '100%' }}>
|
|
317
|
-
|
|
318
|
-
{/* Left — config list */}
|
|
319
|
-
<Box sx={{ width: 190, display: 'flex', flexDirection: 'column', gap: 1, flexShrink: 0 }}>
|
|
320
|
-
<Typography variant='caption' color='text.secondary' fontWeight='bold'>Configs</Typography>
|
|
321
|
-
<Box sx={{ flex: 1, border: 1, borderColor: 'divider', borderRadius: 1, overflowY: 'auto' }}>
|
|
322
|
-
{loading
|
|
323
|
-
? <Box sx={{ p: 1 }}><CircularProgress size={16} /></Box>
|
|
324
|
-
: configs.length === 0
|
|
325
|
-
? <Typography variant='caption' color='text.disabled' sx={{ p: 1, display: 'block' }}>No configs yet.</Typography>
|
|
326
|
-
: configs.map(cfg => (
|
|
327
|
-
<Box key={cfg.name} sx={{ display: 'flex', alignItems: 'center', px: 1, py: 0.5, borderBottom: 1, borderColor: 'divider', borderLeft: formConfig.name === cfg.name && showForm ? 3 : 0, borderLeftColor: 'primary.main', bgcolor: formConfig.name === cfg.name && showForm ? 'action.selected' : 'transparent' }}>
|
|
328
|
-
<Box sx={{ flex: 1, minWidth: 0, overflow: 'hidden', cursor: 'pointer' }} onClick={() => startEdit(cfg)}>
|
|
329
|
-
<Typography variant='body2' fontWeight='bold' noWrap>{cfg.name}</Typography>
|
|
330
|
-
<Typography variant='caption' color='text.secondary' noWrap display='block'>{configSummary(cfg)}</Typography>
|
|
331
|
-
</Box>
|
|
332
|
-
<Tooltip title='Delete'>
|
|
333
|
-
<span>
|
|
334
|
-
<IconButton size='small' color='error' disabled={deletingName === cfg.name} onClick={() => deleteConfig(cfg.name)}>
|
|
335
|
-
{deletingName === cfg.name ? <CircularProgress size={12} /> : <Delete sx={{ fontSize: 14 }} />}
|
|
336
|
-
</IconButton>
|
|
337
|
-
</span>
|
|
338
|
-
</Tooltip>
|
|
339
|
-
</Box>
|
|
340
|
-
))
|
|
341
|
-
}
|
|
342
|
-
</Box>
|
|
343
|
-
<Stack direction='row' spacing={0.5}>
|
|
344
|
-
<Button size='small' startIcon={<Add />} onClick={startAdd} sx={{ flex: 1 }}>New</Button>
|
|
345
|
-
<Button size='small' startIcon={<ContentCopy />} onClick={() => { const c = configs.find(x => x.name === formConfig.name && showForm); if (c) startClone(c) }} disabled={!showForm || !originalFormName} sx={{ flex: 1 }}>Clone</Button>
|
|
346
|
-
</Stack>
|
|
347
|
-
</Box>
|
|
348
|
-
|
|
349
|
-
<Divider orientation='vertical' flexItem />
|
|
350
|
-
|
|
351
|
-
{/* Right — editor */}
|
|
352
|
-
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 1.5, minWidth: 0, overflow: 'hidden' }}>
|
|
353
|
-
{showForm
|
|
354
|
-
? <>
|
|
355
|
-
<Typography variant='caption' color='text.secondary' fontWeight='bold'>
|
|
356
|
-
{originalFormName ? `Editing: ${originalFormName}` : 'New config'}
|
|
357
|
-
</Typography>
|
|
358
|
-
<Box sx={{ flex: 1, overflowY: 'auto', pt: 1 }}>
|
|
359
|
-
<ConfigForm config={formConfig} onChange={setFormConfig} />
|
|
360
|
-
</Box>
|
|
361
|
-
<Stack direction='row' justifyContent='flex-end' alignItems='center' spacing={1}>
|
|
362
|
-
{error && <Typography variant='caption' color='error' sx={{ flex: 1 }}>{error}</Typography>}
|
|
363
|
-
<Button size='small' onClick={() => { setShowForm(false); setError(undefined) }}>Cancel</Button>
|
|
364
|
-
<Button size='small' variant='contained' disabled={saving || !formConfig.name.trim()} onClick={saveConfig}>
|
|
365
|
-
{saving ? <CircularProgress size={14} /> : originalFormName ? 'Update' : 'Add'}
|
|
366
|
-
</Button>
|
|
367
|
-
</Stack>
|
|
368
|
-
</>
|
|
369
|
-
: <Box sx={{ m: 'auto', color: 'text.disabled' }}>
|
|
370
|
-
<Typography variant='body2'>Select a config to edit or click New.</Typography>
|
|
371
|
-
</Box>
|
|
372
|
-
}
|
|
373
|
-
</Box>
|
|
374
|
-
</DialogContent>
|
|
375
|
-
<DialogActions sx={{ px: 2 }}>
|
|
376
|
-
<Button onClick={onClose}>Close</Button>
|
|
377
|
-
</DialogActions>
|
|
378
|
-
</Dialog>
|
|
379
|
-
)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
export { TimedConfigDialog }
|
|
383
|
-
export default TimedConfigDialog
|
package/src/front/index.tsx
DELETED
package/tsconfig.json
DELETED
package/watch.mjs
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import esbuild from 'esbuild'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
|
|
5
|
-
const kwirthGlobalsPlugin = {
|
|
6
|
-
name: 'kwirth-globals',
|
|
7
|
-
setup(build) {
|
|
8
|
-
const globals = {
|
|
9
|
-
'react': 'window.__kwirth__.React',
|
|
10
|
-
'@mui/material': 'window.__kwirth__.MUI.material',
|
|
11
|
-
'@mui/icons-material': 'window.__kwirth__.MUI.icons',
|
|
12
|
-
'@kwirthmagnify/kwirth-common': 'window.__kwirth__.kwirthCommon',
|
|
13
|
-
}
|
|
14
|
-
for (const pkg of Object.keys(globals)) {
|
|
15
|
-
build.onResolve({ filter: new RegExp(`^${pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`) }, () => ({ path: pkg, namespace: 'kwirth-globals' }))
|
|
16
|
-
}
|
|
17
|
-
build.onLoad({ filter: /.*/, namespace: 'kwirth-globals' }, (args) => ({
|
|
18
|
-
contents: `module.exports = ${globals[args.path]}`, loader: 'js',
|
|
19
|
-
}))
|
|
20
|
-
},
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const notifyPlugin = (label) => ({
|
|
24
|
-
name: `notify-${label}`,
|
|
25
|
-
setup(build) {
|
|
26
|
-
build.onEnd(result => {
|
|
27
|
-
const t = new Date().toLocaleTimeString()
|
|
28
|
-
if (result.errors.length) {
|
|
29
|
-
console.error(`[${t}] ${label} FAILED: ${result.errors.map(e => e.text).join('; ')}`)
|
|
30
|
-
} else {
|
|
31
|
-
console.log(`[${t}] ${label} OK`)
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
},
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
fs.mkdirSync('dist', { recursive: true })
|
|
38
|
-
const meta = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
|
39
|
-
fs.writeFileSync(path.join('dist', 'package.json'), JSON.stringify({ id: meta.id, name: meta.name, displayName: meta.displayName, version: meta.version, description: meta.description }, null, 2))
|
|
40
|
-
|
|
41
|
-
const backCtx = await esbuild.context({
|
|
42
|
-
entryPoints: ['src/back/index.ts'],
|
|
43
|
-
bundle: true,
|
|
44
|
-
format: 'cjs',
|
|
45
|
-
platform: 'node',
|
|
46
|
-
target: 'node20',
|
|
47
|
-
outfile: 'dist/back.js',
|
|
48
|
-
loader: { '.ts': 'ts' },
|
|
49
|
-
minify: false,
|
|
50
|
-
plugins: [notifyPlugin('back')],
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const frontCtx = await esbuild.context({
|
|
54
|
-
entryPoints: ['src/front/index.tsx'],
|
|
55
|
-
bundle: true,
|
|
56
|
-
format: 'iife',
|
|
57
|
-
outfile: 'dist/front.js',
|
|
58
|
-
plugins: [kwirthGlobalsPlugin, notifyPlugin('front')],
|
|
59
|
-
loader: { '.tsx': 'tsx', '.ts': 'ts' },
|
|
60
|
-
jsx: 'transform',
|
|
61
|
-
jsxFactory: 'React.createElement',
|
|
62
|
-
jsxFragment: 'React.Fragment',
|
|
63
|
-
target: 'es2020',
|
|
64
|
-
minify: false,
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
await Promise.all([backCtx.watch(), frontCtx.watch()])
|
|
68
|
-
console.log('[watch] Watching src/ — back.js and front.js rebuild on every change.')
|