Initial commit: StackPilot - Docker stack builder with 186 apps, compose analyser, analytics
This commit is contained in:
115
api/server.js
Normal file
115
api/server.js
Normal 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user