Initial commit: StackPilot - Docker stack builder with 186 apps, compose analyser, analytics

This commit is contained in:
2026-08-03 23:28:15 +01:00
commit 341700ddac
18 changed files with 5042 additions and 0 deletions

115
api/server.js Normal file
View File

@@ -0,0 +1,115 @@
import express from 'express';
import cors from 'cors';
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const DATA_FILE = join(__dirname, 'data', 'popularity.json');
const DOWNLOADS_LOG = join(__dirname, 'data', 'downloads.json');
const app = express();
app.use(cors());
app.use(express.json());
// Ensure data dir exists
import { mkdirSync } from 'fs';
mkdirSync(join(__dirname, 'data'), { recursive: true });
// Load or init popularity data
function loadPopularity() {
if (existsSync(DATA_FILE)) {
return JSON.parse(readFileSync(DATA_FILE, 'utf8'));
}
return {};
}
function savePopularity(data) {
writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
function loadDownloads() {
if (existsSync(DOWNLOADS_LOG)) {
return JSON.parse(readFileSync(DOWNLOADS_LOG, 'utf8'));
}
return { total: 0, history: [] };
}
function saveDownloads(data) {
writeFileSync(DOWNLOADS_LOG, JSON.stringify(data, null, 2));
}
// POST /api/analytics/download — record a bundle download
app.post('/api/analytics/download', (req, res) => {
const { apps, timestamp } = req.body;
if (!apps || !Array.isArray(apps)) {
return res.status(400).json({ error: 'apps array required' });
}
// Update popularity counts
const popularity = loadPopularity();
for (const appId of apps) {
popularity[appId] = (popularity[appId] || 0) + 1;
}
savePopularity(popularity);
// Log the download
const downloads = loadDownloads();
downloads.total += 1;
downloads.history.push({
timestamp: timestamp || new Date().toISOString(),
apps,
count: apps.length,
});
// Keep last 10,000 entries to avoid unbounded growth
if (downloads.history.length > 10000) {
downloads.history = downloads.history.slice(-10000);
}
saveDownloads(downloads);
res.json({ ok: true, total_downloads: downloads.total });
});
// GET /api/analytics/popularity — get app download counts
app.get('/api/analytics/popularity', (req, res) => {
const popularity = loadPopularity();
res.json(popularity);
});
// GET /api/analytics/stats — overview stats
app.get('/api/analytics/stats', (req, res) => {
const popularity = loadPopularity();
const downloads = loadDownloads();
// Top apps
const sorted = Object.entries(popularity)
.sort((a, b) => b[1] - a[1]);
res.json({
total_downloads: downloads.total,
unique_apps_selected: sorted.length,
top_20: sorted.slice(0, 20).map(([id, count]) => ({ id, count })),
recent: downloads.history.slice(-10).reverse(),
});
});
// GET /api/analytics/app/:id — stats for a specific app
app.get('/api/analytics/app/:id', (req, res) => {
const popularity = loadPopularity();
const count = popularity[req.params.id] || 0;
const downloads = loadDownloads();
const total = downloads.total || 1;
const percentage = ((count / total) * 100).toFixed(1);
res.json({
id: req.params.id,
download_count: count,
percentage: `${percentage}%`,
total_bundles: total,
});
});
const PORT = process.env.PORT || 3052;
app.listen(PORT, () => {
console.log(`StackPilot Analytics API running on port ${PORT}`);
});