@jhytabest/plashboard 0.1.7 → 0.1.9
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 +29 -3
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/skills/plashboard-admin/SKILL.md +6 -0
- package/src/config.ts +11 -0
- package/src/fill-runner.test.ts +1 -0
- package/src/plugin.ts +239 -1
- package/src/runtime.test.ts +41 -2
- package/src/runtime.ts +342 -14
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ This plugin manages dashboard templates, scheduled fill runs, validation, and at
|
|
|
9
9
|
```bash
|
|
10
10
|
openclaw plugins install @jhytabest/plashboard
|
|
11
11
|
openclaw plugins enable plashboard
|
|
12
|
+
sudo systemctl restart openclaw-gateway
|
|
12
13
|
openclaw plugins doctor
|
|
13
14
|
```
|
|
14
15
|
|
|
@@ -18,7 +19,21 @@ openclaw plugins doctor
|
|
|
18
19
|
openclaw plugins update plashboard
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## Zero-Config First Run
|
|
23
|
+
|
|
24
|
+
No manual config is required for first use. Defaults are safe:
|
|
25
|
+
- `fill_provider=openclaw`
|
|
26
|
+
- `openclaw_fill_agent_id=main`
|
|
27
|
+
- automatic init on service start
|
|
28
|
+
- automatic starter template seed when template store is empty
|
|
29
|
+
|
|
30
|
+
In chat, run:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
/plashboard quickstart <what this dashboard should focus on>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Optional Config
|
|
22
37
|
|
|
23
38
|
Add to `openclaw.json`:
|
|
24
39
|
|
|
@@ -35,6 +50,7 @@ Add to `openclaw.json`:
|
|
|
35
50
|
"default_retry_count": 1,
|
|
36
51
|
"retry_backoff_seconds": 20,
|
|
37
52
|
"session_timeout_seconds": 90,
|
|
53
|
+
"auto_seed_template": true,
|
|
38
54
|
"fill_provider": "openclaw",
|
|
39
55
|
"openclaw_fill_agent_id": "main",
|
|
40
56
|
"display_profile": {
|
|
@@ -59,6 +75,9 @@ Use `fill_provider: "command"` only if you need a custom external runner.
|
|
|
59
75
|
|
|
60
76
|
```text
|
|
61
77
|
/plashboard setup [openclaw [agent_id]|mock|command <fill_command>]
|
|
78
|
+
/plashboard quickstart <description>
|
|
79
|
+
/plashboard doctor [local_url] [https_port] [repo_dir]
|
|
80
|
+
/plashboard web-guide [local_url] [repo_dir]
|
|
62
81
|
/plashboard expose-guide [local_url] [https_port]
|
|
63
82
|
/plashboard expose-check [local_url] [https_port]
|
|
64
83
|
/plashboard init
|
|
@@ -74,8 +93,15 @@ Use `fill_provider: "command"` only if you need a custom external runner.
|
|
|
74
93
|
Recommended first run:
|
|
75
94
|
|
|
76
95
|
```text
|
|
77
|
-
/plashboard
|
|
78
|
-
|
|
96
|
+
/plashboard quickstart "Focus on service health, priorities, blockers, and next actions."
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
If `quickstart` returns web/exposure warnings:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
/plashboard web-guide
|
|
103
|
+
/plashboard expose-guide
|
|
104
|
+
/plashboard doctor
|
|
79
105
|
```
|
|
80
106
|
|
|
81
107
|
Tailscale helper flow:
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "plashboard",
|
|
3
3
|
"name": "Plashboard",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"description": "Template-driven dashboard runtime with scheduled OpenClaw fills and safe publish.",
|
|
6
6
|
"entry": "./src/index.ts",
|
|
7
7
|
"skills": ["./skills/plashboard-admin"],
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"default_retry_count": { "type": "integer", "minimum": 0, "maximum": 5, "default": 1 },
|
|
16
16
|
"retry_backoff_seconds": { "type": "integer", "minimum": 1, "maximum": 300, "default": 20 },
|
|
17
17
|
"session_timeout_seconds": { "type": "integer", "minimum": 10, "maximum": 600, "default": 90 },
|
|
18
|
+
"auto_seed_template": { "type": "boolean", "default": true },
|
|
18
19
|
"fill_provider": { "type": "string", "enum": ["command", "mock", "openclaw"], "default": "openclaw" },
|
|
19
20
|
"fill_command": { "type": "string" },
|
|
20
21
|
"openclaw_fill_agent_id": { "type": "string", "default": "main" },
|
package/package.json
CHANGED
|
@@ -20,7 +20,10 @@ Always use plugin tools:
|
|
|
20
20
|
- `plashboard_setup`
|
|
21
21
|
- `plashboard_exposure_guide`
|
|
22
22
|
- `plashboard_exposure_check`
|
|
23
|
+
- `plashboard_web_guide`
|
|
24
|
+
- `plashboard_doctor`
|
|
23
25
|
- `plashboard_init`
|
|
26
|
+
- `plashboard_quickstart`
|
|
24
27
|
- `plashboard_template_create`
|
|
25
28
|
- `plashboard_template_update`
|
|
26
29
|
- `plashboard_template_list`
|
|
@@ -39,7 +42,10 @@ Always use plugin tools:
|
|
|
39
42
|
- Never ask the model to generate full dashboard structure when filling values.
|
|
40
43
|
|
|
41
44
|
## Command Shortcuts
|
|
45
|
+
- `/plashboard quickstart <description>`
|
|
42
46
|
- `/plashboard setup [openclaw [agent_id]|mock|command <fill_command>]`
|
|
47
|
+
- `/plashboard doctor [local_url] [https_port] [repo_dir]`
|
|
48
|
+
- `/plashboard web-guide [local_url] [repo_dir]`
|
|
43
49
|
- `/plashboard expose-guide [local_url] [https_port]`
|
|
44
50
|
- `/plashboard expose-check [local_url] [https_port]`
|
|
45
51
|
- `/plashboard init`
|
package/src/config.ts
CHANGED
|
@@ -22,6 +22,16 @@ function asString(value: unknown, fallback: string): string {
|
|
|
22
22
|
return typeof value === 'string' && value.trim() ? value : fallback;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function asBoolean(value: unknown, fallback: boolean): boolean {
|
|
26
|
+
if (typeof value === 'boolean') return value;
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
const normalized = value.trim().toLowerCase();
|
|
29
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') return true;
|
|
30
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') return false;
|
|
31
|
+
}
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
function asObject(value: unknown): Record<string, unknown> {
|
|
26
36
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
27
37
|
? (value as Record<string, unknown>)
|
|
@@ -65,6 +75,7 @@ export function resolveConfig(api: unknown): PlashboardConfig {
|
|
|
65
75
|
default_retry_count: Math.max(0, Math.floor(asNumber(raw.default_retry_count, 1))),
|
|
66
76
|
retry_backoff_seconds: Math.max(1, Math.floor(asNumber(raw.retry_backoff_seconds, 20))),
|
|
67
77
|
session_timeout_seconds: Math.max(10, Math.floor(asNumber(raw.session_timeout_seconds, 90))),
|
|
78
|
+
auto_seed_template: asBoolean(raw.auto_seed_template, true),
|
|
68
79
|
fill_provider: fillProvider,
|
|
69
80
|
fill_command: typeof raw.fill_command === 'string' ? raw.fill_command : undefined,
|
|
70
81
|
openclaw_fill_agent_id: asString(raw.openclaw_fill_agent_id, 'main'),
|
package/src/fill-runner.test.ts
CHANGED
|
@@ -37,6 +37,7 @@ function config(overrides: Partial<PlashboardConfig>): PlashboardConfig {
|
|
|
37
37
|
default_retry_count: 0,
|
|
38
38
|
retry_backoff_seconds: 1,
|
|
39
39
|
session_timeout_seconds: 30,
|
|
40
|
+
auto_seed_template: false,
|
|
40
41
|
fill_provider: 'mock',
|
|
41
42
|
fill_command: undefined,
|
|
42
43
|
openclaw_fill_agent_id: 'main',
|
package/src/plugin.ts
CHANGED
|
@@ -90,6 +90,7 @@ type SetupParams = {
|
|
|
90
90
|
fill_provider?: 'mock' | 'command' | 'openclaw';
|
|
91
91
|
fill_command?: string;
|
|
92
92
|
openclaw_fill_agent_id?: string;
|
|
93
|
+
auto_seed_template?: boolean;
|
|
93
94
|
data_dir?: string;
|
|
94
95
|
scheduler_tick_seconds?: number;
|
|
95
96
|
session_timeout_seconds?: number;
|
|
@@ -101,12 +102,30 @@ type SetupParams = {
|
|
|
101
102
|
layout_safety_margin_px?: number;
|
|
102
103
|
};
|
|
103
104
|
|
|
105
|
+
type QuickstartParams = {
|
|
106
|
+
description?: string;
|
|
107
|
+
template_id?: string;
|
|
108
|
+
template_name?: string;
|
|
109
|
+
every_minutes?: number;
|
|
110
|
+
activate?: boolean;
|
|
111
|
+
run_now?: boolean;
|
|
112
|
+
};
|
|
113
|
+
|
|
104
114
|
type ExposureParams = {
|
|
105
115
|
local_url?: string;
|
|
106
116
|
tailscale_https_port?: number;
|
|
107
117
|
dashboard_output_path?: string;
|
|
108
118
|
};
|
|
109
119
|
|
|
120
|
+
type WebGuideParams = {
|
|
121
|
+
local_url?: string;
|
|
122
|
+
repo_dir?: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type DoctorParams = ExposureParams & {
|
|
126
|
+
repo_dir?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
110
129
|
type CommandExecResult = {
|
|
111
130
|
ok: boolean;
|
|
112
131
|
stdout: string;
|
|
@@ -220,6 +239,40 @@ async function buildExposureGuide(resolvedConfig: ReturnType<typeof resolveConfi
|
|
|
220
239
|
} satisfies ToolResponse<Record<string, unknown>>;
|
|
221
240
|
}
|
|
222
241
|
|
|
242
|
+
function deriveRepoDir(raw?: string): string {
|
|
243
|
+
const value = (raw || '').trim();
|
|
244
|
+
return value || '/opt/plashboard';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function buildWebGuide(resolvedConfig: ReturnType<typeof resolveConfig>, params: WebGuideParams = {}) {
|
|
248
|
+
const localUrl = normalizeLocalUrl(params.local_url);
|
|
249
|
+
const repoDir = deriveRepoDir(params.repo_dir);
|
|
250
|
+
const dashboardPath = resolvedConfig.dashboard_output_path;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
errors: [],
|
|
255
|
+
data: {
|
|
256
|
+
local_url: localUrl,
|
|
257
|
+
repo_dir: repoDir,
|
|
258
|
+
dashboard_output_path: dashboardPath,
|
|
259
|
+
commands: [
|
|
260
|
+
`git clone https://github.com/jhytabest/plashboard.git ${repoDir} || true`,
|
|
261
|
+
`git -C ${repoDir} pull --ff-only`,
|
|
262
|
+
`docker compose -f ${repoDir}/docker-compose.yml up -d`,
|
|
263
|
+
`docker ps --format "{{.Names}} {{.Ports}}" | grep -E "plash-web|18888"`,
|
|
264
|
+
`curl -I ${localUrl}`,
|
|
265
|
+
`curl -I ${localUrl}/healthz`
|
|
266
|
+
],
|
|
267
|
+
notes: [
|
|
268
|
+
'Plashboard writes dashboard JSON; a local web server must serve the UI and /data/dashboard.json.',
|
|
269
|
+
'The bundled docker-compose stack exposes nginx at 127.0.0.1:18888 by default.',
|
|
270
|
+
'If you host UI differently, update local_url in expose-check/expose-guide.'
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
} satisfies ToolResponse<Record<string, unknown>>;
|
|
274
|
+
}
|
|
275
|
+
|
|
223
276
|
async function runExposureCheck(resolvedConfig: ReturnType<typeof resolveConfig>, params: ExposureParams = {}) {
|
|
224
277
|
const localUrl = normalizeLocalUrl(params.local_url);
|
|
225
278
|
const httpsPort = normalizePort(asNumber(params.tailscale_https_port), 8444);
|
|
@@ -357,6 +410,13 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
357
410
|
|| asString(currentPluginConfig.fill_command)
|
|
358
411
|
|| asString(resolvedConfig.fill_command)
|
|
359
412
|
).trim();
|
|
413
|
+
const selectedAutoSeed = (
|
|
414
|
+
typeof params.auto_seed_template === 'boolean'
|
|
415
|
+
? params.auto_seed_template
|
|
416
|
+
: typeof currentPluginConfig.auto_seed_template === 'boolean'
|
|
417
|
+
? currentPluginConfig.auto_seed_template
|
|
418
|
+
: resolvedConfig.auto_seed_template
|
|
419
|
+
);
|
|
360
420
|
const selectedAgentId = (
|
|
361
421
|
params.openclaw_fill_agent_id
|
|
362
422
|
|| asString(currentPluginConfig.openclaw_fill_agent_id)
|
|
@@ -397,6 +457,7 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
397
457
|
)
|
|
398
458
|
),
|
|
399
459
|
fill_provider: selectedProvider,
|
|
460
|
+
auto_seed_template: selectedAutoSeed,
|
|
400
461
|
display_profile: displayProfile
|
|
401
462
|
};
|
|
402
463
|
|
|
@@ -438,6 +499,7 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
438
499
|
fill_provider: selectedProvider,
|
|
439
500
|
fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
|
|
440
501
|
openclaw_fill_agent_id: selectedProvider === 'openclaw' ? selectedAgentId : undefined,
|
|
502
|
+
auto_seed_template: selectedAutoSeed,
|
|
441
503
|
data_dir: nextPluginConfig.data_dir,
|
|
442
504
|
scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
|
|
443
505
|
session_timeout_seconds: nextPluginConfig.session_timeout_seconds,
|
|
@@ -450,6 +512,101 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
450
512
|
} satisfies ToolResponse<Record<string, unknown>>;
|
|
451
513
|
}
|
|
452
514
|
|
|
515
|
+
async function runQuickstart(
|
|
516
|
+
runtime: PlashboardRuntime,
|
|
517
|
+
resolvedConfig: ReturnType<typeof resolveConfig>,
|
|
518
|
+
params: QuickstartParams = {}
|
|
519
|
+
): Promise<ToolResponse<Record<string, unknown>>> {
|
|
520
|
+
const quickstart = await runtime.quickstart(params);
|
|
521
|
+
const exposure = await runExposureCheck(resolvedConfig, {});
|
|
522
|
+
const guide = await buildExposureGuide(resolvedConfig, {});
|
|
523
|
+
const webGuide = await buildWebGuide(resolvedConfig, {});
|
|
524
|
+
|
|
525
|
+
const warnings: string[] = [];
|
|
526
|
+
if (!exposure.ok) {
|
|
527
|
+
warnings.push(...exposure.errors);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
ok: quickstart.ok,
|
|
532
|
+
errors: quickstart.errors,
|
|
533
|
+
data: {
|
|
534
|
+
...(quickstart.data || {}),
|
|
535
|
+
postcheck: {
|
|
536
|
+
local_url: exposure.data?.local_url,
|
|
537
|
+
local_url_ok: exposure.data?.local_url_ok,
|
|
538
|
+
tailscale_port_configured: exposure.data?.tailscale_port_configured,
|
|
539
|
+
dashboard_exists: exposure.data?.dashboard_exists
|
|
540
|
+
},
|
|
541
|
+
warnings,
|
|
542
|
+
next_steps: warnings.length
|
|
543
|
+
? [
|
|
544
|
+
'run /plashboard web-guide and execute its commands',
|
|
545
|
+
'run /plashboard expose-guide and apply tailscale mapping',
|
|
546
|
+
'run /plashboard doctor'
|
|
547
|
+
]
|
|
548
|
+
: [
|
|
549
|
+
'dashboard generation is working',
|
|
550
|
+
'run /plashboard doctor for full readiness check'
|
|
551
|
+
],
|
|
552
|
+
exposure_guide: guide.data,
|
|
553
|
+
web_guide: webGuide.data
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function runDoctor(
|
|
559
|
+
runtime: PlashboardRuntime,
|
|
560
|
+
resolvedConfig: ReturnType<typeof resolveConfig>,
|
|
561
|
+
params: DoctorParams = {}
|
|
562
|
+
): Promise<ToolResponse<Record<string, unknown>>> {
|
|
563
|
+
const status = await runtime.status();
|
|
564
|
+
const exposure = await runExposureCheck(resolvedConfig, params);
|
|
565
|
+
const exposureGuide = await buildExposureGuide(resolvedConfig, params);
|
|
566
|
+
const webGuide = await buildWebGuide(resolvedConfig, params);
|
|
567
|
+
|
|
568
|
+
const issues: string[] = [];
|
|
569
|
+
const statusData = status.data;
|
|
570
|
+
const templateCount = Number(statusData?.template_count ?? 0);
|
|
571
|
+
const activeTemplateId = statusData?.active_template_id || null;
|
|
572
|
+
|
|
573
|
+
if (!status.ok) issues.push(...status.errors);
|
|
574
|
+
if (templateCount === 0) issues.push('no templates exist; run /plashboard quickstart "<description>"');
|
|
575
|
+
if (!activeTemplateId) issues.push('no active template; activate one with /plashboard activate <template-id>');
|
|
576
|
+
if (exposure.data?.dashboard_exists !== true) {
|
|
577
|
+
issues.push(`dashboard output missing at ${resolvedConfig.dashboard_output_path}`);
|
|
578
|
+
}
|
|
579
|
+
if (exposure.data?.local_url_ok !== true) {
|
|
580
|
+
issues.push(`local dashboard server is not reachable at ${String(exposure.data?.local_url || 'http://127.0.0.1:18888')}`);
|
|
581
|
+
}
|
|
582
|
+
if (exposure.data?.tailscale_status_ok !== true) {
|
|
583
|
+
issues.push('tailscale serve status failed');
|
|
584
|
+
} else if (exposure.data?.tailscale_port_configured !== true) {
|
|
585
|
+
issues.push(`tailscale serve mapping missing for port ${String(exposure.data?.tailscale_https_port || 8444)}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const ready = issues.length === 0;
|
|
589
|
+
return {
|
|
590
|
+
ok: ready,
|
|
591
|
+
errors: issues,
|
|
592
|
+
data: {
|
|
593
|
+
ready,
|
|
594
|
+
status: statusData,
|
|
595
|
+
exposure: exposure.data,
|
|
596
|
+
exposure_guide: exposureGuide.data,
|
|
597
|
+
web_guide: webGuide.data,
|
|
598
|
+
next_steps: ready
|
|
599
|
+
? ['dashboard runtime + web exposure look healthy']
|
|
600
|
+
: [
|
|
601
|
+
'run /plashboard quickstart "<description>" if no templates exist',
|
|
602
|
+
'run /plashboard web-guide and start local UI server',
|
|
603
|
+
'run /plashboard expose-guide and apply tailscale mapping',
|
|
604
|
+
're-run /plashboard doctor'
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
453
610
|
export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
454
611
|
const config = resolveConfig(api);
|
|
455
612
|
const runtimeCommand = api.runtime?.system?.runCommandWithTimeout;
|
|
@@ -528,6 +685,40 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
528
685
|
toToolResult(await runExposureCheck(config, params))
|
|
529
686
|
});
|
|
530
687
|
|
|
688
|
+
api.registerTool?.({
|
|
689
|
+
name: 'plashboard_web_guide',
|
|
690
|
+
description: 'Return exact commands to start the local plashboard web UI server.',
|
|
691
|
+
optional: true,
|
|
692
|
+
parameters: {
|
|
693
|
+
type: 'object',
|
|
694
|
+
properties: {
|
|
695
|
+
local_url: { type: 'string' },
|
|
696
|
+
repo_dir: { type: 'string' }
|
|
697
|
+
},
|
|
698
|
+
additionalProperties: false
|
|
699
|
+
},
|
|
700
|
+
execute: async (_toolCallId: unknown, params: WebGuideParams = {}) =>
|
|
701
|
+
toToolResult(await buildWebGuide(config, params))
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
api.registerTool?.({
|
|
705
|
+
name: 'plashboard_doctor',
|
|
706
|
+
description: 'Run full plashboard readiness checks (templates, local UI, and tailscale mapping).',
|
|
707
|
+
optional: true,
|
|
708
|
+
parameters: {
|
|
709
|
+
type: 'object',
|
|
710
|
+
properties: {
|
|
711
|
+
local_url: { type: 'string' },
|
|
712
|
+
tailscale_https_port: { type: 'number' },
|
|
713
|
+
dashboard_output_path: { type: 'string' },
|
|
714
|
+
repo_dir: { type: 'string' }
|
|
715
|
+
},
|
|
716
|
+
additionalProperties: false
|
|
717
|
+
},
|
|
718
|
+
execute: async (_toolCallId: unknown, params: DoctorParams = {}) =>
|
|
719
|
+
toToolResult(await runDoctor(runtime, config, params))
|
|
720
|
+
});
|
|
721
|
+
|
|
531
722
|
api.registerTool?.({
|
|
532
723
|
name: 'plashboard_setup',
|
|
533
724
|
description: 'Bootstrap or update plashboard plugin configuration in openclaw.json.',
|
|
@@ -538,6 +729,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
538
729
|
fill_provider: { type: 'string', enum: ['mock', 'command', 'openclaw'] },
|
|
539
730
|
fill_command: { type: 'string' },
|
|
540
731
|
openclaw_fill_agent_id: { type: 'string' },
|
|
732
|
+
auto_seed_template: { type: 'boolean' },
|
|
541
733
|
data_dir: { type: 'string' },
|
|
542
734
|
scheduler_tick_seconds: { type: 'number' },
|
|
543
735
|
session_timeout_seconds: { type: 'number' },
|
|
@@ -566,6 +758,26 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
566
758
|
execute: async () => toToolResult(await runtime.init())
|
|
567
759
|
});
|
|
568
760
|
|
|
761
|
+
api.registerTool?.({
|
|
762
|
+
name: 'plashboard_quickstart',
|
|
763
|
+
description: 'Create a first dashboard template from a short description, activate it, and run it once.',
|
|
764
|
+
optional: true,
|
|
765
|
+
parameters: {
|
|
766
|
+
type: 'object',
|
|
767
|
+
properties: {
|
|
768
|
+
description: { type: 'string' },
|
|
769
|
+
template_id: { type: 'string' },
|
|
770
|
+
template_name: { type: 'string' },
|
|
771
|
+
every_minutes: { type: 'number' },
|
|
772
|
+
activate: { type: 'boolean' },
|
|
773
|
+
run_now: { type: 'boolean' }
|
|
774
|
+
},
|
|
775
|
+
additionalProperties: false
|
|
776
|
+
},
|
|
777
|
+
execute: async (_toolCallId: unknown, params: QuickstartParams = {}) =>
|
|
778
|
+
toToolResult(await runQuickstart(runtime, config, params))
|
|
779
|
+
});
|
|
780
|
+
|
|
569
781
|
api.registerTool?.({
|
|
570
782
|
name: 'plashboard_template_create',
|
|
571
783
|
description: 'Create a new dashboard template.',
|
|
@@ -769,6 +981,28 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
769
981
|
})
|
|
770
982
|
);
|
|
771
983
|
}
|
|
984
|
+
if (cmd === 'web-guide') {
|
|
985
|
+
const localUrl = rest.find((token) => token.startsWith('http://') || token.startsWith('https://'));
|
|
986
|
+
const repoDir = rest.find((token) => token.startsWith('/'));
|
|
987
|
+
return toCommandResult(
|
|
988
|
+
await buildWebGuide(config, {
|
|
989
|
+
local_url: localUrl,
|
|
990
|
+
repo_dir: repoDir
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
if (cmd === 'doctor') {
|
|
995
|
+
const localUrl = rest.find((token) => token.startsWith('http://') || token.startsWith('https://'));
|
|
996
|
+
const portToken = rest.find((token) => /^[0-9]+$/.test(token));
|
|
997
|
+
const repoDir = rest.find((token) => token.startsWith('/'));
|
|
998
|
+
return toCommandResult(
|
|
999
|
+
await runDoctor(runtime, config, {
|
|
1000
|
+
local_url: localUrl,
|
|
1001
|
+
tailscale_https_port: portToken ? Number(portToken) : undefined,
|
|
1002
|
+
repo_dir: repoDir
|
|
1003
|
+
})
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
772
1006
|
if (cmd === 'setup') {
|
|
773
1007
|
const mode = asString(rest[0]).toLowerCase();
|
|
774
1008
|
const fillProvider = mode === 'command' || mode === 'mock' || mode === 'openclaw' ? mode : undefined;
|
|
@@ -782,6 +1016,10 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
782
1016
|
})
|
|
783
1017
|
);
|
|
784
1018
|
}
|
|
1019
|
+
if (cmd === 'quickstart') {
|
|
1020
|
+
const description = rest.join(' ').trim() || undefined;
|
|
1021
|
+
return toCommandResult(await runQuickstart(runtime, config, { description }));
|
|
1022
|
+
}
|
|
785
1023
|
if (cmd === 'init') return toCommandResult(await runtime.init());
|
|
786
1024
|
if (cmd === 'status') return toCommandResult(await runtime.status());
|
|
787
1025
|
if (cmd === 'list') return toCommandResult(await runtime.templateList());
|
|
@@ -806,7 +1044,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
806
1044
|
return toCommandResult({
|
|
807
1045
|
ok: false,
|
|
808
1046
|
errors: [
|
|
809
|
-
'unknown command. supported: setup [openclaw [agent_id]|mock|command <fill_command>], expose-guide [local_url] [https_port], expose-check [local_url] [https_port], init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
|
|
1047
|
+
'unknown command. supported: setup [openclaw [agent_id]|mock|command <fill_command>], quickstart <description>, doctor [local_url] [https_port] [repo_dir], web-guide [local_url] [repo_dir], expose-guide [local_url] [https_port], expose-check [local_url] [https_port], init, status, list, activate <id>, delete <id>, copy <src> <new-id> [new-name] [activate], run <id>, set-display <width> <height> <top> <bottom>'
|
|
810
1048
|
]
|
|
811
1049
|
});
|
|
812
1050
|
}
|
package/src/runtime.test.ts
CHANGED
|
@@ -51,7 +51,7 @@ function template(id: string): DashboardTemplate {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
async function setupRuntime() {
|
|
54
|
+
async function setupRuntime(overrides: Partial<PlashboardConfig> = {}) {
|
|
55
55
|
const root = await mkdtemp(join(tmpdir(), 'plashboard-test-'));
|
|
56
56
|
const config: PlashboardConfig = {
|
|
57
57
|
data_dir: root,
|
|
@@ -61,6 +61,7 @@ async function setupRuntime() {
|
|
|
61
61
|
default_retry_count: 0,
|
|
62
62
|
retry_backoff_seconds: 1,
|
|
63
63
|
session_timeout_seconds: 30,
|
|
64
|
+
auto_seed_template: false,
|
|
64
65
|
fill_provider: 'mock',
|
|
65
66
|
fill_command: undefined,
|
|
66
67
|
python_bin: 'python3',
|
|
@@ -75,7 +76,8 @@ async function setupRuntime() {
|
|
|
75
76
|
safe_side_px: 28,
|
|
76
77
|
layout_safety_margin_px: 24
|
|
77
78
|
},
|
|
78
|
-
model_defaults: {}
|
|
79
|
+
model_defaults: {},
|
|
80
|
+
...overrides
|
|
79
81
|
};
|
|
80
82
|
|
|
81
83
|
const runtime = new PlashboardRuntime(config);
|
|
@@ -160,4 +162,41 @@ describe('PlashboardRuntime', () => {
|
|
|
160
162
|
await rm(root, { recursive: true, force: true });
|
|
161
163
|
}
|
|
162
164
|
});
|
|
165
|
+
|
|
166
|
+
it('auto-seeds a starter template when enabled', async () => {
|
|
167
|
+
const { runtime, root } = await setupRuntime({ auto_seed_template: true });
|
|
168
|
+
try {
|
|
169
|
+
const list = await runtime.templateList();
|
|
170
|
+
expect(list.ok).toBe(true);
|
|
171
|
+
const templates = list.data?.templates ?? [];
|
|
172
|
+
expect(templates.length).toBe(1);
|
|
173
|
+
expect(String(templates[0]?.id)).toBe('starter');
|
|
174
|
+
|
|
175
|
+
const status = await runtime.status();
|
|
176
|
+
expect(status.ok).toBe(true);
|
|
177
|
+
expect(status.data?.active_template_id).toBe('starter');
|
|
178
|
+
} finally {
|
|
179
|
+
await rm(root, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('quickstart creates, activates, and runs once', async () => {
|
|
184
|
+
const { runtime, root, config } = await setupRuntime({ auto_seed_template: false });
|
|
185
|
+
try {
|
|
186
|
+
const quickstart = await runtime.quickstart({
|
|
187
|
+
description: 'Focus on homelab health, priorities, blockers, and next actions.'
|
|
188
|
+
});
|
|
189
|
+
expect(quickstart.ok).toBe(true);
|
|
190
|
+
const templateId = String(quickstart.data?.template_id || '');
|
|
191
|
+
expect(templateId).toBeTruthy();
|
|
192
|
+
expect(quickstart.data?.active_template_id).toBe(templateId);
|
|
193
|
+
expect(quickstart.data?.run_status).toBe('success');
|
|
194
|
+
|
|
195
|
+
const published = JSON.parse(await readFile(config.dashboard_output_path, 'utf8')) as Record<string, unknown>;
|
|
196
|
+
expect(published.version).toBe('3.0');
|
|
197
|
+
expect(typeof published.generated_at).toBe('string');
|
|
198
|
+
} finally {
|
|
199
|
+
await rm(root, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
163
202
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -31,6 +31,25 @@ const NOOP_LOGGER: Logger = {
|
|
|
31
31
|
error: () => {}
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
function clampInt(value: number, min: number, max: number): number {
|
|
35
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeTemplateId(input: string): string {
|
|
39
|
+
const cleaned = input
|
|
40
|
+
.trim()
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
43
|
+
.replace(/^-+/, '')
|
|
44
|
+
.replace(/-+$/, '')
|
|
45
|
+
.replace(/--+/g, '-');
|
|
46
|
+
|
|
47
|
+
if (!cleaned) return '';
|
|
48
|
+
const limited = cleaned.slice(0, 64);
|
|
49
|
+
if (!/^[a-z0-9]/.test(limited)) return '';
|
|
50
|
+
return limited;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
function resolveLastAttemptMs(state: PlashboardState, templateId: string): number | null {
|
|
35
54
|
const runState = state.template_runs[templateId];
|
|
36
55
|
if (!runState) return null;
|
|
@@ -100,32 +119,48 @@ export class PlashboardRuntime {
|
|
|
100
119
|
await this.paths.ensure();
|
|
101
120
|
await this.loadState();
|
|
102
121
|
|
|
103
|
-
const templates = await this.templateStore.list();
|
|
104
122
|
const state = await this.loadState();
|
|
123
|
+
let templates = await this.templateStore.list();
|
|
124
|
+
let autoSeededTemplateId: string | null = null;
|
|
125
|
+
let autoSeededSource: 'live_dashboard' | 'starter' | null = null;
|
|
105
126
|
|
|
106
127
|
if (!state.display_profile) {
|
|
107
128
|
state.display_profile = this.config.display_profile;
|
|
108
129
|
await this.saveState(state);
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
if (!templates.length) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
if (!templates.length && this.config.auto_seed_template) {
|
|
133
|
+
autoSeededTemplateId = await this.seedTemplateFromLiveDashboard();
|
|
134
|
+
autoSeededSource = autoSeededTemplateId ? 'live_dashboard' : null;
|
|
135
|
+
|
|
136
|
+
if (!autoSeededTemplateId) {
|
|
137
|
+
autoSeededTemplateId = await this.seedStarterTemplate({
|
|
138
|
+
preferredId: 'starter',
|
|
139
|
+
name: 'Starter Dashboard',
|
|
140
|
+
description: 'A first-run dashboard that summarizes priorities, risks, and next actions.',
|
|
141
|
+
everyMinutes: 15
|
|
142
|
+
});
|
|
143
|
+
autoSeededSource = 'starter';
|
|
119
144
|
}
|
|
120
145
|
}
|
|
121
146
|
|
|
147
|
+
templates = await this.templateStore.list();
|
|
148
|
+
if (!state.active_template_id && templates.length) {
|
|
149
|
+
state.active_template_id = autoSeededTemplateId || templates[0].id;
|
|
150
|
+
await this.saveState(state);
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
return {
|
|
123
154
|
ok: true,
|
|
124
155
|
errors: [],
|
|
125
156
|
data: {
|
|
126
157
|
data_dir: this.paths.dataDir,
|
|
127
158
|
dashboard_output_path: this.paths.liveDashboardPath,
|
|
128
|
-
scheduler_tick_seconds: this.config.scheduler_tick_seconds
|
|
159
|
+
scheduler_tick_seconds: this.config.scheduler_tick_seconds,
|
|
160
|
+
template_count: templates.length,
|
|
161
|
+
active_template_id: state.active_template_id,
|
|
162
|
+
auto_seeded_template_id: autoSeededTemplateId,
|
|
163
|
+
auto_seeded_source: autoSeededSource
|
|
129
164
|
}
|
|
130
165
|
};
|
|
131
166
|
}
|
|
@@ -333,6 +368,88 @@ export class PlashboardRuntime {
|
|
|
333
368
|
};
|
|
334
369
|
}
|
|
335
370
|
|
|
371
|
+
async quickstart(params: {
|
|
372
|
+
description?: string;
|
|
373
|
+
template_id?: string;
|
|
374
|
+
template_name?: string;
|
|
375
|
+
every_minutes?: number;
|
|
376
|
+
activate?: boolean;
|
|
377
|
+
run_now?: boolean;
|
|
378
|
+
} = {}): Promise<ToolResponse<Record<string, unknown>>> {
|
|
379
|
+
const description = (params.description || '').trim()
|
|
380
|
+
|| 'Create a high-signal operational dashboard with concise summaries and actionable updates.';
|
|
381
|
+
const everyMinutes = clampInt(
|
|
382
|
+
Number.isFinite(params.every_minutes) ? Number(params.every_minutes) : 15,
|
|
383
|
+
1,
|
|
384
|
+
24 * 60
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const requestedId = (params.template_id || '').trim();
|
|
388
|
+
if (requestedId && !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(requestedId)) {
|
|
389
|
+
return { ok: false, errors: ['template_id is invalid'] };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const resolvedTemplateId = requestedId
|
|
393
|
+
? await this.allocateTemplateId(requestedId)
|
|
394
|
+
: await this.allocateTemplateId('quickstart');
|
|
395
|
+
|
|
396
|
+
const templateName = (params.template_name || '').trim() || 'Quickstart Dashboard';
|
|
397
|
+
const template = this.buildStarterTemplate({
|
|
398
|
+
id: resolvedTemplateId,
|
|
399
|
+
name: templateName,
|
|
400
|
+
description,
|
|
401
|
+
everyMinutes
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const createResult = await this.templateCreate(template);
|
|
405
|
+
if (!createResult.ok) return createResult;
|
|
406
|
+
|
|
407
|
+
const errors: string[] = [];
|
|
408
|
+
let activeTemplateId: string | null = null;
|
|
409
|
+
|
|
410
|
+
const shouldActivate = params.activate !== false;
|
|
411
|
+
if (shouldActivate) {
|
|
412
|
+
const activateResult = await this.templateActivate(resolvedTemplateId);
|
|
413
|
+
if (!activateResult.ok) {
|
|
414
|
+
errors.push(...activateResult.errors.map((entry) => `activate: ${entry}`));
|
|
415
|
+
} else {
|
|
416
|
+
activeTemplateId = activateResult.data?.active_template_id || null;
|
|
417
|
+
}
|
|
418
|
+
} else {
|
|
419
|
+
const state = await this.loadState();
|
|
420
|
+
activeTemplateId = state.active_template_id;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let runStatus: string | null = null;
|
|
424
|
+
let published = false;
|
|
425
|
+
let runErrors: string[] = [];
|
|
426
|
+
const shouldRunNow = params.run_now !== false;
|
|
427
|
+
if (shouldRunNow) {
|
|
428
|
+
const runResult = await this.runNow(resolvedTemplateId);
|
|
429
|
+
runStatus = String(runResult.data?.status || '');
|
|
430
|
+
published = Boolean(runResult.data?.published);
|
|
431
|
+
runErrors = runResult.errors;
|
|
432
|
+
if (!runResult.ok) {
|
|
433
|
+
errors.push(...runResult.errors.map((entry) => `run: ${entry}`));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
ok: errors.length === 0,
|
|
439
|
+
errors,
|
|
440
|
+
data: {
|
|
441
|
+
template_id: resolvedTemplateId,
|
|
442
|
+
template_name: templateName,
|
|
443
|
+
schedule_every_minutes: everyMinutes,
|
|
444
|
+
active_template_id: activeTemplateId,
|
|
445
|
+
run_now: shouldRunNow,
|
|
446
|
+
run_status: runStatus,
|
|
447
|
+
run_published: published,
|
|
448
|
+
run_errors: runErrors
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
336
453
|
async status(): Promise<ToolResponse<RuntimeStatus>> {
|
|
337
454
|
const state = await this.loadState();
|
|
338
455
|
const templates = await this.templateStore.list();
|
|
@@ -571,18 +688,19 @@ export class PlashboardRuntime {
|
|
|
571
688
|
return state.display_profile || this.config.display_profile;
|
|
572
689
|
}
|
|
573
690
|
|
|
574
|
-
private async
|
|
691
|
+
private async seedTemplateFromLiveDashboard(): Promise<string | null> {
|
|
575
692
|
try {
|
|
576
693
|
await access(this.paths.liveDashboardPath, fsConstants.R_OK);
|
|
577
694
|
} catch {
|
|
578
|
-
return
|
|
695
|
+
return null;
|
|
579
696
|
}
|
|
580
697
|
|
|
581
698
|
const text = await readFile(this.paths.liveDashboardPath, 'utf8');
|
|
582
699
|
const dashboard = JSON.parse(text) as Record<string, unknown>;
|
|
700
|
+
const templateId = await this.allocateTemplateId('default');
|
|
583
701
|
|
|
584
702
|
const template: DashboardTemplate = {
|
|
585
|
-
id:
|
|
703
|
+
id: templateId,
|
|
586
704
|
name: 'Default Dashboard Template',
|
|
587
705
|
enabled: true,
|
|
588
706
|
schedule: {
|
|
@@ -602,7 +720,217 @@ export class PlashboardRuntime {
|
|
|
602
720
|
};
|
|
603
721
|
|
|
604
722
|
await this.templateStore.upsert(template);
|
|
605
|
-
return
|
|
723
|
+
return templateId;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private async seedStarterTemplate(params: {
|
|
727
|
+
preferredId: string;
|
|
728
|
+
name: string;
|
|
729
|
+
description: string;
|
|
730
|
+
everyMinutes: number;
|
|
731
|
+
}): Promise<string> {
|
|
732
|
+
const id = await this.allocateTemplateId(params.preferredId);
|
|
733
|
+
const template = this.buildStarterTemplate({
|
|
734
|
+
id,
|
|
735
|
+
name: params.name,
|
|
736
|
+
description: params.description,
|
|
737
|
+
everyMinutes: clampInt(params.everyMinutes, 1, 24 * 60)
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const validationErrors = await this.validateTemplate(template);
|
|
741
|
+
if (validationErrors.length) {
|
|
742
|
+
throw new Error(`starter template validation failed: ${validationErrors.join('; ')}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
await this.templateStore.upsert(template);
|
|
746
|
+
return id;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private async allocateTemplateId(preferredId: string): Promise<string> {
|
|
750
|
+
const normalized = normalizeTemplateId(preferredId) || 'starter';
|
|
751
|
+
const existing = await this.templateStore.get(normalized);
|
|
752
|
+
if (!existing) return normalized;
|
|
753
|
+
|
|
754
|
+
const base = normalized.slice(0, 58);
|
|
755
|
+
for (let i = 2; i <= 99; i += 1) {
|
|
756
|
+
const candidate = `${base}-${i}`;
|
|
757
|
+
const found = await this.templateStore.get(candidate);
|
|
758
|
+
if (!found) return candidate;
|
|
759
|
+
}
|
|
760
|
+
return `${base}-${Date.now()}`.slice(0, 64);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private buildStarterTemplate(params: {
|
|
764
|
+
id: string;
|
|
765
|
+
name: string;
|
|
766
|
+
description: string;
|
|
767
|
+
everyMinutes: number;
|
|
768
|
+
}): DashboardTemplate {
|
|
769
|
+
const description = params.description.trim();
|
|
770
|
+
return {
|
|
771
|
+
id: params.id,
|
|
772
|
+
name: params.name,
|
|
773
|
+
enabled: true,
|
|
774
|
+
schedule: {
|
|
775
|
+
mode: 'interval',
|
|
776
|
+
every_minutes: params.everyMinutes,
|
|
777
|
+
timezone: this.config.timezone
|
|
778
|
+
},
|
|
779
|
+
base_dashboard: {
|
|
780
|
+
title: params.name,
|
|
781
|
+
summary: 'Preparing summary…',
|
|
782
|
+
ui: {
|
|
783
|
+
timezone: this.config.timezone
|
|
784
|
+
},
|
|
785
|
+
sections: [
|
|
786
|
+
{
|
|
787
|
+
id: 'overview',
|
|
788
|
+
label: 'Overview',
|
|
789
|
+
cards: [
|
|
790
|
+
{
|
|
791
|
+
id: 'pulse',
|
|
792
|
+
title: 'System Pulse',
|
|
793
|
+
description: 'Waiting for first run…',
|
|
794
|
+
long_description: 'This card is filled by OpenClaw on each run.'
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
id: 'priorities',
|
|
798
|
+
title: 'Top Priorities',
|
|
799
|
+
description: 'Waiting for first run…',
|
|
800
|
+
long_description: 'This card highlights what matters most right now.'
|
|
801
|
+
}
|
|
802
|
+
]
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
id: 'operations',
|
|
806
|
+
label: 'Operations',
|
|
807
|
+
cards: [
|
|
808
|
+
{
|
|
809
|
+
id: 'risks',
|
|
810
|
+
title: 'Risks to Watch',
|
|
811
|
+
description: 'Waiting for first run…'
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
id: 'blockers',
|
|
815
|
+
title: 'Potential Blockers',
|
|
816
|
+
description: 'Waiting for first run…'
|
|
817
|
+
}
|
|
818
|
+
]
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
id: 'actions',
|
|
822
|
+
label: 'Actions',
|
|
823
|
+
cards: [
|
|
824
|
+
{
|
|
825
|
+
id: 'next_steps',
|
|
826
|
+
title: 'Next Actions',
|
|
827
|
+
description: 'Waiting for first run…'
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
id: 'questions',
|
|
831
|
+
title: 'Open Questions',
|
|
832
|
+
description: 'Waiting for first run…'
|
|
833
|
+
}
|
|
834
|
+
]
|
|
835
|
+
}
|
|
836
|
+
],
|
|
837
|
+
alerts: []
|
|
838
|
+
},
|
|
839
|
+
fields: [
|
|
840
|
+
{
|
|
841
|
+
id: 'summary',
|
|
842
|
+
pointer: '/summary',
|
|
843
|
+
type: 'string',
|
|
844
|
+
prompt: 'Write one concise dashboard summary (max 260 chars).',
|
|
845
|
+
required: true,
|
|
846
|
+
constraints: { max_len: 260 }
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
id: 'pulse',
|
|
850
|
+
pointer: '/sections/0/cards/0/description',
|
|
851
|
+
type: 'string',
|
|
852
|
+
prompt: 'Describe current pulse/status in one short paragraph.',
|
|
853
|
+
required: true,
|
|
854
|
+
constraints: { max_len: 280 }
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
id: 'pulse_details',
|
|
858
|
+
pointer: '/sections/0/cards/0/long_description',
|
|
859
|
+
type: 'string',
|
|
860
|
+
prompt: 'Provide richer context behind the pulse status with concrete facts.',
|
|
861
|
+
required: true,
|
|
862
|
+
constraints: { max_len: 900 }
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
id: 'priorities',
|
|
866
|
+
pointer: '/sections/0/cards/1/description',
|
|
867
|
+
type: 'string',
|
|
868
|
+
prompt: 'State the top priorities right now as a compact sentence.',
|
|
869
|
+
required: true,
|
|
870
|
+
constraints: { max_len: 280 }
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
id: 'priorities_details',
|
|
874
|
+
pointer: '/sections/0/cards/1/long_description',
|
|
875
|
+
type: 'string',
|
|
876
|
+
prompt: 'Explain why these priorities matter and what outcome is expected.',
|
|
877
|
+
required: true,
|
|
878
|
+
constraints: { max_len: 900 }
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
id: 'risks',
|
|
882
|
+
pointer: '/sections/1/cards/0/description',
|
|
883
|
+
type: 'string',
|
|
884
|
+
prompt: 'Describe the most relevant risks and their likely impact.',
|
|
885
|
+
required: true,
|
|
886
|
+
constraints: { max_len: 320 }
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: 'blockers',
|
|
890
|
+
pointer: '/sections/1/cards/1/description',
|
|
891
|
+
type: 'string',
|
|
892
|
+
prompt: 'List active blockers and the dependency needed to unblock.',
|
|
893
|
+
required: true,
|
|
894
|
+
constraints: { max_len: 320 }
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: 'next_steps',
|
|
898
|
+
pointer: '/sections/2/cards/0/description',
|
|
899
|
+
type: 'string',
|
|
900
|
+
prompt: 'Provide immediate next steps with owners/time hints if possible.',
|
|
901
|
+
required: true,
|
|
902
|
+
constraints: { max_len: 320 }
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
id: 'questions',
|
|
906
|
+
pointer: '/sections/2/cards/1/description',
|
|
907
|
+
type: 'string',
|
|
908
|
+
prompt: 'List open questions that need decisions soon.',
|
|
909
|
+
required: true,
|
|
910
|
+
constraints: { max_len: 320 }
|
|
911
|
+
}
|
|
912
|
+
],
|
|
913
|
+
context: {
|
|
914
|
+
dashboard_prompt: `Dashboard objective: ${description}`,
|
|
915
|
+
section_prompts: {
|
|
916
|
+
overview: 'Summarize high-level status and priorities for quick scanning.',
|
|
917
|
+
operations: 'Highlight operational risks and blockers with practical impact.',
|
|
918
|
+
actions: 'Focus on actionable next steps and unresolved decision points.'
|
|
919
|
+
},
|
|
920
|
+
card_prompts: {
|
|
921
|
+
pulse: `Context: ${description}. Keep it factual and concise.`,
|
|
922
|
+
priorities: 'Surface the top priorities and expected outcomes.',
|
|
923
|
+
risks: 'Call out risks that deserve active monitoring.',
|
|
924
|
+
blockers: 'Describe blockers and what would resolve them.',
|
|
925
|
+
next_steps: 'Give concrete next actions.',
|
|
926
|
+
questions: 'Capture open questions requiring a decision.'
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
run: {
|
|
930
|
+
retry_count: this.config.default_retry_count,
|
|
931
|
+
repair_attempts: 1
|
|
932
|
+
}
|
|
933
|
+
};
|
|
606
934
|
}
|
|
607
935
|
|
|
608
936
|
private async writeRenderedSnapshot(templateId: string, payload: Record<string, unknown>): Promise<void> {
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface PlashboardConfig {
|
|
|
23
23
|
default_retry_count: number;
|
|
24
24
|
retry_backoff_seconds: number;
|
|
25
25
|
session_timeout_seconds: number;
|
|
26
|
+
auto_seed_template: boolean;
|
|
26
27
|
fill_provider: 'command' | 'mock' | 'openclaw';
|
|
27
28
|
fill_command?: string;
|
|
28
29
|
openclaw_fill_agent_id?: string;
|