initial commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
*.bak
|
||||||
10
Dockerfile
Normal file
10
Dockerfile
Normal 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
130
backend/server.js
Normal 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
12
docker-compose.yml
Normal 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
1316
frontend/index.html
Normal file
File diff suppressed because it is too large
Load Diff
7
package.json
Normal file
7
package.json
Normal 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
BIN
seed/fonts/RomanSD.ttf
Normal file
Binary file not shown.
Reference in New Issue
Block a user