changes made
This commit is contained in:
@@ -232,6 +232,7 @@ a {
|
||||
|
||||
/* ── Service Cards ── */
|
||||
.service-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -405,6 +406,248 @@ a {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* ── Card Status Dots ── */
|
||||
.card-status-dot {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
opacity: 0.3;
|
||||
transition: background 0.3s, opacity 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.card-status-dot.loading {
|
||||
background: var(--yellow);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 6px var(--yellow);
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.card-status-dot.ok {
|
||||
background: var(--green);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 6px var(--green);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card-status-dot.error {
|
||||
background: var(--red);
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 6px var(--red);
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.card-status-dot.unknown {
|
||||
background: var(--text-muted);
|
||||
opacity: 0.3;
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* ── Search Overlay ── */
|
||||
.search-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
}
|
||||
|
||||
.search-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.search-input-wrap > i {
|
||||
font-size: 20px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input-wrap input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search-input-wrap input::placeholder {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.search-input-wrap kbd {
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
color: var(--text-muted);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.active {
|
||||
background: rgba(255, 196, 81, 0.08);
|
||||
}
|
||||
|
||||
.search-result-item i {
|
||||
font-size: 18px;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-result-item span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.search-result-empty {
|
||||
padding: 24px 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ── Search Card Visibility ── */
|
||||
.search-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.search-highlight {
|
||||
border-color: var(--accent) !important;
|
||||
box-shadow: 0 0 12px var(--accent-glow) !important;
|
||||
}
|
||||
|
||||
/* ── Nav Search Button ── */
|
||||
.nav-search-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 16px;
|
||||
color: var(--text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.nav-search-btn:hover {
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
/* ── Hero Widget ── */
|
||||
.hero-greeting {
|
||||
font-size: clamp(16px, 2.5vw, 20px);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.hero-clock {
|
||||
font-size: clamp(36px, 6vw, 64px);
|
||||
font-weight: 700;
|
||||
color: var(--text-heading);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: 2px;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.hero-date {
|
||||
font-size: clamp(14px, 2vw, 16px);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hero-weather {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hero-weather i {
|
||||
font-size: 16px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Spin animation for loading icon ── */
|
||||
.ri-spin {
|
||||
display: inline-block;
|
||||
animation: riSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes riSpin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ── Mobile Adjustments ── */
|
||||
@media (max-width: 768px) {
|
||||
.search-overlay {
|
||||
padding: 8vh 16px 0;
|
||||
}
|
||||
|
||||
.search-input-wrap kbd {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-search-btn {
|
||||
padding: 14px 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Reduced Motion ── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
|
||||
@@ -1,7 +1,470 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
// ── Mobile Nav Toggle ──
|
||||
// ── Config Loader ──
|
||||
async function loadConfig() {
|
||||
const res = await fetch('/services.json');
|
||||
if (!res.ok) throw new Error(`Failed to load services.json: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Nav Renderer ──
|
||||
function renderNav(config) {
|
||||
const nav = document.getElementById('nav');
|
||||
config.categories.filter(c => c.showInNav).forEach(cat => {
|
||||
const a = document.createElement('a');
|
||||
a.href = `#${cat.id}`;
|
||||
a.textContent = cat.navLabel;
|
||||
nav.appendChild(a);
|
||||
});
|
||||
|
||||
// Search button in nav
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'nav-search-btn';
|
||||
btn.setAttribute('aria-label', 'Search services');
|
||||
btn.innerHTML = '<i class="ri-search-line"></i>';
|
||||
btn.addEventListener('click', () => SearchOverlay.open());
|
||||
nav.appendChild(btn);
|
||||
}
|
||||
|
||||
// ── Services Renderer ──
|
||||
function renderServices(config) {
|
||||
const container = document.getElementById('services-container');
|
||||
const grouped = {};
|
||||
config.categories.forEach(cat => { grouped[cat.id] = []; });
|
||||
config.services.forEach(svc => {
|
||||
if (grouped[svc.category]) grouped[svc.category].push(svc);
|
||||
});
|
||||
|
||||
config.categories.forEach(cat => {
|
||||
const services = grouped[cat.id];
|
||||
if (!services || services.length === 0) return;
|
||||
|
||||
const section = document.createElement('section');
|
||||
section.id = cat.id;
|
||||
section.className = 'category';
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.className = 'container';
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'section-title';
|
||||
title.innerHTML = `<i class="${cat.icon}"></i> ${cat.title}`;
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'card-grid';
|
||||
|
||||
services.forEach((svc, i) => {
|
||||
const card = document.createElement('a');
|
||||
card.href = svc.url;
|
||||
card.className = 'service-card';
|
||||
card.dataset.name = svc.name.toLowerCase();
|
||||
card.dataset.category = cat.id;
|
||||
card.style.animationDelay = `${(i + 1) * 0.02}s`;
|
||||
|
||||
// Status dot
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'card-status-dot unknown';
|
||||
card.appendChild(dot);
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = svc.icon;
|
||||
card.appendChild(icon);
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'card-name';
|
||||
name.textContent = svc.name;
|
||||
card.appendChild(name);
|
||||
|
||||
if (svc.sub) {
|
||||
const sub = document.createElement('span');
|
||||
sub.className = 'card-sub';
|
||||
sub.textContent = svc.sub;
|
||||
card.appendChild(sub);
|
||||
}
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
inner.appendChild(title);
|
||||
inner.appendChild(grid);
|
||||
section.appendChild(inner);
|
||||
container.appendChild(section);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Status Bar Renderer ──
|
||||
function renderStatusBar(config) {
|
||||
const grid = document.getElementById('status-grid');
|
||||
config.statusMonitors.forEach(mon => {
|
||||
const card = document.createElement('a');
|
||||
card.href = mon.url;
|
||||
card.className = 'status-card';
|
||||
card.id = mon.id;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="status-dot loading"></div>
|
||||
<div class="status-info">
|
||||
<span class="status-label">${mon.label}</span>
|
||||
<span class="status-value">Checking...</span>
|
||||
</div>`;
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Status Monitor ──
|
||||
class StatusMonitor {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
|
||||
// Rich monitor parsers
|
||||
this.parsers = {
|
||||
glances: (data) => `CPU ${Math.round(data.cpu_percent)}% | RAM ${Math.round(data.mem_percent)}%`,
|
||||
uptime: (data) => {
|
||||
const groups = data.heartbeatList || {};
|
||||
const monitors = Object.values(groups);
|
||||
const total = monitors.length;
|
||||
const up = monitors.filter(beats => beats.length > 0 && beats[beats.length - 1].status === 1).length;
|
||||
return `${up}/${total} services up`;
|
||||
},
|
||||
cadvisor: (data) => {
|
||||
const cores = data.num_cores || '?';
|
||||
const ramGB = data.memory_capacity ? (data.memory_capacity / 1073741824).toFixed(0) : '?';
|
||||
return `${cores} cores | ${ramGB} GB RAM`;
|
||||
}
|
||||
};
|
||||
|
||||
this.pollRichStatus();
|
||||
this.pollHealthChecks();
|
||||
setInterval(() => this.pollRichStatus(), 60000);
|
||||
setInterval(() => this.pollHealthChecks(), 120000);
|
||||
}
|
||||
|
||||
// ── Rich status monitors (3 detailed cards) ──
|
||||
async pollRichStatus() {
|
||||
this.config.statusMonitors.forEach(mon => this.checkRich(mon));
|
||||
}
|
||||
|
||||
async checkRich(mon) {
|
||||
const card = document.getElementById(mon.id);
|
||||
if (!card) return;
|
||||
|
||||
const dot = card.querySelector('.status-dot');
|
||||
const value = card.querySelector('.status-value');
|
||||
dot.className = 'status-dot loading';
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
const res = await fetch(mon.apiUrl, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
dot.className = 'status-dot ok';
|
||||
value.textContent = this.parsers[mon.parseType](data);
|
||||
} catch {
|
||||
dot.className = 'status-dot error';
|
||||
value.textContent = 'Unreachable';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-card health checks (simple up/down) ──
|
||||
async pollHealthChecks() {
|
||||
const services = this.config.services.filter(s => s.healthCheckUrl);
|
||||
const batchSize = 6;
|
||||
|
||||
for (let i = 0; i < services.length; i += batchSize) {
|
||||
const batch = services.slice(i, i + batchSize);
|
||||
await Promise.allSettled(batch.map(svc => this.checkHealth(svc)));
|
||||
if (i + batchSize < services.length) {
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkHealth(svc) {
|
||||
const card = document.querySelector(`.service-card[data-name="${svc.name.toLowerCase()}"]`);
|
||||
if (!card) return;
|
||||
|
||||
const dot = card.querySelector('.card-status-dot');
|
||||
if (!dot) return;
|
||||
|
||||
dot.className = 'card-status-dot loading';
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const url = svc.healthCheckUrl;
|
||||
const isRelative = url.startsWith('/');
|
||||
const isHttps = url.startsWith('https://');
|
||||
|
||||
// Relative URLs and HTTPS: normal fetch. HTTP cross-origin: no-cors.
|
||||
const fetchOpts = { signal: controller.signal };
|
||||
if (!isRelative && !isHttps) {
|
||||
fetchOpts.mode = 'no-cors';
|
||||
}
|
||||
|
||||
await fetch(url, fetchOpts);
|
||||
clearTimeout(timeout);
|
||||
dot.className = 'card-status-dot ok';
|
||||
} catch {
|
||||
dot.className = 'card-status-dot error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search Overlay ──
|
||||
const SearchOverlay = {
|
||||
overlay: null,
|
||||
input: null,
|
||||
results: null,
|
||||
services: [],
|
||||
selectedIndex: -1,
|
||||
isOpen: false,
|
||||
|
||||
init(config) {
|
||||
this.overlay = document.getElementById('search-overlay');
|
||||
this.input = document.getElementById('search-input');
|
||||
this.results = document.getElementById('search-results');
|
||||
this.services = config.services;
|
||||
|
||||
// Close on backdrop click
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) this.close();
|
||||
});
|
||||
|
||||
// Input handler
|
||||
this.input.addEventListener('input', () => {
|
||||
this.selectedIndex = -1;
|
||||
this.renderResults(this.input.value.trim());
|
||||
this.filterCards(this.input.value.trim());
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
this.input.addEventListener('keydown', (e) => {
|
||||
const items = this.results.querySelectorAll('.search-result-item');
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1);
|
||||
this.highlightItem(items);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.highlightItem(items);
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (this.selectedIndex >= 0 && items[this.selectedIndex]) {
|
||||
window.location.href = items[this.selectedIndex].dataset.url;
|
||||
} else if (items.length === 1) {
|
||||
window.location.href = items[0].dataset.url;
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Global '/' shortcut
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === '/' && !this.isOpen && !isInputFocused()) {
|
||||
e.preventDefault();
|
||||
this.open();
|
||||
} else if (e.key === 'Escape' && this.isOpen) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
open() {
|
||||
this.isOpen = true;
|
||||
this.overlay.classList.remove('hidden');
|
||||
this.input.value = '';
|
||||
this.results.innerHTML = '';
|
||||
this.selectedIndex = -1;
|
||||
this.clearCardFilters();
|
||||
setTimeout(() => this.input.focus(), 50);
|
||||
},
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.overlay.classList.add('hidden');
|
||||
this.input.value = '';
|
||||
this.results.innerHTML = '';
|
||||
this.selectedIndex = -1;
|
||||
this.clearCardFilters();
|
||||
},
|
||||
|
||||
fuzzyMatch(query, text) {
|
||||
query = query.toLowerCase();
|
||||
text = text.toLowerCase();
|
||||
let qi = 0;
|
||||
for (let ti = 0; ti < text.length && qi < query.length; ti++) {
|
||||
if (text[ti] === query[qi]) qi++;
|
||||
}
|
||||
return qi === query.length;
|
||||
},
|
||||
|
||||
renderResults(query) {
|
||||
this.results.innerHTML = '';
|
||||
if (!query) return;
|
||||
|
||||
const matches = this.services.filter(svc => this.fuzzyMatch(query, svc.name));
|
||||
|
||||
matches.forEach((svc, i) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'search-result-item' + (i === this.selectedIndex ? ' active' : '');
|
||||
item.dataset.url = svc.url;
|
||||
|
||||
item.innerHTML = `<i class="${svc.icon}"></i><span>${svc.name}</span>`;
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
window.location.href = svc.url;
|
||||
});
|
||||
|
||||
item.addEventListener('mouseenter', () => {
|
||||
this.selectedIndex = i;
|
||||
this.highlightItem(this.results.querySelectorAll('.search-result-item'));
|
||||
});
|
||||
|
||||
this.results.appendChild(item);
|
||||
});
|
||||
|
||||
if (matches.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'search-result-empty';
|
||||
empty.textContent = 'No services found';
|
||||
this.results.appendChild(empty);
|
||||
}
|
||||
},
|
||||
|
||||
highlightItem(items) {
|
||||
items.forEach((item, i) => {
|
||||
item.classList.toggle('active', i === this.selectedIndex);
|
||||
});
|
||||
if (items[this.selectedIndex]) {
|
||||
items[this.selectedIndex].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
},
|
||||
|
||||
filterCards(query) {
|
||||
const allCards = document.querySelectorAll('.service-card');
|
||||
const allSections = document.querySelectorAll('.category');
|
||||
|
||||
if (!query) {
|
||||
this.clearCardFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
allCards.forEach(card => {
|
||||
const name = card.dataset.name || '';
|
||||
const match = this.fuzzyMatch(query, name);
|
||||
card.classList.toggle('search-hidden', !match);
|
||||
card.classList.toggle('search-highlight', match);
|
||||
});
|
||||
|
||||
// Hide sections with no visible cards
|
||||
allSections.forEach(section => {
|
||||
const visibleCards = section.querySelectorAll('.service-card:not(.search-hidden)');
|
||||
section.classList.toggle('search-hidden', visibleCards.length === 0);
|
||||
});
|
||||
},
|
||||
|
||||
clearCardFilters() {
|
||||
document.querySelectorAll('.search-hidden').forEach(el => el.classList.remove('search-hidden'));
|
||||
document.querySelectorAll('.search-highlight').forEach(el => el.classList.remove('search-highlight'));
|
||||
}
|
||||
};
|
||||
|
||||
function isInputFocused() {
|
||||
const el = document.activeElement;
|
||||
return el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable);
|
||||
}
|
||||
|
||||
// ── Hero Widget (Clock / Weather / Greeting) ──
|
||||
class HeroWidget {
|
||||
constructor(config) {
|
||||
this.el = document.getElementById('hero-widget');
|
||||
this.city = config.weather?.city || 'Copenhagen';
|
||||
this.weatherData = null;
|
||||
|
||||
this.render();
|
||||
this.updateTime();
|
||||
this.fetchWeather();
|
||||
|
||||
setInterval(() => this.updateTime(), 1000);
|
||||
setInterval(() => this.fetchWeather(), 900000); // 15 min
|
||||
}
|
||||
|
||||
render() {
|
||||
this.el.innerHTML = `
|
||||
<p class="hero-greeting" id="hero-greeting"></p>
|
||||
<p class="hero-clock" id="hero-clock"></p>
|
||||
<p class="hero-date" id="hero-date"></p>
|
||||
<p class="hero-weather" id="hero-weather"><i class="ri-loader-4-line ri-spin"></i> Loading weather...</p>`;
|
||||
}
|
||||
|
||||
updateTime() {
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
let greeting;
|
||||
if (hour < 5) greeting = 'Good Night';
|
||||
else if (hour < 12) greeting = 'Good Morning';
|
||||
else if (hour < 17) greeting = 'Good Afternoon';
|
||||
else if (hour < 21) greeting = 'Good Evening';
|
||||
else greeting = 'Good Night';
|
||||
|
||||
const greetEl = document.getElementById('hero-greeting');
|
||||
const clockEl = document.getElementById('hero-clock');
|
||||
const dateEl = document.getElementById('hero-date');
|
||||
|
||||
if (greetEl) greetEl.textContent = greeting;
|
||||
if (clockEl) clockEl.textContent = now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
if (dateEl) dateEl.textContent = now.toLocaleDateString('en-GB', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
|
||||
async fetchWeather() {
|
||||
const weatherEl = document.getElementById('hero-weather');
|
||||
if (!weatherEl) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://wttr.in/${encodeURIComponent(this.city)}?format=j1`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
const current = data.current_condition?.[0];
|
||||
if (!current) throw new Error('No data');
|
||||
|
||||
const temp = current.temp_C;
|
||||
const desc = current.weatherDesc?.[0]?.value || '';
|
||||
const code = parseInt(current.weatherCode, 10);
|
||||
const iconClass = this.weatherIcon(code);
|
||||
|
||||
weatherEl.innerHTML = `<i class="${iconClass}"></i> ${temp}°C — ${desc}`;
|
||||
this.weatherData = current;
|
||||
} catch {
|
||||
weatherEl.innerHTML = `<i class="ri-cloud-off-line"></i> Weather unavailable`;
|
||||
}
|
||||
}
|
||||
|
||||
weatherIcon(code) {
|
||||
// wttr.in weather codes to Remix Icon
|
||||
if (code === 113) return 'ri-sun-line'; // Clear/Sunny
|
||||
if (code === 116) return 'ri-sun-cloudy-line'; // Partly cloudy
|
||||
if (code === 119 || code === 122) return 'ri-cloudy-line'; // Cloudy / Overcast
|
||||
if (code === 143 || code === 248 || code === 260) return 'ri-mist-line'; // Fog/Mist
|
||||
if ([176, 263, 266, 293, 296, 299, 302, 305, 308, 353, 356, 359].includes(code)) return 'ri-rainy-line'; // Rain
|
||||
if ([179, 182, 185, 227, 230, 323, 326, 329, 332, 335, 338, 368, 371, 374, 377].includes(code)) return 'ri-snowy-line'; // Snow
|
||||
if ([200, 386, 389, 392, 395].includes(code)) return 'ri-thunderstorms-line'; // Thunder
|
||||
if ([281, 284, 311, 314, 317, 320, 350, 362, 365].includes(code)) return 'ri-drizzle-line'; // Sleet/Freezing
|
||||
return 'ri-cloudy-line';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mobile Nav Toggle (event delegation for dynamic links) ──
|
||||
function initNavToggle() {
|
||||
const navToggle = document.getElementById('nav-toggle');
|
||||
const nav = document.getElementById('nav');
|
||||
|
||||
@@ -11,16 +474,18 @@
|
||||
navToggle.querySelector('i').className = open ? 'ri-close-line' : 'ri-menu-line';
|
||||
});
|
||||
|
||||
// Close nav on link click (mobile)
|
||||
nav.querySelectorAll('a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
// Event delegation for dynamically added nav links
|
||||
nav.addEventListener('click', (e) => {
|
||||
if (e.target.closest('a')) {
|
||||
nav.classList.remove('open');
|
||||
navToggle.setAttribute('aria-expanded', 'false');
|
||||
navToggle.querySelector('i').className = 'ri-menu-line';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Header Scroll Effect ──
|
||||
function initScrollEffect() {
|
||||
const header = document.getElementById('header');
|
||||
let ticking = false;
|
||||
|
||||
@@ -33,73 +498,37 @@
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
|
||||
// ── Status Monitor ──
|
||||
class StatusMonitor {
|
||||
constructor() {
|
||||
this.services = [
|
||||
{
|
||||
id: 'status-glances',
|
||||
url: '/proxy/glances/api/4/quicklook',
|
||||
parse: (data) => `CPU ${Math.round(data.cpu_percent)}% | RAM ${Math.round(data.mem_percent)}%`
|
||||
},
|
||||
{
|
||||
id: 'status-uptime',
|
||||
url: '/proxy/uptime/api/status-page/heartbeat/default',
|
||||
parse: (data) => {
|
||||
const groups = data.heartbeatList || {};
|
||||
const monitors = Object.values(groups);
|
||||
const total = monitors.length;
|
||||
const up = monitors.filter(beats => beats.length > 0 && beats[beats.length - 1].status === 1).length;
|
||||
return `${up}/${total} services up`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'status-cadvisor',
|
||||
url: '/proxy/cadvisor/api/v1.0/machine',
|
||||
parse: (data) => {
|
||||
const cores = data.num_cores || '?';
|
||||
const ramGB = data.memory_capacity ? (data.memory_capacity / 1073741824).toFixed(0) : '?';
|
||||
return `${cores} cores | ${ramGB} GB RAM`;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
this.poll();
|
||||
setInterval(() => this.poll(), 60000);
|
||||
}
|
||||
|
||||
async poll() {
|
||||
this.services.forEach(svc => this.check(svc));
|
||||
}
|
||||
|
||||
async check(svc) {
|
||||
const card = document.getElementById(svc.id);
|
||||
if (!card) return;
|
||||
|
||||
const dot = card.querySelector('.status-dot');
|
||||
const value = card.querySelector('.status-value');
|
||||
|
||||
dot.className = 'status-dot loading';
|
||||
// ── Init ──
|
||||
async function init() {
|
||||
// These work independently of config
|
||||
initNavToggle();
|
||||
initScrollEffect();
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||
const config = await loadConfig();
|
||||
|
||||
const res = await fetch(svc.url, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
renderNav(config);
|
||||
renderStatusBar(config);
|
||||
renderServices(config);
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
dot.className = 'status-dot ok';
|
||||
value.textContent = svc.parse(data);
|
||||
} catch {
|
||||
dot.className = 'status-dot error';
|
||||
value.textContent = 'Unreachable';
|
||||
new StatusMonitor(config);
|
||||
SearchOverlay.init(config);
|
||||
new HeroWidget(config);
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize dashboard:', err);
|
||||
const container = document.getElementById('services-container');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="container" style="padding: 60px 24px; text-align: center;">
|
||||
<i class="ri-error-warning-line" style="font-size: 48px; color: var(--red); margin-bottom: 16px;"></i>
|
||||
<h2 style="color: var(--text-heading); margin-bottom: 8px;">Failed to load services</h2>
|
||||
<p style="color: var(--text-muted);">Could not load services.json. Check the console for details.</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new StatusMonitor();
|
||||
init();
|
||||
})();
|
||||
|
||||
260
index.html
260
index.html
@@ -30,13 +30,7 @@
|
||||
<header id="header">
|
||||
<div class="header-inner">
|
||||
<a href="index.html" class="logo">Intranet<span>.</span>local</a>
|
||||
<nav id="nav" class="nav-links">
|
||||
<a href="#infrastructure">Infra</a>
|
||||
<a href="#monitoring">Monitoring</a>
|
||||
<a href="#development">Dev</a>
|
||||
<a href="#media-streaming">Media</a>
|
||||
<a href="#tesla">Tesla</a>
|
||||
</nav>
|
||||
<nav id="nav" class="nav-links"></nav>
|
||||
<button id="nav-toggle" class="nav-toggle" aria-label="Toggle navigation" aria-expanded="false">
|
||||
<i class="ri-menu-line"></i>
|
||||
</button>
|
||||
@@ -45,249 +39,33 @@
|
||||
|
||||
<main>
|
||||
|
||||
<!-- Hero -->
|
||||
<!-- Hero (JS renders clock/weather widget inside) -->
|
||||
<section id="hero">
|
||||
<div class="hero-content">
|
||||
<h1>Welcome!</h1>
|
||||
<p>Your local network services portal.</p>
|
||||
</div>
|
||||
<div class="hero-content" id="hero-widget"></div>
|
||||
</section>
|
||||
|
||||
<!-- Server Status Bar -->
|
||||
<!-- Search overlay (hidden by default) -->
|
||||
<div id="search-overlay" class="search-overlay hidden">
|
||||
<div class="search-container">
|
||||
<div class="search-input-wrap">
|
||||
<i class="ri-search-line"></i>
|
||||
<input type="text" id="search-input" placeholder="Search services..." autocomplete="off">
|
||||
<kbd>/</kbd>
|
||||
</div>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status bar (3 rich monitors, rendered from config) -->
|
||||
<section id="status-bar">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-pulse-line"></i> System Status</h2>
|
||||
<div class="status-grid">
|
||||
<a href="http://192.168.1.13:3010" class="status-card" id="status-uptime">
|
||||
<div class="status-dot loading"></div>
|
||||
<div class="status-info">
|
||||
<span class="status-label">Uptime Kuma</span>
|
||||
<span class="status-value">Checking...</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:61208" class="status-card" id="status-glances">
|
||||
<div class="status-dot loading"></div>
|
||||
<div class="status-info">
|
||||
<span class="status-label">Glances</span>
|
||||
<span class="status-value">Checking...</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:8084" class="status-card" id="status-cadvisor">
|
||||
<div class="status-dot loading"></div>
|
||||
<div class="status-info">
|
||||
<span class="status-label">cAdvisor</span>
|
||||
<span class="status-value">Checking...</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-grid" id="status-grid"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Infrastructure & Security -->
|
||||
<section id="infrastructure" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-shield-check-line"></i> Infrastructure & Security</h2>
|
||||
<div class="card-grid">
|
||||
<a href="http://adguard.local" class="service-card">
|
||||
<i class="ri-shield-check-line"></i>
|
||||
<span class="card-name">AdGuard Home</span>
|
||||
</a>
|
||||
<a href="http://omada.local" class="service-card">
|
||||
<i class="ri-wifi-line"></i>
|
||||
<span class="card-name">Omada Controller</span>
|
||||
</a>
|
||||
<a href="https://auth.hasanbasic.dk" class="service-card">
|
||||
<i class="ri-login-box-fill"></i>
|
||||
<span class="card-name">Authentik</span>
|
||||
</a>
|
||||
<a href="http://longhorn.local" class="service-card">
|
||||
<i class="ri-storage-line"></i>
|
||||
<span class="card-name">Longhorn</span>
|
||||
</a>
|
||||
<a href="http://rancher.local" class="service-card">
|
||||
<i class="ri-dashboard-line"></i>
|
||||
<span class="card-name">Rancher</span>
|
||||
</a>
|
||||
<a href="http://nas.local" class="service-card">
|
||||
<i class="ri-server-line"></i>
|
||||
<span class="card-name">Synology NAS</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Monitoring & Analytics -->
|
||||
<section id="monitoring" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-pulse-line"></i> Monitoring & Analytics</h2>
|
||||
<div class="card-grid">
|
||||
<a href="http://192.168.1.13:3010" class="service-card">
|
||||
<i class="ri-heart-pulse-line"></i>
|
||||
<span class="card-name">Uptime Kuma</span>
|
||||
<span class="card-sub">192.168.1.13:3010</span>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:61208" class="service-card">
|
||||
<i class="ri-eye-line"></i>
|
||||
<span class="card-name">Glances</span>
|
||||
<span class="card-sub">192.168.1.13:61208</span>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:8084" class="service-card">
|
||||
<i class="ri-container-line"></i>
|
||||
<span class="card-name">cAdvisor</span>
|
||||
<span class="card-sub">192.168.1.13:8084</span>
|
||||
</a>
|
||||
<a href="https://umami.hasanbasic.dk" class="service-card">
|
||||
<i class="ri-bar-chart-box-line"></i>
|
||||
<span class="card-name">Umami</span>
|
||||
<span class="card-sub">umami.hasanbasic.dk</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Development & Tools -->
|
||||
<section id="development" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-code-s-slash-line"></i> Development & Tools</h2>
|
||||
<div class="card-grid">
|
||||
<a href="https://code.mirzahasanbasic.dk" class="service-card">
|
||||
<i class="ri-code-s-slash-line"></i>
|
||||
<span class="card-name">Code Server</span>
|
||||
</a>
|
||||
<a href="https://git.mirzahasanbasic.dk" class="service-card">
|
||||
<i class="ri-git-repository-line"></i>
|
||||
<span class="card-name">Gitea</span>
|
||||
</a>
|
||||
<a href="http://pgadmin.local" class="service-card">
|
||||
<i class="ri-database-2-line"></i>
|
||||
<span class="card-name">pgAdmin</span>
|
||||
</a>
|
||||
<a href="http://portainer.local" class="service-card">
|
||||
<i class="ri-ship-line"></i>
|
||||
<span class="card-name">Portainer</span>
|
||||
</a>
|
||||
<a href="http://minio.local" class="service-card">
|
||||
<i class="ri-database-line"></i>
|
||||
<span class="card-name">MinIO</span>
|
||||
</a>
|
||||
<a href="https://histogram.hasanbasic.dk" class="service-card">
|
||||
<i class="ri-database-2-line"></i>
|
||||
<span class="card-name">Histogram</span>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:3080" class="service-card">
|
||||
<i class="ri-chat-smile-ai-line"></i>
|
||||
<span class="card-name">LibreChat</span>
|
||||
<span class="card-sub">192.168.1.13:3080</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Media Streaming -->
|
||||
<section id="media-streaming" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-film-line"></i> Media Streaming</h2>
|
||||
<div class="card-grid">
|
||||
<a href="https://plex.mirzahasanbasic.dk" class="service-card">
|
||||
<i class="ri-film-line"></i>
|
||||
<span class="card-name">Plex</span>
|
||||
</a>
|
||||
<a href="https://overseer.hasanbasic.dk" class="service-card">
|
||||
<i class="ri-movie-2-line"></i>
|
||||
<span class="card-name">Overseerr</span>
|
||||
</a>
|
||||
<a href="https://immich.hasanbasic.dk" class="service-card">
|
||||
<i class="ri-image-line"></i>
|
||||
<span class="card-name">Immich</span>
|
||||
</a>
|
||||
<a href="http://ombi.local" class="service-card">
|
||||
<i class="ri-hand-heart-fill"></i>
|
||||
<span class="card-name">Ombi</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Media Automation -->
|
||||
<section id="media-automation" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-robot-line"></i> Media Automation</h2>
|
||||
<div class="card-grid">
|
||||
<a href="http://sonarr.local" class="service-card">
|
||||
<i class="ri-tv-line"></i>
|
||||
<span class="card-name">Sonarr</span>
|
||||
</a>
|
||||
<a href="http://radarr.local" class="service-card">
|
||||
<i class="ri-movie-line"></i>
|
||||
<span class="card-name">Radarr</span>
|
||||
</a>
|
||||
<a href="http://prowlarr.local" class="service-card">
|
||||
<i class="ri-search-line"></i>
|
||||
<span class="card-name">Prowlarr</span>
|
||||
</a>
|
||||
<a href="http://lidarr.local" class="service-card">
|
||||
<i class="ri-music-line"></i>
|
||||
<span class="card-name">Lidarr</span>
|
||||
</a>
|
||||
<a href="http://readarr.local" class="service-card">
|
||||
<i class="ri-book-read-line"></i>
|
||||
<span class="card-name">Readarr</span>
|
||||
</a>
|
||||
<a href="http://bazarr.local" class="service-card">
|
||||
<i class="ri-translate"></i>
|
||||
<span class="card-name">Bazarr</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tesla -->
|
||||
<section id="tesla" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-car-line"></i> Tesla</h2>
|
||||
<div class="card-grid">
|
||||
<a href="http://teslamate.local" class="service-card">
|
||||
<i class="ri-car-line"></i>
|
||||
<span class="card-name">TeslaMate</span>
|
||||
</a>
|
||||
<a href="http://tesla-grafana.local" class="service-card">
|
||||
<i class="ri-line-chart-line"></i>
|
||||
<span class="card-name">Tesla Charts</span>
|
||||
</a>
|
||||
<a href="http://tesla-pgadmin.local" class="service-card">
|
||||
<i class="ri-database-line"></i>
|
||||
<span class="card-name">Tesla DB</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Productivity & Utilities -->
|
||||
<section id="productivity" class="category">
|
||||
<div class="container">
|
||||
<h2 class="section-title"><i class="ri-tools-line"></i> Productivity & Utilities</h2>
|
||||
<div class="card-grid">
|
||||
<a href="http://stirling-pdf.local" class="service-card">
|
||||
<i class="ri-file-pdf-2-line"></i>
|
||||
<span class="card-name">PDF Tools</span>
|
||||
</a>
|
||||
<a href="https://bookstack.hasanbasic.dk" class="service-card">
|
||||
<i class="ri-book-line"></i>
|
||||
<span class="card-name">BookStack</span>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:8089" class="service-card">
|
||||
<i class="ri-restaurant-line"></i>
|
||||
<span class="card-name">Tandoor</span>
|
||||
<span class="card-sub">192.168.1.13:8089</span>
|
||||
</a>
|
||||
<a href="http://192.168.1.13:8000" class="service-card">
|
||||
<i class="ri-file-paper-2-line"></i>
|
||||
<span class="card-name">Paperless</span>
|
||||
<span class="card-sub">192.168.1.13:8000</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Service sections (all rendered from JSON) -->
|
||||
<div id="services-container"></div>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
79
services.json
Normal file
79
services.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"categories": [
|
||||
{ "id": "infrastructure", "title": "Infrastructure & Security", "icon": "ri-shield-check-line", "navLabel": "Infra", "showInNav": true },
|
||||
{ "id": "monitoring", "title": "Monitoring & Analytics", "icon": "ri-pulse-line", "navLabel": "Monitoring", "showInNav": true },
|
||||
{ "id": "development", "title": "Development & Tools", "icon": "ri-code-s-slash-line", "navLabel": "Dev", "showInNav": true },
|
||||
{ "id": "media-streaming", "title": "Media Streaming", "icon": "ri-film-line", "navLabel": "Media", "showInNav": true },
|
||||
{ "id": "media-automation", "title": "Media Automation", "icon": "ri-robot-line", "navLabel": "Automation", "showInNav": false },
|
||||
{ "id": "tesla", "title": "Tesla", "icon": "ri-car-line", "navLabel": "Tesla", "showInNav": true },
|
||||
{ "id": "productivity", "title": "Productivity & Utilities", "icon": "ri-tools-line", "navLabel": "Tools", "showInNav": false }
|
||||
],
|
||||
"services": [
|
||||
{ "name": "AdGuard Home", "url": "http://adguard.local", "icon": "ri-shield-check-line", "category": "infrastructure", "healthCheckUrl": "http://adguard.local" },
|
||||
{ "name": "Omada Controller","url": "http://omada.local", "icon": "ri-wifi-line", "category": "infrastructure", "healthCheckUrl": "http://omada.local" },
|
||||
{ "name": "Authentik", "url": "https://auth.hasanbasic.dk", "icon": "ri-login-box-fill", "category": "infrastructure", "healthCheckUrl": "https://auth.hasanbasic.dk" },
|
||||
{ "name": "Longhorn", "url": "http://longhorn.local", "icon": "ri-storage-line", "category": "infrastructure", "healthCheckUrl": "http://longhorn.local" },
|
||||
{ "name": "Rancher", "url": "http://rancher.local", "icon": "ri-dashboard-line", "category": "infrastructure", "healthCheckUrl": "http://rancher.local" },
|
||||
{ "name": "Synology NAS", "url": "http://nas.local", "icon": "ri-server-line", "category": "infrastructure", "healthCheckUrl": "http://nas.local" },
|
||||
|
||||
{ "name": "Uptime Kuma", "url": "http://192.168.1.13:3010", "icon": "ri-heart-pulse-line", "category": "monitoring", "sub": "192.168.1.13:3010", "healthCheckUrl": "/proxy/uptime/" },
|
||||
{ "name": "Glances", "url": "http://192.168.1.13:61208", "icon": "ri-eye-line", "category": "monitoring", "sub": "192.168.1.13:61208", "healthCheckUrl": "/proxy/glances/" },
|
||||
{ "name": "cAdvisor", "url": "http://192.168.1.13:8084", "icon": "ri-container-line", "category": "monitoring", "sub": "192.168.1.13:8084", "healthCheckUrl": "/proxy/cadvisor/" },
|
||||
{ "name": "Umami", "url": "https://umami.hasanbasic.dk", "icon": "ri-bar-chart-box-line", "category": "monitoring", "sub": "umami.hasanbasic.dk", "healthCheckUrl": "https://umami.hasanbasic.dk" },
|
||||
|
||||
{ "name": "Code Server", "url": "https://code.mirzahasanbasic.dk", "icon": "ri-code-s-slash-line", "category": "development", "healthCheckUrl": "https://code.mirzahasanbasic.dk" },
|
||||
{ "name": "Gitea", "url": "https://git.mirzahasanbasic.dk", "icon": "ri-git-repository-line", "category": "development", "healthCheckUrl": "https://git.mirzahasanbasic.dk" },
|
||||
{ "name": "pgAdmin", "url": "http://pgadmin.local", "icon": "ri-database-2-line", "category": "development", "healthCheckUrl": "http://pgadmin.local" },
|
||||
{ "name": "Portainer", "url": "http://portainer.local", "icon": "ri-ship-line", "category": "development", "healthCheckUrl": "http://portainer.local" },
|
||||
{ "name": "MinIO", "url": "http://minio.local", "icon": "ri-database-line", "category": "development", "healthCheckUrl": "http://minio.local" },
|
||||
{ "name": "Histogram", "url": "https://histogram.hasanbasic.dk", "icon": "ri-database-2-line", "category": "development", "healthCheckUrl": "https://histogram.hasanbasic.dk" },
|
||||
{ "name": "LibreChat", "url": "http://192.168.1.13:3080", "icon": "ri-chat-smile-ai-line", "category": "development", "sub": "192.168.1.13:3080", "healthCheckUrl": "http://192.168.1.13:3080" },
|
||||
|
||||
{ "name": "Plex", "url": "https://plex.mirzahasanbasic.dk", "icon": "ri-film-line", "category": "media-streaming", "healthCheckUrl": "https://plex.mirzahasanbasic.dk" },
|
||||
{ "name": "Overseerr", "url": "https://overseer.hasanbasic.dk", "icon": "ri-movie-2-line", "category": "media-streaming", "healthCheckUrl": "https://overseer.hasanbasic.dk" },
|
||||
{ "name": "Immich", "url": "https://immich.hasanbasic.dk", "icon": "ri-image-line", "category": "media-streaming", "healthCheckUrl": "https://immich.hasanbasic.dk" },
|
||||
{ "name": "Ombi", "url": "http://ombi.local", "icon": "ri-hand-heart-fill", "category": "media-streaming", "healthCheckUrl": "http://ombi.local" },
|
||||
|
||||
{ "name": "Sonarr", "url": "http://sonarr.local", "icon": "ri-tv-line", "category": "media-automation", "healthCheckUrl": "http://sonarr.local" },
|
||||
{ "name": "Radarr", "url": "http://radarr.local", "icon": "ri-movie-line", "category": "media-automation", "healthCheckUrl": "http://radarr.local" },
|
||||
{ "name": "Prowlarr", "url": "http://prowlarr.local", "icon": "ri-search-line", "category": "media-automation", "healthCheckUrl": "http://prowlarr.local" },
|
||||
{ "name": "Lidarr", "url": "http://lidarr.local", "icon": "ri-music-line", "category": "media-automation", "healthCheckUrl": "http://lidarr.local" },
|
||||
{ "name": "Readarr", "url": "http://readarr.local", "icon": "ri-book-read-line", "category": "media-automation", "healthCheckUrl": "http://readarr.local" },
|
||||
{ "name": "Bazarr", "url": "http://bazarr.local", "icon": "ri-translate", "category": "media-automation", "healthCheckUrl": "http://bazarr.local" },
|
||||
|
||||
{ "name": "TeslaMate", "url": "http://teslamate.local", "icon": "ri-car-line", "category": "tesla", "healthCheckUrl": "http://teslamate.local" },
|
||||
{ "name": "Tesla Charts", "url": "http://tesla-grafana.local", "icon": "ri-line-chart-line", "category": "tesla", "healthCheckUrl": "http://tesla-grafana.local" },
|
||||
{ "name": "Tesla DB", "url": "http://tesla-pgadmin.local", "icon": "ri-database-line", "category": "tesla", "healthCheckUrl": "http://tesla-pgadmin.local" },
|
||||
|
||||
{ "name": "PDF Tools", "url": "http://stirling-pdf.local", "icon": "ri-file-pdf-2-line", "category": "productivity", "healthCheckUrl": "http://stirling-pdf.local" },
|
||||
{ "name": "BookStack", "url": "https://bookstack.hasanbasic.dk", "icon": "ri-book-line", "category": "productivity", "healthCheckUrl": "https://bookstack.hasanbasic.dk" },
|
||||
{ "name": "Tandoor", "url": "http://192.168.1.13:8089", "icon": "ri-restaurant-line", "category": "productivity", "sub": "192.168.1.13:8089", "healthCheckUrl": "http://192.168.1.13:8089" },
|
||||
{ "name": "Paperless", "url": "http://192.168.1.13:8000", "icon": "ri-file-paper-2-line", "category": "productivity", "sub": "192.168.1.13:8000", "healthCheckUrl": "http://192.168.1.13:8000" }
|
||||
],
|
||||
"statusMonitors": [
|
||||
{
|
||||
"id": "status-uptime",
|
||||
"label": "Uptime Kuma",
|
||||
"url": "http://192.168.1.13:3010",
|
||||
"apiUrl": "/proxy/uptime/api/status-page/heartbeat/default",
|
||||
"parseType": "uptime"
|
||||
},
|
||||
{
|
||||
"id": "status-glances",
|
||||
"label": "Glances",
|
||||
"url": "http://192.168.1.13:61208",
|
||||
"apiUrl": "/proxy/glances/api/4/quicklook",
|
||||
"parseType": "glances"
|
||||
},
|
||||
{
|
||||
"id": "status-cadvisor",
|
||||
"label": "cAdvisor",
|
||||
"url": "http://192.168.1.13:8084",
|
||||
"apiUrl": "/proxy/cadvisor/api/v1.0/machine",
|
||||
"parseType": "cadvisor"
|
||||
}
|
||||
],
|
||||
"weather": {
|
||||
"city": "Copenhagen"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user