commit a85e9f93ab100fa93d297b755974e909b698c1bf Author: indifferentketchup Date: Sat Mar 28 21:05:02 2026 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8056e25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +node_modules/ +*.log +*.bak diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a816e3f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:20-alpine +WORKDIR /app +COPY package.json . +RUN npm install --omit=dev +COPY backend/ ./backend/ +COPY frontend/ ./frontend/ +ENV PORT=3000 +ENV CONFIG_PATH=/data/config.json +EXPOSE 3000 +CMD ["node", "backend/server.js"] diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..093badd --- /dev/null +++ b/backend/server.js @@ -0,0 +1,130 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const multer = require('multer'); + +const app = express(); +const CONFIG_PATH = process.env.CONFIG_PATH || '/data/config.json'; +const PORT = process.env.PORT || 3000; +const UPTIME_KUMA_URL = process.env.UPTIME_KUMA_URL || 'http://100.114.205.53:3001'; +const UPLOAD_DIR = '/data/icons'; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { fs.mkdirSync(UPLOAD_DIR, { recursive: true }); cb(null, UPLOAD_DIR); }, + filename: (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + const base = path.basename(file.originalname, ext).replace(/[^a-z0-9_-]/gi, '_').toLowerCase(); + cb(null, base + ext); + } +}); +const ALLOWED = new Set(['.jpg','.jpeg','.png','.gif','.webp','.svg','.ico']); +const upload = multer({ + storage, + limits: { fileSize: 10 * 1024 * 1024 }, + fileFilter: (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + ALLOWED.has(ext) ? cb(null, true) : cb(new Error('Invalid file type: ' + ext)); + } +}); + +app.use(express.json({ limit: '2mb' })); +app.use(express.static(path.join(__dirname, '../frontend'))); +app.use('/uploads', express.static('/data', { index: false })); + +app.post('/api/upload', upload.single('file'), (req, res) => { + if (!req.file) return res.status(400).json({ error: 'No file received' }); + const url = '/uploads/icons/' + req.file.filename; + const slot = req.query.slot; + if (slot && ['banner','logo','favicon'].includes(slot)) { + try { const cfg = readConfig(); cfg.site[slot] = url; writeConfig(cfg); } catch (e) {} + } + res.json({ ok: true, url, filename: req.file.filename, slot: slot || null }); +}); + +app.get('/api/uploads', (req, res) => { + try { + fs.mkdirSync(UPLOAD_DIR, { recursive: true }); + const files = fs.readdirSync(UPLOAD_DIR) + .filter(f => ALLOWED.has(path.extname(f).toLowerCase())) + .map(f => ({ filename: f, url: '/uploads/icons/' + f })); + res.json(files); + } catch (e) { res.json([]); } +}); + +function readConfig() { + try { + if (!fs.existsSync(CONFIG_PATH)) { + const def = defaultConfig(); + fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true }); + fs.writeFileSync(CONFIG_PATH, JSON.stringify(def, null, 2)); + return def; + } + return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); + } catch (e) { return defaultConfig(); } +} + +function writeConfig(cfg) { + fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true }); + fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2)); +} + +function defaultConfig() { + return { + site: { title: 'Dashgaard', slogans: ['all systems nominal.', 'boogaard is watching.'], banner: '', logo: '', favicon: '' }, + uptime: { url: UPTIME_KUMA_URL, slug: 'homelab', enabled: true }, + theme: { + fontBody: 'Rajdhani', fontMono: 'Share Tech Mono', fontDisplay: 'JetBrains Mono', + cardSize: 'medium', accentColor: '#f5a623', bgColor: '#0a0600', cardBg: '#120c04', + textColor: 'rgba(255,220,170,0.92)', pageMaxWidth: '960', baseFontSize: '16', + cardGap: '10', cardRadius: '10', heartbeatHeight: '16' + }, + groups: [ + { id: 'g1', name: 'Core', order: 0, services: [ + { id: 's1', name: 'BourBites', url: 'https://bourbites.boogaardmusic.com', icon: '๐Ÿ–', description: 'Academic knowledge OS', color: '#f5a623', order: 0 }, + { id: 's2', name: 'booops', url: 'https://booops.boogaardmusic.com', icon: '๐Ÿค–', description: 'Open WebUI โ€” LLM chat', color: '#ff2d6b', order: 1 }, + { id: 's3', name: 'b2b', url: 'https://b2b.boogaardmusic.com', icon: 'โšก', description: 'n8n automation', color: '#c8e06b', order: 2 }, + { id: 's4', name: 'Kimai', url: 'https://time.boogaardmusic.com', icon: 'โฑ', description: 'Time tracking', color: '#f5a623', order: 3 } + ]}, + { id: 'g2', name: 'Infrastructure', order: 1, services: [ + { id: 's5', name: 'Komodo', url: 'http://100.114.205.53:9120', icon: '๐ŸฆŽ', description: 'Container management', color: '#c8e06b', order: 0 }, + { id: 's6', name: 'File Browser', url: 'https://files.boogaardmusic.com', icon: '๐Ÿ“', description: 'File management', color: '#f5a623', order: 1 }, + { id: 's7', name: 'AdGuard', url: 'https://adguard.boogaardmusic.com', icon: '๐Ÿ›ก', description: 'DNS + ad blocking', color: '#00e87a', order: 2 }, + { id: 's8', name: 'Uptime Kuma', url: 'https://uptime.boogaardmusic.com', icon: '๐Ÿ“ก', description: 'Uptime monitoring', color: '#ff2d6b', order: 3 } + ]}, + { id: 'g3', name: 'Tools', order: 2, services: [ + { id: 's9', name: 'CalDAV', url: 'https://cal.boogaardmusic.com', icon: '๐Ÿ“…', description: 'Baikal calendar', color: '#c8e06b', order: 0 }, + { id: 's10', name: '808notes', url: 'https://808notes.boogaardmusic.com', icon: '๐ŸŽต', description: 'Music notes', color: '#ff2d6b', order: 1 }, + { id: 's11', name: 'Wonkcrop', url: 'https://wonkcrop.boogaardmusic.com', icon: 'โœ‚๏ธ', description: 'Image cropper', color: '#f5a623', order: 2 }, + { id: 's12', name: 'DubDrive', url: 'http://100.114.205.53:9200', icon: 'โ˜๏ธ', description: 'Cloud storage', color: '#c084fc', order: 3 }, + { id: 's13', name: 'Grafana', url: 'https://monitoring.boogaardmusic.com', icon: '๐Ÿ“Š', description: 'Dashboards', color: '#ff6b35', order: 4 }, + { id: 's14', name: 'Dashy', url: 'http://100.114.205.53:4000', icon: '๐Ÿ—‚', description: 'Old dashboard', color: '#f5a623', order: 5 } + ]} + ] + }; +} + +app.get('/api/config', (req, res) => res.json(readConfig())); +app.put('/api/config', (req, res) => { + try { writeConfig(req.body); res.json({ ok: true }); } + catch (e) { res.status(500).json({ error: e.message }); } +}); + +app.get('/api/uptime/:slug', async (req, res) => { + const cfg = readConfig(); + const base = cfg.uptime?.url || UPTIME_KUMA_URL; + try { + const [pr, hr] = await Promise.all([ + fetch(`${base}/api/status-page/${req.params.slug}`), + fetch(`${base}/api/status-page/heartbeat/${req.params.slug}`) + ]); + if (!pr.ok || !hr.ok) return res.status(502).json({ error: `${pr.status}/${hr.status}` }); + res.json({ page: await pr.json(), heartbeat: await hr.json() }); + } catch (e) { res.status(502).json({ error: e.message }); } +}); + +app.use((err, req, res, next) => { + if (err.code === 'LIMIT_FILE_SIZE') return res.status(413).json({ error: 'File too large (max 10MB)' }); + res.status(400).json({ error: err.message }); +}); + +app.listen(PORT, () => console.log(`Dashgaard :${PORT}`)); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..510f61f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + dashgaard: + build: . + container_name: dashgaard + restart: unless-stopped + ports: + - "8094:3000" + volumes: + - /docker/dashgaard/data:/data + environment: + - CONFIG_PATH=/data/config.json + - UPTIME_KUMA_URL=http://100.114.205.53:3001 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b078700 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,1316 @@ + + + + + +Dashgaard + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+ +
+
Dashgaard
+
+
+
+
status
โ€”
+ + +
+
+
+ +
+ + + + + + + + + + +
+
+
+
// settings
+ +
+
+ + + + + +
+
+
+ + +
+
+ + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..5c25bbd --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "dashgaard", + "version": "2.0.0", + "main": "backend/server.js", + "scripts": { "start": "node backend/server.js" }, + "dependencies": { "express": "^4.18.2", "multer": "^1.4.5-lts.1" } +} diff --git a/seed/fonts/RomanSD.ttf b/seed/fonts/RomanSD.ttf new file mode 100644 index 0000000..2a2a0ce Binary files /dev/null and b/seed/fonts/RomanSD.ttf differ