From a85e9f93ab100fa93d297b755974e909b698c1bf Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sat, 28 Mar 2026 21:05:02 +0000 Subject: [PATCH] initial commit --- .gitignore | 4 + Dockerfile | 10 + backend/server.js | 130 ++++ docker-compose.yml | 12 + frontend/index.html | 1316 ++++++++++++++++++++++++++++++++++++++++ package.json | 7 + seed/fonts/RomanSD.ttf | Bin 0 -> 50408 bytes 7 files changed, 1479 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 backend/server.js create mode 100644 docker-compose.yml create mode 100644 frontend/index.html create mode 100644 package.json create mode 100644 seed/fonts/RomanSD.ttf 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 0000000000000000000000000000000000000000..2a2a0cef290d7cb48c430198e73ade9e7af1db83 GIT binary patch literal 50408 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 drC9*dtQV58A95@VIkphFmf^1nvT8X={ckx