@jhytabest/plashboard 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/skills/plashboard-admin/SKILL.md +2 -0
- package/src/config.ts +11 -0
- package/src/fill-runner.test.ts +1 -0
- package/src/plugin.ts +60 -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,7 @@ 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>
|
|
62
79
|
/plashboard expose-guide [local_url] [https_port]
|
|
63
80
|
/plashboard expose-check [local_url] [https_port]
|
|
64
81
|
/plashboard init
|
|
@@ -74,8 +91,7 @@ Use `fill_provider: "command"` only if you need a custom external runner.
|
|
|
74
91
|
Recommended first run:
|
|
75
92
|
|
|
76
93
|
```text
|
|
77
|
-
/plashboard
|
|
78
|
-
/plashboard init
|
|
94
|
+
/plashboard quickstart "Focus on service health, priorities, blockers, and next actions."
|
|
79
95
|
```
|
|
80
96
|
|
|
81
97
|
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.8",
|
|
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
|
@@ -21,6 +21,7 @@ Always use plugin tools:
|
|
|
21
21
|
- `plashboard_exposure_guide`
|
|
22
22
|
- `plashboard_exposure_check`
|
|
23
23
|
- `plashboard_init`
|
|
24
|
+
- `plashboard_quickstart`
|
|
24
25
|
- `plashboard_template_create`
|
|
25
26
|
- `plashboard_template_update`
|
|
26
27
|
- `plashboard_template_list`
|
|
@@ -39,6 +40,7 @@ Always use plugin tools:
|
|
|
39
40
|
- Never ask the model to generate full dashboard structure when filling values.
|
|
40
41
|
|
|
41
42
|
## Command Shortcuts
|
|
43
|
+
- `/plashboard quickstart <description>`
|
|
42
44
|
- `/plashboard setup [openclaw [agent_id]|mock|command <fill_command>]`
|
|
43
45
|
- `/plashboard expose-guide [local_url] [https_port]`
|
|
44
46
|
- `/plashboard expose-check [local_url] [https_port]`
|
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,6 +102,15 @@ 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;
|
|
@@ -357,6 +367,13 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
357
367
|
|| asString(currentPluginConfig.fill_command)
|
|
358
368
|
|| asString(resolvedConfig.fill_command)
|
|
359
369
|
).trim();
|
|
370
|
+
const selectedAutoSeed = (
|
|
371
|
+
typeof params.auto_seed_template === 'boolean'
|
|
372
|
+
? params.auto_seed_template
|
|
373
|
+
: typeof currentPluginConfig.auto_seed_template === 'boolean'
|
|
374
|
+
? currentPluginConfig.auto_seed_template
|
|
375
|
+
: resolvedConfig.auto_seed_template
|
|
376
|
+
);
|
|
360
377
|
const selectedAgentId = (
|
|
361
378
|
params.openclaw_fill_agent_id
|
|
362
379
|
|| asString(currentPluginConfig.openclaw_fill_agent_id)
|
|
@@ -397,6 +414,7 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
397
414
|
)
|
|
398
415
|
),
|
|
399
416
|
fill_provider: selectedProvider,
|
|
417
|
+
auto_seed_template: selectedAutoSeed,
|
|
400
418
|
display_profile: displayProfile
|
|
401
419
|
};
|
|
402
420
|
|
|
@@ -438,6 +456,7 @@ async function runSetup(api: UnknownApi, resolvedConfig: ReturnType<typeof resol
|
|
|
438
456
|
fill_provider: selectedProvider,
|
|
439
457
|
fill_command: selectedProvider === 'command' ? selectedCommand : undefined,
|
|
440
458
|
openclaw_fill_agent_id: selectedProvider === 'openclaw' ? selectedAgentId : undefined,
|
|
459
|
+
auto_seed_template: selectedAutoSeed,
|
|
441
460
|
data_dir: nextPluginConfig.data_dir,
|
|
442
461
|
scheduler_tick_seconds: nextPluginConfig.scheduler_tick_seconds,
|
|
443
462
|
session_timeout_seconds: nextPluginConfig.session_timeout_seconds,
|
|
@@ -538,6 +557,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
538
557
|
fill_provider: { type: 'string', enum: ['mock', 'command', 'openclaw'] },
|
|
539
558
|
fill_command: { type: 'string' },
|
|
540
559
|
openclaw_fill_agent_id: { type: 'string' },
|
|
560
|
+
auto_seed_template: { type: 'boolean' },
|
|
541
561
|
data_dir: { type: 'string' },
|
|
542
562
|
scheduler_tick_seconds: { type: 'number' },
|
|
543
563
|
session_timeout_seconds: { type: 'number' },
|
|
@@ -558,9 +578,34 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
558
578
|
name: 'plashboard_init',
|
|
559
579
|
description: 'Initialize plashboard state directories and optional default template.',
|
|
560
580
|
optional: true,
|
|
581
|
+
parameters: {
|
|
582
|
+
type: 'object',
|
|
583
|
+
properties: {},
|
|
584
|
+
additionalProperties: false
|
|
585
|
+
},
|
|
561
586
|
execute: async () => toToolResult(await runtime.init())
|
|
562
587
|
});
|
|
563
588
|
|
|
589
|
+
api.registerTool?.({
|
|
590
|
+
name: 'plashboard_quickstart',
|
|
591
|
+
description: 'Create a first dashboard template from a short description, activate it, and run it once.',
|
|
592
|
+
optional: true,
|
|
593
|
+
parameters: {
|
|
594
|
+
type: 'object',
|
|
595
|
+
properties: {
|
|
596
|
+
description: { type: 'string' },
|
|
597
|
+
template_id: { type: 'string' },
|
|
598
|
+
template_name: { type: 'string' },
|
|
599
|
+
every_minutes: { type: 'number' },
|
|
600
|
+
activate: { type: 'boolean' },
|
|
601
|
+
run_now: { type: 'boolean' }
|
|
602
|
+
},
|
|
603
|
+
additionalProperties: false
|
|
604
|
+
},
|
|
605
|
+
execute: async (_toolCallId: unknown, params: QuickstartParams = {}) =>
|
|
606
|
+
toToolResult(await runtime.quickstart(params))
|
|
607
|
+
});
|
|
608
|
+
|
|
564
609
|
api.registerTool?.({
|
|
565
610
|
name: 'plashboard_template_create',
|
|
566
611
|
description: 'Create a new dashboard template.',
|
|
@@ -598,6 +643,11 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
598
643
|
name: 'plashboard_template_list',
|
|
599
644
|
description: 'List available dashboard templates with schedule and run state.',
|
|
600
645
|
optional: true,
|
|
646
|
+
parameters: {
|
|
647
|
+
type: 'object',
|
|
648
|
+
properties: {},
|
|
649
|
+
additionalProperties: false
|
|
650
|
+
},
|
|
601
651
|
execute: async () => toToolResult(await runtime.templateList())
|
|
602
652
|
});
|
|
603
653
|
|
|
@@ -703,6 +753,11 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
703
753
|
name: 'plashboard_status',
|
|
704
754
|
description: 'Read current plashboard runtime status.',
|
|
705
755
|
optional: true,
|
|
756
|
+
parameters: {
|
|
757
|
+
type: 'object',
|
|
758
|
+
properties: {},
|
|
759
|
+
additionalProperties: false
|
|
760
|
+
},
|
|
706
761
|
execute: async () => toToolResult(await runtime.status())
|
|
707
762
|
});
|
|
708
763
|
|
|
@@ -767,6 +822,10 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
767
822
|
})
|
|
768
823
|
);
|
|
769
824
|
}
|
|
825
|
+
if (cmd === 'quickstart') {
|
|
826
|
+
const description = rest.join(' ').trim() || undefined;
|
|
827
|
+
return toCommandResult(await runtime.quickstart({ description }));
|
|
828
|
+
}
|
|
770
829
|
if (cmd === 'init') return toCommandResult(await runtime.init());
|
|
771
830
|
if (cmd === 'status') return toCommandResult(await runtime.status());
|
|
772
831
|
if (cmd === 'list') return toCommandResult(await runtime.templateList());
|
|
@@ -791,7 +850,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
|
|
|
791
850
|
return toCommandResult({
|
|
792
851
|
ok: false,
|
|
793
852
|
errors: [
|
|
794
|
-
'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>'
|
|
853
|
+
'unknown command. supported: setup [openclaw [agent_id]|mock|command <fill_command>], quickstart <description>, 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>'
|
|
795
854
|
]
|
|
796
855
|
});
|
|
797
856
|
}
|
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;
|