crushdataai 1.2.12 → 1.2.14
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/assets/.agent/workflows/data-analyst.md +23 -0
- package/assets/.claude/skills/data-analyst/SKILL.md +48 -0
- package/assets/.cursor/commands/data-analyst.md +20 -0
- package/assets/.github/prompts/data-analyst.prompt.md +18 -0
- package/assets/.kiro/steering/data-analyst.md +18 -0
- package/assets/.windsurf/workflows/data-analyst.md +18 -0
- package/dist/commands.js +20 -8
- package/dist/dashboard-server.d.ts +1 -0
- package/dist/dashboard-server.js +78 -0
- package/dist/index.js +17 -1
- package/dist/routes/dashboard.d.ts +2 -0
- package/dist/routes/dashboard.js +113 -0
- package/dist/types/dashboard.d.ts +48 -0
- package/dist/types/dashboard.js +3 -0
- package/package.json +4 -3
- package/ui-dashboard-dist/assets/index-BhtUalwh.js +112 -0
- package/ui-dashboard-dist/assets/index-uepFwkLY.css +1 -0
- package/ui-dashboard-dist/favicon.svg +13 -0
- package/ui-dashboard-dist/index.html +14 -0
|
@@ -114,6 +114,29 @@ print(f"Missing values:\n{df.isnull().sum()}")
|
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
|
117
|
+
## Step 5: Generate Dashboard Output
|
|
118
|
+
|
|
119
|
+
After analysis, save for visualization:
|
|
120
|
+
```python
|
|
121
|
+
from pathlib import Path
|
|
122
|
+
import json
|
|
123
|
+
from datetime import datetime
|
|
124
|
+
|
|
125
|
+
Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
dashboard = {
|
|
128
|
+
"metadata": {"title": "Analysis Dashboard", "generatedAt": datetime.now().isoformat()},
|
|
129
|
+
"kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%", "trendDirection": "up"}],
|
|
130
|
+
"charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
with open("reports/dashboards/dashboard.json", "w") as f:
|
|
134
|
+
json.dump(dashboard, f, indent=2)
|
|
135
|
+
```
|
|
136
|
+
Tell user: "Run `npx crushdataai dashboard` to view."
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
117
140
|
## Pre-Delivery Checklist
|
|
118
141
|
|
|
119
142
|
- [ ] Business question answered
|
|
@@ -170,6 +170,54 @@ FROM table;
|
|
|
170
170
|
|
|
171
171
|
---
|
|
172
172
|
|
|
173
|
+
## Step 5: Generate Dashboard Output
|
|
174
|
+
|
|
175
|
+
**After analysis, save results for dashboard visualization:**
|
|
176
|
+
|
|
177
|
+
1. **Search for best chart type:**
|
|
178
|
+
```bash
|
|
179
|
+
python3 .claude/skills/data-analyst/scripts/search.py "<metric description>" --domain chart
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
2. **Create dashboard JSON:**
|
|
183
|
+
```python
|
|
184
|
+
from pathlib import Path
|
|
185
|
+
import json
|
|
186
|
+
from datetime import datetime
|
|
187
|
+
|
|
188
|
+
Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
|
|
189
|
+
|
|
190
|
+
dashboard = {
|
|
191
|
+
"metadata": {
|
|
192
|
+
"title": "Analysis Dashboard",
|
|
193
|
+
"generatedAt": datetime.now().isoformat(),
|
|
194
|
+
"dataRange": f"{start_date} to {end_date}"
|
|
195
|
+
},
|
|
196
|
+
"kpis": [
|
|
197
|
+
{"id": "kpi-1", "label": "Total Revenue", "value": "$50,000", "trend": "+12%", "trendDirection": "up"}
|
|
198
|
+
],
|
|
199
|
+
"charts": [
|
|
200
|
+
{
|
|
201
|
+
"id": "chart-1",
|
|
202
|
+
"type": "line", # line, bar, pie, area, scatter, donut, table
|
|
203
|
+
"title": "Monthly Trend",
|
|
204
|
+
"data": {
|
|
205
|
+
"labels": ["Jan", "Feb", "Mar"],
|
|
206
|
+
"datasets": [{"label": "Revenue", "values": [10000, 15000, 25000]}]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
with open("reports/dashboards/dashboard.json", "w") as f:
|
|
213
|
+
json.dump(dashboard, f, indent=2)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
3. **Tell user:**
|
|
217
|
+
> "Dashboard ready! Run `npx crushdataai dashboard` to view."
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
173
221
|
## Pre-Delivery Checklist
|
|
174
222
|
|
|
175
223
|
Before presenting final results:
|
|
@@ -60,6 +60,26 @@ Report findings and ask user for confirmation.
|
|
|
60
60
|
- Compare to benchmarks
|
|
61
61
|
- Present for user validation
|
|
62
62
|
|
|
63
|
+
### 5. Generate Dashboard Output
|
|
64
|
+
After analysis, save for visualization:
|
|
65
|
+
```python
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
import json
|
|
68
|
+
from datetime import datetime
|
|
69
|
+
|
|
70
|
+
Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
dashboard = {
|
|
73
|
+
"metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
|
|
74
|
+
"kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
|
|
75
|
+
"charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
with open("reports/dashboards/dashboard.json", "w") as f:
|
|
79
|
+
json.dump(dashboard, f, indent=2)
|
|
80
|
+
```
|
|
81
|
+
Tell user: "Run `npx crushdataai dashboard` to view."
|
|
82
|
+
|
|
63
83
|
---
|
|
64
84
|
|
|
65
85
|
## Quick Reference
|
|
@@ -58,3 +58,21 @@ Report and confirm before proceeding.
|
|
|
58
58
|
- Check totals
|
|
59
59
|
- Compare benchmarks
|
|
60
60
|
- User validation
|
|
61
|
+
|
|
62
|
+
### 5. Generate Dashboard Output
|
|
63
|
+
After analysis, save for visualization:
|
|
64
|
+
```python
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
import json
|
|
67
|
+
from datetime import datetime
|
|
68
|
+
|
|
69
|
+
Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
|
|
70
|
+
dashboard = {
|
|
71
|
+
"metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
|
|
72
|
+
"kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
|
|
73
|
+
"charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
|
|
74
|
+
}
|
|
75
|
+
with open("reports/dashboards/dashboard.json", "w") as f:
|
|
76
|
+
json.dump(dashboard, f, indent=2)
|
|
77
|
+
```
|
|
78
|
+
Tell user: "Run `npx crushdataai dashboard` to view."
|
|
@@ -60,3 +60,21 @@ Ask: "Does this match your expectation?"
|
|
|
60
60
|
- Compare to benchmarks
|
|
61
61
|
- Document assumptions
|
|
62
62
|
- Present for user confirmation
|
|
63
|
+
|
|
64
|
+
### 5. Generate Dashboard Output
|
|
65
|
+
After analysis, save for visualization:
|
|
66
|
+
```python
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
import json
|
|
69
|
+
from datetime import datetime
|
|
70
|
+
|
|
71
|
+
Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
|
|
72
|
+
dashboard = {
|
|
73
|
+
"metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
|
|
74
|
+
"kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
|
|
75
|
+
"charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
|
|
76
|
+
}
|
|
77
|
+
with open("reports/dashboards/dashboard.json", "w") as f:
|
|
78
|
+
json.dump(dashboard, f, indent=2)
|
|
79
|
+
```
|
|
80
|
+
Tell user: "Run `npx crushdataai dashboard` to view."
|
|
@@ -58,3 +58,21 @@ print(f"Shape: {df.shape}, Dates: {df['date'].min()} to {df['date'].max()}")
|
|
|
58
58
|
- Check totals are reasonable
|
|
59
59
|
- Compare to benchmarks
|
|
60
60
|
- Present for user validation
|
|
61
|
+
|
|
62
|
+
### 5. Generate Dashboard Output
|
|
63
|
+
After analysis, save for visualization:
|
|
64
|
+
```python
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
import json
|
|
67
|
+
from datetime import datetime
|
|
68
|
+
|
|
69
|
+
Path("reports/dashboards").mkdir(parents=True, exist_ok=True)
|
|
70
|
+
dashboard = {
|
|
71
|
+
"metadata": {"title": "Analysis", "generatedAt": datetime.now().isoformat()},
|
|
72
|
+
"kpis": [{"id": "kpi-1", "label": "Total", "value": "$50K", "trend": "+12%"}],
|
|
73
|
+
"charts": [{"id": "chart-1", "type": "line", "title": "Trend", "data": {"labels": ["Jan","Feb"], "datasets": [{"label": "Revenue", "values": [10000,20000]}]}}]
|
|
74
|
+
}
|
|
75
|
+
with open("reports/dashboards/dashboard.json", "w") as f:
|
|
76
|
+
json.dump(dashboard, f, indent=2)
|
|
77
|
+
```
|
|
78
|
+
Tell user: "Run `npx crushdataai dashboard` to view."
|
package/dist/commands.js
CHANGED
|
@@ -79,17 +79,25 @@ function getAssetsDir() {
|
|
|
79
79
|
function copySharedFiles(targetDir, force) {
|
|
80
80
|
const sharedSource = path.join(getAssetsDir(), '.shared', 'data-analyst');
|
|
81
81
|
const sharedTarget = path.join(targetDir, SHARED_DIR);
|
|
82
|
-
if (fs.existsSync(sharedTarget)
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
if (fs.existsSync(sharedTarget)) {
|
|
83
|
+
if (!force) {
|
|
84
|
+
console.log(` ⏭️ ${SHARED_DIR} already exists (use --force to overwrite)`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Clean reinstall: remove existing folder first
|
|
88
|
+
fs.removeSync(sharedTarget);
|
|
85
89
|
}
|
|
86
|
-
fs.copySync(sharedSource, sharedTarget
|
|
90
|
+
fs.copySync(sharedSource, sharedTarget);
|
|
87
91
|
console.log(` ✅ Created ${SHARED_DIR}/`);
|
|
88
92
|
}
|
|
89
93
|
function copyAIFiles(aiType, targetDir, force) {
|
|
90
94
|
const config = AI_PATHS[aiType];
|
|
91
95
|
const sourceDir = path.join(getAssetsDir(), config.sourceDir);
|
|
92
96
|
const targetPath = path.join(targetDir, config.dir);
|
|
97
|
+
// Clean reinstall: remove entire folder first when force=true
|
|
98
|
+
if (fs.existsSync(targetPath) && force) {
|
|
99
|
+
fs.removeSync(targetPath);
|
|
100
|
+
}
|
|
93
101
|
// Ensure directory exists
|
|
94
102
|
fs.ensureDirSync(targetPath);
|
|
95
103
|
for (const file of config.files) {
|
|
@@ -104,14 +112,18 @@ function copyAIFiles(aiType, targetDir, force) {
|
|
|
104
112
|
console.log(` ✅ Created ${config.dir}/${file}`);
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
|
-
// For Claude, also
|
|
115
|
+
// For Claude, also copy shared scripts and data
|
|
108
116
|
if (aiType === 'claude') {
|
|
109
117
|
const scriptsLink = path.join(targetPath, 'scripts');
|
|
110
118
|
const dataLink = path.join(targetPath, 'data');
|
|
111
|
-
const sharedScripts = path.relative(targetPath, path.join(targetDir, SHARED_DIR, 'scripts'));
|
|
112
|
-
const sharedData = path.relative(targetPath, path.join(targetDir, SHARED_DIR, 'data'));
|
|
113
|
-
// Note: Symlinks may require admin on Windows. Copy as fallback.
|
|
114
119
|
try {
|
|
120
|
+
// Clean reinstall: remove existing folders first when force=true
|
|
121
|
+
if (force) {
|
|
122
|
+
if (fs.existsSync(scriptsLink))
|
|
123
|
+
fs.removeSync(scriptsLink);
|
|
124
|
+
if (fs.existsSync(dataLink))
|
|
125
|
+
fs.removeSync(dataLink);
|
|
126
|
+
}
|
|
115
127
|
if (!fs.existsSync(scriptsLink)) {
|
|
116
128
|
fs.copySync(path.join(targetDir, SHARED_DIR, 'scripts'), scriptsLink);
|
|
117
129
|
console.log(` ✅ Copied scripts to ${config.dir}/scripts/`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startDashboardServer(port: number): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.startDashboardServer = startDashboardServer;
|
|
40
|
+
const express_1 = __importDefault(require("express"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const open_1 = __importDefault(require("open"));
|
|
43
|
+
const dashboard_1 = __importDefault(require("./routes/dashboard"));
|
|
44
|
+
async function startDashboardServer(port) {
|
|
45
|
+
const app = (0, express_1.default)();
|
|
46
|
+
// Serve static files from the built dashboard UI
|
|
47
|
+
const uiPath = path.join(__dirname, '..', 'ui-dashboard-dist');
|
|
48
|
+
app.use(express_1.default.static(uiPath));
|
|
49
|
+
// API routes
|
|
50
|
+
app.use('/api', dashboard_1.default);
|
|
51
|
+
// SPA fallback - serve index.html for client-side routing
|
|
52
|
+
app.get('*', (_req, res) => {
|
|
53
|
+
res.sendFile(path.join(uiPath, 'index.html'));
|
|
54
|
+
});
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const server = app.listen(port, async () => {
|
|
57
|
+
console.log(`\n📊 Dashboard UI running at http://localhost:${port}\n`);
|
|
58
|
+
console.log(' Looking for dashboards in: reports/dashboards/');
|
|
59
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
60
|
+
// Open browser
|
|
61
|
+
try {
|
|
62
|
+
await (0, open_1.default)(`http://localhost:${port}`);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Ignore open errors
|
|
66
|
+
}
|
|
67
|
+
resolve();
|
|
68
|
+
});
|
|
69
|
+
server.on('error', (err) => {
|
|
70
|
+
if (err.code === 'EADDRINUSE') {
|
|
71
|
+
reject(new Error(`Port ${port} is already in use. Try a different port with --port`));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
reject(err);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -37,12 +37,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
37
37
|
const commander_1 = require("commander");
|
|
38
38
|
const commands_1 = require("./commands");
|
|
39
39
|
const server_1 = require("./server");
|
|
40
|
+
const dashboard_server_1 = require("./dashboard-server");
|
|
40
41
|
const connections_1 = require("./connections");
|
|
41
42
|
const program = new commander_1.Command();
|
|
42
43
|
program
|
|
43
44
|
.name('crushdataai')
|
|
44
45
|
.description('CLI to install CrushData AI data analyst skill for AI coding assistants')
|
|
45
|
-
.version('1.2.
|
|
46
|
+
.version('1.2.14');
|
|
46
47
|
program
|
|
47
48
|
.command('init')
|
|
48
49
|
.description('Initialize CrushData AI skill in current project')
|
|
@@ -125,4 +126,19 @@ program
|
|
|
125
126
|
const { schema } = await Promise.resolve().then(() => __importStar(require('./commands/schema')));
|
|
126
127
|
await schema(connection, table);
|
|
127
128
|
});
|
|
129
|
+
program
|
|
130
|
+
.command('dashboard')
|
|
131
|
+
.description('Open the dashboard visualization UI')
|
|
132
|
+
.option('-p, --port <port>', 'Server port', '3002')
|
|
133
|
+
.action(async (options) => {
|
|
134
|
+
const port = parseInt(options.port);
|
|
135
|
+
console.log('\n📊 Starting CrushData AI Dashboard...\n');
|
|
136
|
+
try {
|
|
137
|
+
await (0, dashboard_server_1.startDashboardServer)(port);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.error(`❌ Error: ${error.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
128
144
|
program.parse();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const express_1 = require("express");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const router = (0, express_1.Router)();
|
|
40
|
+
// Get reports/dashboards directory path relative to current working directory
|
|
41
|
+
function getDashboardsDir() {
|
|
42
|
+
return path.join(process.cwd(), 'reports', 'dashboards');
|
|
43
|
+
}
|
|
44
|
+
// List all available dashboards
|
|
45
|
+
router.get('/dashboards', (_req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const dashboardsDir = getDashboardsDir();
|
|
48
|
+
// Check if directory exists
|
|
49
|
+
if (!fs.existsSync(dashboardsDir)) {
|
|
50
|
+
return res.json([]);
|
|
51
|
+
}
|
|
52
|
+
// Read all JSON files in the directory
|
|
53
|
+
const files = fs.readdirSync(dashboardsDir)
|
|
54
|
+
.filter(file => file.endsWith('.json'));
|
|
55
|
+
const dashboards = files.map(file => {
|
|
56
|
+
const filePath = path.join(dashboardsDir, file);
|
|
57
|
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
58
|
+
// Use filename without extension as ID
|
|
59
|
+
const id = path.basename(file, '.json');
|
|
60
|
+
return {
|
|
61
|
+
id,
|
|
62
|
+
title: content.metadata?.title || id,
|
|
63
|
+
generatedAt: content.metadata?.generatedAt || '',
|
|
64
|
+
chartCount: content.charts?.length || 0,
|
|
65
|
+
kpiCount: content.kpis?.length || 0
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
res.json(dashboards);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
console.error('Error listing dashboards:', error);
|
|
72
|
+
res.status(500).json({ error: 'Failed to list dashboards' });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
// Get a specific dashboard by ID
|
|
76
|
+
router.get('/dashboards/:id', (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { id } = req.params;
|
|
79
|
+
const dashboardsDir = getDashboardsDir();
|
|
80
|
+
const filePath = path.join(dashboardsDir, `${id}.json`);
|
|
81
|
+
if (!fs.existsSync(filePath)) {
|
|
82
|
+
return res.status(404).json({ error: 'Dashboard not found' });
|
|
83
|
+
}
|
|
84
|
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
85
|
+
res.json(content);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('Error reading dashboard:', error);
|
|
89
|
+
res.status(500).json({ error: 'Failed to read dashboard' });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// Refresh a chart's data (placeholder for now - would re-run query)
|
|
93
|
+
router.post('/charts/:id/refresh', (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
const { id } = req.params;
|
|
96
|
+
// For now, just return a success message
|
|
97
|
+
// In the future, this would:
|
|
98
|
+
// 1. Find the chart in a dashboard
|
|
99
|
+
// 2. Re-run its query against the data source
|
|
100
|
+
// 3. Update the chart data
|
|
101
|
+
// 4. Save the updated dashboard
|
|
102
|
+
res.json({
|
|
103
|
+
message: 'Refresh not yet implemented',
|
|
104
|
+
chartId: id,
|
|
105
|
+
lastRefreshed: new Date().toISOString()
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('Error refreshing chart:', error);
|
|
110
|
+
res.status(500).json({ error: 'Failed to refresh chart' });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
exports.default = router;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface DashboardMetadata {
|
|
2
|
+
title: string;
|
|
3
|
+
generatedAt: string;
|
|
4
|
+
dataRange?: string;
|
|
5
|
+
recordCount?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface KPI {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
trend?: string;
|
|
12
|
+
trendDirection?: 'up' | 'down' | 'neutral';
|
|
13
|
+
}
|
|
14
|
+
export interface ChartQuery {
|
|
15
|
+
type: 'sql' | 'api';
|
|
16
|
+
connection?: string;
|
|
17
|
+
sql?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ChartDataset {
|
|
20
|
+
label: string;
|
|
21
|
+
values: number[];
|
|
22
|
+
color?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ChartData {
|
|
25
|
+
labels: string[];
|
|
26
|
+
datasets: ChartDataset[];
|
|
27
|
+
}
|
|
28
|
+
export interface Chart {
|
|
29
|
+
id: string;
|
|
30
|
+
type: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
query?: ChartQuery;
|
|
34
|
+
data: ChartData;
|
|
35
|
+
lastRefreshed?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Dashboard {
|
|
38
|
+
metadata: DashboardMetadata;
|
|
39
|
+
kpis: KPI[];
|
|
40
|
+
charts: Chart[];
|
|
41
|
+
}
|
|
42
|
+
export interface DashboardListItem {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string;
|
|
45
|
+
generatedAt: string;
|
|
46
|
+
chartCount: number;
|
|
47
|
+
kpiCount: number;
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crushdataai",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.14",
|
|
4
4
|
"description": "CLI to install CrushData AI data analyst skill for AI coding assistants",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"crushdataai": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsc && cd ui-react && npm install && npm run build",
|
|
10
|
+
"build": "tsc && cd ui-react && npm install && npm run build && cd ../ui-dashboard && npm install && npm run build",
|
|
11
11
|
"dev": "ts-node src/index.ts",
|
|
12
12
|
"prepublishOnly": "npm run build"
|
|
13
13
|
},
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
49
|
"assets",
|
|
50
|
-
"ui"
|
|
50
|
+
"ui",
|
|
51
|
+
"ui-dashboard-dist"
|
|
51
52
|
],
|
|
52
53
|
"repository": {
|
|
53
54
|
"type": "git",
|