initial commit

This commit is contained in:
2026-03-28 21:05:02 +00:00
commit a85e9f93ab
7 changed files with 1479 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.env
node_modules/
*.log
*.bak

10
Dockerfile Normal file
View File

@@ -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"]

130
backend/server.js Normal file
View File

@@ -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}`));

12
docker-compose.yml Normal file
View File

@@ -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

1316
frontend/index.html Normal file

File diff suppressed because it is too large Load Diff

7
package.json Normal file
View File

@@ -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" }
}

BIN
seed/fonts/RomanSD.ttf Normal file

Binary file not shown.