@jhytabest/plashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import type { DisplayProfile, PlashboardConfig } from './types.js';
5
+
6
+ interface PublishOptions {
7
+ outputPath: string;
8
+ validateOnly: boolean;
9
+ displayProfile: DisplayProfile;
10
+ }
11
+
12
+ function spawnPython(
13
+ pythonBin: string,
14
+ scriptPath: string,
15
+ args: string[],
16
+ profile: DisplayProfile,
17
+ tolerance: number
18
+ ): Promise<string> {
19
+ return new Promise((resolve, reject) => {
20
+ const child = spawn(pythonBin, [scriptPath, ...args], {
21
+ env: {
22
+ ...process.env,
23
+ PLASH_TARGET_VIEWPORT_HEIGHT: String(profile.height_px),
24
+ PLASH_LAYOUT_SAFETY_MARGIN: String(profile.layout_safety_margin_px),
25
+ PLASH_LAYOUT_OVERFLOW_TOLERANCE: String(tolerance),
26
+ PLASH_FRAME_GUTTER_TOP: String(profile.safe_top_px),
27
+ PLASH_FRAME_GUTTER_BOTTOM: String(profile.safe_bottom_px)
28
+ }
29
+ });
30
+
31
+ let stdout = '';
32
+ let stderr = '';
33
+
34
+ child.stdout.on('data', (chunk) => {
35
+ stdout += String(chunk);
36
+ });
37
+ child.stderr.on('data', (chunk) => {
38
+ stderr += String(chunk);
39
+ });
40
+
41
+ child.on('error', (error) => reject(error));
42
+
43
+ child.on('close', (code) => {
44
+ if (code !== 0) {
45
+ reject(new Error(stderr.trim() || stdout.trim() || `writer script failed with code ${code}`));
46
+ return;
47
+ }
48
+ resolve(stdout.trim());
49
+ });
50
+ });
51
+ }
52
+
53
+ export class DashboardValidatorPublisher {
54
+ constructor(private readonly config: PlashboardConfig) {}
55
+
56
+ async validateOnly(payload: Record<string, unknown>, displayProfile: DisplayProfile): Promise<void> {
57
+ await this.run(payload, {
58
+ validateOnly: true,
59
+ outputPath: this.config.dashboard_output_path,
60
+ displayProfile
61
+ });
62
+ }
63
+
64
+ async publish(payload: Record<string, unknown>, displayProfile: DisplayProfile): Promise<string> {
65
+ return this.run(payload, {
66
+ validateOnly: false,
67
+ outputPath: this.config.dashboard_output_path,
68
+ displayProfile
69
+ });
70
+ }
71
+
72
+ private async run(payload: Record<string, unknown>, options: PublishOptions): Promise<string> {
73
+ const tempDir = await mkdtemp(join(dirname(options.outputPath), '.plashboard-run-'));
74
+ const inputPath = join(tempDir, 'dashboard.next.json');
75
+ try {
76
+ await writeFile(inputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
77
+
78
+ const args = ['--input', inputPath];
79
+ if (options.validateOnly) {
80
+ args.push('--validate-only');
81
+ } else {
82
+ args.push('--output', options.outputPath);
83
+ }
84
+
85
+ const output = await spawnPython(
86
+ this.config.python_bin,
87
+ this.config.writer_script_path,
88
+ args,
89
+ options.displayProfile,
90
+ this.config.layout_overflow_tolerance_px
91
+ );
92
+
93
+ return output;
94
+ } finally {
95
+ await rm(tempDir, { recursive: true, force: true });
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,163 @@
1
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { PlashboardRuntime } from './runtime.js';
6
+ import type { DashboardTemplate, PlashboardConfig } from './types.js';
7
+
8
+ function baseDashboard() {
9
+ return {
10
+ title: 'Dashboard',
11
+ summary: 'old summary',
12
+ ui: { timezone: 'Europe/Berlin' },
13
+ sections: [
14
+ {
15
+ id: 'main',
16
+ label: 'Main',
17
+ cards: [
18
+ {
19
+ id: 'card-1',
20
+ title: 'Card One',
21
+ description: 'desc'
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ alerts: []
27
+ };
28
+ }
29
+
30
+ function template(id: string): DashboardTemplate {
31
+ return {
32
+ id,
33
+ name: id,
34
+ enabled: true,
35
+ schedule: {
36
+ mode: 'interval',
37
+ every_minutes: 5,
38
+ timezone: 'Europe/Berlin'
39
+ },
40
+ base_dashboard: baseDashboard(),
41
+ fields: [
42
+ {
43
+ id: 'summary',
44
+ pointer: '/summary',
45
+ type: 'string',
46
+ prompt: 'Write summary',
47
+ required: true,
48
+ constraints: { max_len: 200 }
49
+ }
50
+ ]
51
+ };
52
+ }
53
+
54
+ async function setupRuntime() {
55
+ const root = await mkdtemp(join(tmpdir(), 'plashboard-test-'));
56
+ const config: PlashboardConfig = {
57
+ data_dir: root,
58
+ timezone: 'Europe/Berlin',
59
+ scheduler_tick_seconds: 30,
60
+ max_parallel_runs: 1,
61
+ default_retry_count: 0,
62
+ retry_backoff_seconds: 1,
63
+ session_timeout_seconds: 30,
64
+ fill_provider: 'mock',
65
+ fill_command: undefined,
66
+ python_bin: 'python3',
67
+ writer_script_path: join(process.cwd(), 'scripts', 'dashboard_write.py'),
68
+ dashboard_output_path: join(root, 'dashboard.json'),
69
+ layout_overflow_tolerance_px: 40,
70
+ display_profile: {
71
+ width_px: 1920,
72
+ height_px: 1080,
73
+ safe_top_px: 96,
74
+ safe_bottom_px: 106,
75
+ safe_side_px: 28,
76
+ layout_safety_margin_px: 24
77
+ },
78
+ model_defaults: {}
79
+ };
80
+
81
+ const runtime = new PlashboardRuntime(config);
82
+ await runtime.init();
83
+ return { runtime, root, config };
84
+ }
85
+
86
+ describe('PlashboardRuntime', () => {
87
+ it('creates template and runs pipeline with publish', async () => {
88
+ const { runtime, root, config } = await setupRuntime();
89
+ try {
90
+ const created = await runtime.templateCreate(template('ops'));
91
+ expect(created.ok).toBe(true);
92
+
93
+ const run = await runtime.runNow('ops');
94
+ expect(run.ok).toBe(true);
95
+ expect(run.data?.published).toBe(true);
96
+
97
+ const published = JSON.parse(await readFile(config.dashboard_output_path, 'utf8')) as Record<string, unknown>;
98
+ expect(published.version).toBe('3.0');
99
+ expect(typeof published.generated_at).toBe('string');
100
+ expect(String(published.summary)).toContain('updated summary');
101
+ } finally {
102
+ await rm(root, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ it('does not publish when template is inactive', async () => {
107
+ const { runtime, root, config } = await setupRuntime();
108
+ try {
109
+ expect((await runtime.templateCreate(template('one'))).ok).toBe(true);
110
+ expect((await runtime.templateCreate(template('two'))).ok).toBe(true);
111
+ expect((await runtime.templateActivate('one')).ok).toBe(true);
112
+ expect((await runtime.runNow('one')).ok).toBe(true);
113
+
114
+ const run = await runtime.runNow('two');
115
+ expect(run.ok).toBe(true);
116
+ expect(run.data?.published).toBe(false);
117
+
118
+ const published = JSON.parse(await readFile(config.dashboard_output_path, 'utf8')) as Record<string, unknown>;
119
+ expect(published).toBeTruthy();
120
+ } finally {
121
+ await rm(root, { recursive: true, force: true });
122
+ }
123
+ });
124
+
125
+ it('rejects template with invalid field pointer', async () => {
126
+ const { runtime, root } = await setupRuntime();
127
+ try {
128
+ const bad = template('bad');
129
+ bad.fields[0].pointer = '/sections/0/cards/0/unknown';
130
+ const result = await runtime.templateCreate(bad);
131
+ expect(result.ok).toBe(false);
132
+ expect(result.errors.join(' ')).toMatch(/pointer path not found|validation failed/i);
133
+ } finally {
134
+ await rm(root, { recursive: true, force: true });
135
+ }
136
+ });
137
+
138
+ it('copies and deletes templates', async () => {
139
+ const { runtime, root } = await setupRuntime();
140
+ try {
141
+ expect((await runtime.templateCreate(template('ops'))).ok).toBe(true);
142
+
143
+ const copied = await runtime.templateCopy('ops', 'ops-copy', 'Ops Copy', true);
144
+ expect(copied.ok).toBe(true);
145
+ expect(copied.data?.template_id).toBe('ops-copy');
146
+ expect(copied.data?.active_template_id).toBe('ops-copy');
147
+
148
+ const listAfterCopy = await runtime.templateList();
149
+ expect(listAfterCopy.ok).toBe(true);
150
+ expect(listAfterCopy.data?.templates.map((entry) => entry.id)).toEqual(['ops', 'ops-copy']);
151
+
152
+ const deleted = await runtime.templateDelete('ops-copy');
153
+ expect(deleted.ok).toBe(true);
154
+ expect(deleted.data?.deleted_template_id).toBe('ops-copy');
155
+
156
+ const listAfterDelete = await runtime.templateList();
157
+ expect(listAfterDelete.ok).toBe(true);
158
+ expect(listAfterDelete.data?.templates.map((entry) => entry.id)).toEqual(['ops']);
159
+ } finally {
160
+ await rm(root, { recursive: true, force: true });
161
+ }
162
+ });
163
+ });