@jhytabest/plashboard 0.1.7 → 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 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
- ## Minimal Config
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 setup openclaw
78
- /plashboard init
94
+ /plashboard quickstart "Focus on service health, priorities, blockers, and next actions."
79
95
  ```
80
96
 
81
97
  Tailscale helper flow:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "plashboard",
3
3
  "name": "Plashboard",
4
- "version": "0.1.7",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhytabest/plashboard",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "description": "Plashboard OpenClaw plugin runtime",
6
6
  "license": "MIT",
@@ -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'),
@@ -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' },
@@ -566,6 +586,26 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
566
586
  execute: async () => toToolResult(await runtime.init())
567
587
  });
568
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
+
569
609
  api.registerTool?.({
570
610
  name: 'plashboard_template_create',
571
611
  description: 'Create a new dashboard template.',
@@ -782,6 +822,10 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
782
822
  })
783
823
  );
784
824
  }
825
+ if (cmd === 'quickstart') {
826
+ const description = rest.join(' ').trim() || undefined;
827
+ return toCommandResult(await runtime.quickstart({ description }));
828
+ }
785
829
  if (cmd === 'init') return toCommandResult(await runtime.init());
786
830
  if (cmd === 'status') return toCommandResult(await runtime.status());
787
831
  if (cmd === 'list') return toCommandResult(await runtime.templateList());
@@ -806,7 +850,7 @@ export function registerPlashboardPlugin(api: UnknownApi): void {
806
850
  return toCommandResult({
807
851
  ok: false,
808
852
  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>'
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>'
810
854
  ]
811
855
  });
812
856
  }
@@ -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
- const seeded = await this.seedDefaultTemplate();
113
- if (seeded) {
114
- const next = await this.templateStore.list();
115
- if (!state.active_template_id && next.length) {
116
- state.active_template_id = next[0].id;
117
- await this.saveState(state);
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 seedDefaultTemplate(): Promise<boolean> {
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 false;
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: 'default',
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 true;
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;