@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.
package/src/stores.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { readdir, rm } from 'node:fs/promises';
2
+ import { basename, join } from 'node:path';
3
+ import type { DashboardTemplate, PlashboardConfig, PlashboardState, RunArtifact } from './types.js';
4
+ import { atomicWriteJson, ensureDir, readJsonFile } from './utils.js';
5
+
6
+ function emptyState(): PlashboardState {
7
+ return {
8
+ version: 1,
9
+ active_template_id: null,
10
+ template_runs: {}
11
+ };
12
+ }
13
+
14
+ export class Paths {
15
+ readonly dataDir: string;
16
+ readonly statePath: string;
17
+ readonly templatesDir: string;
18
+ readonly runsDir: string;
19
+ readonly renderedDir: string;
20
+ readonly liveDashboardPath: string;
21
+
22
+ constructor(config: PlashboardConfig) {
23
+ this.dataDir = config.data_dir;
24
+ this.statePath = join(this.dataDir, 'state.json');
25
+ this.templatesDir = join(this.dataDir, 'templates');
26
+ this.runsDir = join(this.dataDir, 'runs');
27
+ this.renderedDir = join(this.dataDir, 'rendered');
28
+ this.liveDashboardPath = config.dashboard_output_path;
29
+ }
30
+
31
+ async ensure(): Promise<void> {
32
+ await Promise.all([
33
+ ensureDir(this.dataDir),
34
+ ensureDir(this.templatesDir),
35
+ ensureDir(this.runsDir),
36
+ ensureDir(this.renderedDir)
37
+ ]);
38
+ }
39
+ }
40
+
41
+ export class StateStore {
42
+ constructor(private readonly paths: Paths) {}
43
+
44
+ async load(): Promise<PlashboardState> {
45
+ const value = await readJsonFile<PlashboardState>(this.paths.statePath);
46
+ if (!value) {
47
+ return emptyState();
48
+ }
49
+ return {
50
+ version: 1,
51
+ active_template_id: value.active_template_id ?? null,
52
+ template_runs: value.template_runs ?? {},
53
+ display_profile: value.display_profile
54
+ };
55
+ }
56
+
57
+ async save(state: PlashboardState): Promise<void> {
58
+ await atomicWriteJson(this.paths.statePath, state);
59
+ }
60
+ }
61
+
62
+ export class TemplateStore {
63
+ constructor(private readonly paths: Paths) {}
64
+
65
+ pathForId(templateId: string): string {
66
+ return join(this.paths.templatesDir, `${templateId}.json`);
67
+ }
68
+
69
+ async list(): Promise<DashboardTemplate[]> {
70
+ await ensureDir(this.paths.templatesDir);
71
+ const entries = await readdir(this.paths.templatesDir, { withFileTypes: true });
72
+ const templates: DashboardTemplate[] = [];
73
+
74
+ for (const entry of entries) {
75
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
76
+ const fullPath = join(this.paths.templatesDir, entry.name);
77
+ const loaded = await readJsonFile<DashboardTemplate>(fullPath);
78
+ if (!loaded) continue;
79
+ templates.push(loaded);
80
+ }
81
+
82
+ templates.sort((a, b) => a.id.localeCompare(b.id));
83
+ return templates;
84
+ }
85
+
86
+ async get(templateId: string): Promise<DashboardTemplate | null> {
87
+ return readJsonFile<DashboardTemplate>(this.pathForId(templateId));
88
+ }
89
+
90
+ async upsert(template: DashboardTemplate): Promise<void> {
91
+ await atomicWriteJson(this.pathForId(template.id), template);
92
+ }
93
+
94
+ async remove(templateId: string): Promise<void> {
95
+ await rm(this.pathForId(templateId), { force: true });
96
+ }
97
+ }
98
+
99
+ export class RunStore {
100
+ constructor(private readonly paths: Paths) {}
101
+
102
+ async write(templateId: string, artifact: RunArtifact): Promise<string> {
103
+ const safeTimestamp = artifact.started_at.replaceAll(':', '-');
104
+ const dir = join(this.paths.runsDir, templateId);
105
+ await ensureDir(dir);
106
+ const filePath = join(dir, `${safeTimestamp}.json`);
107
+ await atomicWriteJson(filePath, artifact);
108
+ return filePath;
109
+ }
110
+
111
+ async latestByTemplate(templateId: string, limit = 10): Promise<RunArtifact[]> {
112
+ const dir = join(this.paths.runsDir, templateId);
113
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
114
+ const files = entries
115
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
116
+ .map((entry) => join(dir, entry.name))
117
+ .sort((a, b) => basename(b).localeCompare(basename(a)))
118
+ .slice(0, limit);
119
+
120
+ const output: RunArtifact[] = [];
121
+ for (const file of files) {
122
+ const loaded = await readJsonFile<RunArtifact>(file);
123
+ if (loaded) output.push(loaded);
124
+ }
125
+ return output;
126
+ }
127
+ }
package/src/types.ts ADDED
@@ -0,0 +1,139 @@
1
+ export type FillFieldType = 'string' | 'number' | 'boolean' | 'array';
2
+
3
+ export interface DisplayProfile {
4
+ width_px: number;
5
+ height_px: number;
6
+ safe_top_px: number;
7
+ safe_bottom_px: number;
8
+ safe_side_px: number;
9
+ layout_safety_margin_px: number;
10
+ }
11
+
12
+ export interface ModelDefaults {
13
+ model?: string;
14
+ temperature?: number;
15
+ max_tokens?: number;
16
+ }
17
+
18
+ export interface PlashboardConfig {
19
+ data_dir: string;
20
+ timezone: string;
21
+ scheduler_tick_seconds: number;
22
+ max_parallel_runs: number;
23
+ default_retry_count: number;
24
+ retry_backoff_seconds: number;
25
+ session_timeout_seconds: number;
26
+ fill_provider: 'command' | 'mock';
27
+ fill_command?: string;
28
+ python_bin: string;
29
+ writer_script_path: string;
30
+ dashboard_output_path: string;
31
+ layout_overflow_tolerance_px: number;
32
+ display_profile: DisplayProfile;
33
+ model_defaults: ModelDefaults;
34
+ }
35
+
36
+ export interface TemplateSchedule {
37
+ mode: 'interval';
38
+ every_minutes: number;
39
+ timezone: string;
40
+ }
41
+
42
+ export interface TemplateContext {
43
+ dashboard_prompt?: string;
44
+ section_prompts?: Record<string, string>;
45
+ card_prompts?: Record<string, string>;
46
+ }
47
+
48
+ export interface FieldConstraints {
49
+ max_len?: number;
50
+ min?: number;
51
+ max?: number;
52
+ enum?: Array<string | number | boolean>;
53
+ min_items?: number;
54
+ max_items?: number;
55
+ }
56
+
57
+ export interface FieldSpec {
58
+ id: string;
59
+ pointer: string;
60
+ type: FillFieldType;
61
+ prompt: string;
62
+ required?: boolean;
63
+ constraints?: FieldConstraints;
64
+ }
65
+
66
+ export interface TemplateRunConfig {
67
+ retry_count?: number;
68
+ repair_attempts?: number;
69
+ model?: string;
70
+ temperature?: number;
71
+ max_tokens?: number;
72
+ }
73
+
74
+ export interface DashboardTemplate {
75
+ id: string;
76
+ name: string;
77
+ enabled: boolean;
78
+ schedule: TemplateSchedule;
79
+ base_dashboard: Record<string, unknown>;
80
+ fields: FieldSpec[];
81
+ context?: TemplateContext;
82
+ run?: TemplateRunConfig;
83
+ }
84
+
85
+ export interface TemplateRunState {
86
+ last_attempt_at?: string;
87
+ last_success_at?: string;
88
+ last_status?: 'success' | 'failed';
89
+ last_error?: string;
90
+ }
91
+
92
+ export interface PlashboardState {
93
+ version: 1;
94
+ active_template_id: string | null;
95
+ template_runs: Record<string, TemplateRunState>;
96
+ display_profile?: DisplayProfile;
97
+ }
98
+
99
+ export interface RunArtifact {
100
+ template_id: string;
101
+ trigger: 'schedule' | 'manual';
102
+ status: 'success' | 'failed';
103
+ started_at: string;
104
+ finished_at: string;
105
+ duration_ms: number;
106
+ attempt_count: number;
107
+ published: boolean;
108
+ errors: string[];
109
+ response?: unknown;
110
+ }
111
+
112
+ export interface ToolResponse<T = Record<string, unknown>> {
113
+ ok: boolean;
114
+ errors: string[];
115
+ data?: T;
116
+ }
117
+
118
+ export interface FillResponse {
119
+ values: Record<string, unknown>;
120
+ }
121
+
122
+ export interface FillRunContext {
123
+ template: DashboardTemplate;
124
+ currentValues: Record<string, unknown>;
125
+ attempt: number;
126
+ errorHint?: string;
127
+ }
128
+
129
+ export interface FillRunner {
130
+ run(context: FillRunContext): Promise<FillResponse>;
131
+ }
132
+
133
+ export interface RuntimeStatus {
134
+ active_template_id: string | null;
135
+ template_count: number;
136
+ enabled_template_count: number;
137
+ running_template_ids: string[];
138
+ state: PlashboardState;
139
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ export function nowIso(): string {
5
+ return new Date().toISOString();
6
+ }
7
+
8
+ export function deepClone<T>(value: T): T {
9
+ return JSON.parse(JSON.stringify(value)) as T;
10
+ }
11
+
12
+ export async function ensureDir(path: string): Promise<void> {
13
+ await mkdir(path, { recursive: true });
14
+ }
15
+
16
+ export async function readJsonFile<T>(path: string): Promise<T | null> {
17
+ try {
18
+ const text = await readFile(path, 'utf8');
19
+ return JSON.parse(text) as T;
20
+ } catch (error) {
21
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
22
+ return null;
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ export async function atomicWriteJson(path: string, value: unknown): Promise<void> {
29
+ const targetDir = dirname(path);
30
+ await ensureDir(targetDir);
31
+ const tmpBase = await mkdtemp(join(targetDir, '.plashboard-write-'));
32
+ const tmpPath = join(tmpBase, 'next.json');
33
+ await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
34
+ await rename(tmpPath, path);
35
+ await rm(tmpBase, { recursive: true, force: true });
36
+ }
37
+
38
+ export function sleep(ms: number): Promise<void> {
39
+ return new Promise((resolve) => setTimeout(resolve, ms));
40
+ }
41
+
42
+ export function asErrorMessage(value: unknown): string {
43
+ if (value instanceof Error) return value.message;
44
+ if (typeof value === 'string') return value;
45
+ return JSON.stringify(value);
46
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "types": ["node", "vitest/globals"]
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['src/**/*.test.ts']
7
+ }
8
+ });