2765 lines
122 KiB
YAML
2765 lines
122 KiB
YAML
# Glance Dashboard for Kisfenyo
|
||
# Namespace: glance-system
|
||
# Domain: kisfenyo.dooplex.hu
|
||
# Version: v0.8.4
|
||
#
|
||
# Features:
|
||
# - Custom background image
|
||
# - Custom logo
|
||
# - Weather widget (Budapest)
|
||
# - YouTube subscriptions
|
||
# - RSS feeds
|
||
# - To-do list
|
||
# - iFrames for Cal.com, Google Calendar, Outline
|
||
# - Bookmarks/Links to all apps
|
||
# - Calendar widget
|
||
#
|
||
# Authentik Integration:
|
||
# 1. Create Application: "Glance Home"
|
||
# 2. Create Provider: Proxy Provider with external host https://kisfenyo.dooplex.hu
|
||
# 3. Create Outpost: glance-outpost
|
||
# 4. Update auth-url annotation with actual outpost service name
|
||
---
|
||
apiVersion: v1
|
||
kind: ConfigMap
|
||
metadata:
|
||
name: glance-config-kisfenyo
|
||
namespace: glance-system
|
||
labels:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
data:
|
||
glance.yml: |
|
||
# Glance Configuration
|
||
# Documentation: https://github.com/glanceapp/glance/blob/main/docs/configuration.md
|
||
|
||
server:
|
||
host: 0.0.0.0
|
||
port: 8080
|
||
assets-path: /app/assets
|
||
|
||
document:
|
||
head: |
|
||
<style>
|
||
/* Glance Quick Launch overlay */
|
||
#gql-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,.35);
|
||
display: none;
|
||
align-items: flex-start;
|
||
justify-content: center;
|
||
padding-top: 10vh;
|
||
z-index: 99999;
|
||
}
|
||
#gql-box {
|
||
width: min(720px, 92vw);
|
||
border-radius: 14px;
|
||
background: rgba(20, 22, 30, .92);
|
||
box-shadow: 0 10px 30px rgba(0,0,0,.35);
|
||
overflow: hidden;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
#gql-input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
padding: 14px 16px;
|
||
border: 0;
|
||
outline: none;
|
||
font-size: 16px;
|
||
color: #fff;
|
||
background: rgba(0,0,0,.25);
|
||
}
|
||
#gql-hint {
|
||
padding: 8px 16px;
|
||
font-size: 12px;
|
||
opacity: .75;
|
||
color: #fff;
|
||
}
|
||
#gql-list {
|
||
max-height: 360px;
|
||
overflow: auto;
|
||
padding: 6px;
|
||
}
|
||
.gql-item {
|
||
padding: 10px 10px;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
color: #fff;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: baseline;
|
||
}
|
||
.gql-item:hover, .gql-item.active { background: rgba(255,255,255,.10); }
|
||
.gql-title { font-weight: 600; }
|
||
.gql-url { font-size: 12px; opacity: .7; word-break: break-all; }
|
||
</style>
|
||
|
||
<script>
|
||
(() => {
|
||
const MAX_RESULTS = 12;
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'gql-overlay';
|
||
overlay.innerHTML = `
|
||
<div id="gql-box" role="dialog" aria-modal="true">
|
||
<input id="gql-input" type="text" autocomplete="off" spellcheck="false" placeholder="Type to search bookmarks…"/>
|
||
<div id="gql-hint">↑/↓ to navigate • Enter to open • Esc to close</div>
|
||
<div id="gql-list"></div>
|
||
</div>
|
||
`;
|
||
document.addEventListener('DOMContentLoaded', () => document.body.appendChild(overlay));
|
||
|
||
const $ = (sel) => overlay.querySelector(sel);
|
||
const input = () => $('#gql-input');
|
||
const list = () => $('#gql-list');
|
||
|
||
let indexed = [];
|
||
let activeIndex = 0;
|
||
let lastQuery = '';
|
||
|
||
const BOOKMARKS_INDEX_URL = '/assets/bookmarks.json';
|
||
|
||
let indexLoaded = false;
|
||
let indexLoading = null;
|
||
|
||
function indexLinksFromDom() {
|
||
const anchors = document.querySelectorAll('.widget.widget-type-bookmarks a.bookmarks-link[href]');
|
||
indexed = Array.from(anchors).map(a => ({
|
||
title: (a.textContent || '').trim(),
|
||
url: a.href,
|
||
meta: ''
|
||
}));
|
||
}
|
||
|
||
function loadBookmarksIndex() {
|
||
if (indexLoaded) return Promise.resolve();
|
||
if (indexLoading) return indexLoading;
|
||
|
||
indexLoading = fetch(BOOKMARKS_INDEX_URL, { cache: 'no-store' })
|
||
.then(res => {
|
||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
return res.json();
|
||
})
|
||
.then(data => {
|
||
indexed = data.map(x => ({
|
||
title: String(x.title ?? x.url ?? ''),
|
||
url: String(x.url ?? ''),
|
||
meta: [x.page, x.widget, x.group].filter(Boolean).map(v => String(v)).join(' • ')
|
||
}));
|
||
indexLoaded = true;
|
||
})
|
||
.catch(e => {
|
||
console.warn('Could not load bookmarks index, falling back to DOM only:', e);
|
||
indexLinksFromDom();
|
||
indexLoaded = true;
|
||
});
|
||
|
||
return indexLoading;
|
||
}
|
||
|
||
// Load ASAP (works even if DOMContentLoaded already happened)
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => loadBookmarksIndex());
|
||
} else {
|
||
loadBookmarksIndex();
|
||
}
|
||
|
||
function normalize(s) {
|
||
return String(s ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function score(item, q) {
|
||
const t = normalize(item.title);
|
||
if (!q) return 0;
|
||
|
||
if (t === q) return 200;
|
||
if (t.startsWith(q)) return 120;
|
||
|
||
// boost if query matches start of any word
|
||
const words = t.split(' ');
|
||
if (words.some(w => w.startsWith(q))) return 95;
|
||
|
||
if (t.includes(q)) return 70;
|
||
return -1;
|
||
}
|
||
|
||
function render(q) {
|
||
const results = indexed
|
||
.map(it => ({ ...it, s: score(it, q) }))
|
||
.filter(it => it.s >= 0)
|
||
.sort((a,b) => b.s - a.s)
|
||
.slice(0, MAX_RESULTS);
|
||
|
||
activeIndex = 0;
|
||
list().innerHTML = results.map((r, i) => `
|
||
<div class="gql-item ${i===0 ? 'active' : ''}" data-i="${i}">
|
||
<div class="gql-title">${escapeHtml(r.title || r.url)}</div>
|
||
<div class="gql-url">${escapeHtml(r.meta || r.url)}</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
list().onclick = (e) => {
|
||
const item = e.target.closest('.gql-item');
|
||
if (!item) return;
|
||
const i = Number(item.dataset.i);
|
||
openResult(results, i, false);
|
||
};
|
||
|
||
return results;
|
||
}
|
||
|
||
function setActive(i) {
|
||
const items = Array.from(list().querySelectorAll('.gql-item'));
|
||
items.forEach(el => el.classList.remove('active'));
|
||
if (items[i]) {
|
||
items[i].classList.add('active');
|
||
items[i].scrollIntoView({ block: 'nearest' });
|
||
}
|
||
}
|
||
|
||
function openOverlay(withInitialText = '') {
|
||
overlay.style.display = 'flex';
|
||
input().value = withInitialText;
|
||
lastQuery = withInitialText;
|
||
|
||
// show something instantly
|
||
list().innerHTML = `<div class="gql-item active"><div class="gql-title">Loading…</div></div>`;
|
||
|
||
// then render once index is available
|
||
loadBookmarksIndex().then(() => {
|
||
render(normalize(input().value));
|
||
input().focus();
|
||
});
|
||
|
||
function onInput() {
|
||
lastQuery = input().value;
|
||
render(normalize(lastQuery));
|
||
}
|
||
input().oninput = onInput;
|
||
|
||
input().onkeydown = (e) => {
|
||
const items = list().querySelectorAll('.gql-item');
|
||
const count = items.length;
|
||
|
||
if (e.key === 'Escape') { e.preventDefault(); closeOverlay(); return; }
|
||
if (e.key === 'ArrowDown' && count) { e.preventDefault(); activeIndex = Math.min(activeIndex+1, count-1); setActive(activeIndex); return; }
|
||
if (e.key === 'ArrowUp' && count) { e.preventDefault(); activeIndex = Math.max(activeIndex-1, 0); setActive(activeIndex); return; }
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const q = normalize(input().value);
|
||
const resultsNow = indexed
|
||
.map(it => ({ ...it, s: score(it, q) }))
|
||
.filter(it => it.s >= 0)
|
||
.sort((a,b) => b.s - a.s)
|
||
.slice(0, MAX_RESULTS);
|
||
|
||
openResult(resultsNow, activeIndex);
|
||
}
|
||
};
|
||
|
||
overlay.onclick = (e) => { if (e.target === overlay) closeOverlay(); };
|
||
}
|
||
|
||
function openResult(results, i) {
|
||
const r = results[i];
|
||
if (!r) return;
|
||
closeOverlay();
|
||
window.open(r.url, '_blank', 'noopener,noreferrer');
|
||
}
|
||
|
||
function closeOverlay() {
|
||
overlay.style.display = 'none';
|
||
list().innerHTML = '';
|
||
activeIndex = 0;
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return (str || '').replace(/[&<>"']/g, c => ({
|
||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||
}[c]));
|
||
}
|
||
|
||
// Global key handler: "just start typing"
|
||
window.addEventListener('keydown', (e) => {
|
||
if (overlay.style.display === 'flex') return;
|
||
|
||
// ignore when typing in inputs/textareas or using modifier shortcuts
|
||
const tag = (document.activeElement && document.activeElement.tagName || '').toLowerCase();
|
||
const typingIntoField = tag === 'input' || tag === 'textarea' || document.activeElement?.isContentEditable;
|
||
if (typingIntoField) return;
|
||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||
|
||
// Printable character opens overlay with that character
|
||
if (e.key.length === 1 && !e.repeat) {
|
||
openOverlay(e.key);
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
|
||
// Optional: slash to open empty
|
||
if (e.key === '/') {
|
||
openOverlay('');
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
})();
|
||
|
||
// ================================
|
||
// Todo Widget Controller
|
||
// ================================
|
||
(function() {
|
||
function initTodoWidgets() {
|
||
document.querySelectorAll('.todo-widget').forEach(function(widget) {
|
||
if (widget.dataset.initialized) return;
|
||
widget.dataset.initialized = 'true';
|
||
|
||
const API = widget.dataset.api;
|
||
const USER = widget.dataset.user;
|
||
const KEY = widget.dataset.key;
|
||
|
||
function apiUrl(path) {
|
||
return API + path + (KEY ? '?key=' + KEY : '');
|
||
}
|
||
|
||
const input = widget.querySelector('.todo-add-input');
|
||
const addBtn = widget.querySelector('.todo-add-btn');
|
||
|
||
async function addTodo() {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
input.disabled = true;
|
||
try {
|
||
const r = await fetch(apiUrl('/userdata/' + USER + '/todos'), {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({text: text, done: false})
|
||
});
|
||
if (r.ok) location.reload();
|
||
else console.error('Todo add failed:', await r.text());
|
||
} catch(e) { console.error('Todo add error:', e); }
|
||
input.disabled = false;
|
||
}
|
||
|
||
if (input) {
|
||
input.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); addTodo(); }
|
||
});
|
||
}
|
||
if (addBtn) {
|
||
addBtn.addEventListener('click', function() {
|
||
if (input.value.trim()) addTodo();
|
||
else input.focus();
|
||
});
|
||
}
|
||
|
||
widget.addEventListener('click', async function(e) {
|
||
const item = e.target.closest('.todo-item');
|
||
if (!item) return;
|
||
|
||
if (e.target.closest('.todo-checkbox')) {
|
||
const id = item.dataset.id;
|
||
const text = item.dataset.text;
|
||
const newDone = item.dataset.done !== 'true';
|
||
try {
|
||
const r = await fetch(apiUrl('/userdata/' + USER + '/todos/' + id), {
|
||
method: 'PUT',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({text: text, done: newDone})
|
||
});
|
||
if (r.ok) location.reload();
|
||
} catch(e) { console.error('Todo toggle error:', e); }
|
||
}
|
||
|
||
if (e.target.closest('.todo-delete')) {
|
||
const id = item.dataset.id;
|
||
try {
|
||
const r = await fetch(apiUrl('/userdata/' + USER + '/todos/' + id), {
|
||
method: 'DELETE'
|
||
});
|
||
if (r.ok) location.reload();
|
||
} catch(e) { console.error('Todo delete error:', e); }
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initTodoWidgets);
|
||
} else {
|
||
initTodoWidgets();
|
||
}
|
||
// Also run after a delay for dynamically loaded content
|
||
setTimeout(initTodoWidgets, 500);
|
||
setTimeout(initTodoWidgets, 1500);
|
||
})();
|
||
|
||
// ================================
|
||
// Motivation Widget Controller
|
||
// ================================
|
||
(function() {
|
||
function initMotivWidgets() {
|
||
document.querySelectorAll('.motiv-widget').forEach(function(widget) {
|
||
if (widget.dataset.initialized) return;
|
||
widget.dataset.initialized = 'true';
|
||
|
||
const API = widget.dataset.api;
|
||
const USER = widget.dataset.user;
|
||
const KEY = widget.dataset.key;
|
||
|
||
const modal = widget.querySelector('.motiv-modal-overlay');
|
||
const listEl = widget.querySelector('.motiv-list');
|
||
const input = widget.querySelector('.motiv-modal-input');
|
||
const addBtn = widget.querySelector('.motiv-modal-btn');
|
||
const closeBtn = widget.querySelector('.motiv-modal-close');
|
||
const gearBtn = widget.querySelector('.motiv-gear');
|
||
|
||
if (!modal || !listEl || !gearBtn) return;
|
||
|
||
let quotes = [];
|
||
|
||
function apiUrl(path) {
|
||
return API + path + (KEY ? '?key=' + KEY : '');
|
||
}
|
||
|
||
function esc(s) {
|
||
const d = document.createElement('div');
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
async function loadQuotes() {
|
||
listEl.innerHTML = '<div class="motiv-empty">Loading...</div>';
|
||
try {
|
||
const r = await fetch(API + '/userdata/' + USER + '/motivation');
|
||
const d = await r.json();
|
||
quotes = d.quotes || [];
|
||
renderQuotes();
|
||
} catch(e) {
|
||
listEl.innerHTML = '<div class="motiv-empty">Error loading</div>';
|
||
console.error('Motiv load error:', e);
|
||
}
|
||
}
|
||
|
||
function renderQuotes() {
|
||
if (!quotes.length) {
|
||
listEl.innerHTML = '<div class="motiv-empty">No quotes yet</div>';
|
||
return;
|
||
}
|
||
listEl.innerHTML = quotes.map(function(q, i) {
|
||
return '<div class="motiv-item" data-index="' + i + '">' +
|
||
'<span class="motiv-item-text">' + esc(q) + '</span>' +
|
||
'<button class="motiv-item-delete" type="button">🗑</button>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
gearBtn.addEventListener('click', function() {
|
||
modal.classList.add('active');
|
||
loadQuotes();
|
||
});
|
||
|
||
if (closeBtn) {
|
||
closeBtn.addEventListener('click', function() {
|
||
modal.classList.remove('active');
|
||
});
|
||
}
|
||
|
||
modal.addEventListener('click', function(e) {
|
||
if (e.target === modal) modal.classList.remove('active');
|
||
});
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||
modal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
async function addQuote() {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
input.disabled = true;
|
||
if (addBtn) addBtn.disabled = true;
|
||
try {
|
||
const r = await fetch(apiUrl('/userdata/' + USER + '/motivation'), {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({text: text})
|
||
});
|
||
if (r.ok) {
|
||
input.value = '';
|
||
const d = await r.json();
|
||
quotes = d.quotes || [];
|
||
renderQuotes();
|
||
}
|
||
} catch(e) { console.error('Motiv add error:', e); }
|
||
input.disabled = false;
|
||
if (addBtn) addBtn.disabled = false;
|
||
input.focus();
|
||
}
|
||
|
||
if (addBtn) addBtn.addEventListener('click', addQuote);
|
||
if (input) {
|
||
input.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); addQuote(); }
|
||
});
|
||
}
|
||
|
||
listEl.addEventListener('click', async function(e) {
|
||
const del = e.target.closest('.motiv-item-delete');
|
||
if (!del) return;
|
||
const item = del.closest('.motiv-item');
|
||
const idx = item.dataset.index;
|
||
try {
|
||
const r = await fetch(apiUrl('/userdata/' + USER + '/motivation/' + idx), {
|
||
method: 'DELETE'
|
||
});
|
||
if (r.ok) {
|
||
const d = await r.json();
|
||
quotes = d.quotes || [];
|
||
renderQuotes();
|
||
}
|
||
} catch(e) { console.error('Motiv delete error:', e); }
|
||
});
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initMotivWidgets);
|
||
} else {
|
||
initMotivWidgets();
|
||
}
|
||
// Also run after a delay for dynamically loaded content
|
||
setTimeout(initMotivWidgets, 500);
|
||
setTimeout(initMotivWidgets, 1500);
|
||
})();
|
||
</script>
|
||
|
||
branding:
|
||
logo-url: https://web.dooplex.hu/static/DooPlex_logo_3.png
|
||
favicon-url: https://web.dooplex.hu/static/DooPlex_favicon_3.png
|
||
app-name: "Kisfenyo's Home"
|
||
app-icon-url: https://web.dooplex.hu/static/DooPlex_favicon_3.png
|
||
app-background-color: "#132b66"
|
||
hide-footer: true
|
||
|
||
theme:
|
||
disable-picker: true
|
||
background-color: 210 35 12 # Was: 280 30 15 (purple → blue)
|
||
primary-color: 200 70 60 # Was: 280 60 70 (purple → cyan-blue)
|
||
positive-color: 150 55 50 # Was: 120 50 50 (green → teal-green, matches better)
|
||
negative-color: 0 70 60 # Unchanged (red)
|
||
contrast-multiplier: 1.2 # Unchanged
|
||
text-saturation-multiplier: 0.8 # Unchanged
|
||
custom-css-file: /assets/custom.css
|
||
|
||
pages:
|
||
# ==================== HOME PAGE ====================
|
||
- name: Home
|
||
slug: home
|
||
width: wide
|
||
columns:
|
||
# ---------- LEFT COLUMN ----------
|
||
- size: small
|
||
widgets:
|
||
# Glance Custom API Widget - System Stats from Prometheus
|
||
# Add this widget to your glance.yml configuration under a column's widgets section
|
||
#
|
||
# Prometheus URL: http://prometheus.mon-system.svc.cluster.local:9090
|
||
#
|
||
# This widget displays:
|
||
# - Hostname & Uptime
|
||
# - CPU usage % and Temperature
|
||
# - Memory usage %
|
||
# - Disk usage for all mount points with progress bars
|
||
# - Fan speeds
|
||
#
|
||
# Note: Make sure your Glance version is v0.8.0+ for all template functions
|
||
|
||
- type: custom-api
|
||
title: DooPlex Server
|
||
cache: 30s
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_uname_info
|
||
subrequests:
|
||
uptime_days:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: floor((time() - node_boot_time_seconds) / 86400)
|
||
uptime_hours:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: floor((time() - node_boot_time_seconds) % 86400 / 3600)
|
||
cpu:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
|
||
memory:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100
|
||
cpu_temp:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_hwmon_temp_celsius{instance="dooplex",chip="platform_coretemp_0",sensor="temp1"}
|
||
fans:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: fan_speed_rpm{instance="dooplex"}
|
||
disk_root:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})
|
||
disk_root_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) / 1073741824
|
||
disk_root_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/"} / 1073741824
|
||
disk_ssd2:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/mnt/ssd_2"} / node_filesystem_size_bytes{mountpoint="/mnt/ssd_2"})
|
||
disk_ssd2_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/mnt/ssd_2"} - node_filesystem_avail_bytes{mountpoint="/mnt/ssd_2"}) / 1073741824
|
||
disk_ssd2_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/mnt/ssd_2"} / 1073741824
|
||
disk_hdd1:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/mnt/1_hdd"} / node_filesystem_size_bytes{mountpoint="/mnt/1_hdd"})
|
||
disk_hdd1_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/mnt/1_hdd"} - node_filesystem_avail_bytes{mountpoint="/mnt/1_hdd"}) / 1099511627776
|
||
disk_hdd1_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/mnt/1_hdd"} / 1099511627776
|
||
disk_hdd2:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/mnt/2_hdd"} / node_filesystem_size_bytes{mountpoint="/mnt/2_hdd"})
|
||
disk_hdd2_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/mnt/2_hdd"} - node_filesystem_avail_bytes{mountpoint="/mnt/2_hdd"}) / 1099511627776
|
||
disk_hdd2_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/mnt/2_hdd"} / 1099511627776
|
||
disk_hdd3:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/mnt/3_hdd"} / node_filesystem_size_bytes{mountpoint="/mnt/3_hdd"})
|
||
disk_hdd3_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/mnt/3_hdd"} - node_filesystem_avail_bytes{mountpoint="/mnt/3_hdd"}) / 1099511627776
|
||
disk_hdd3_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/mnt/3_hdd"} / 1099511627776
|
||
disk_hdd4:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/mnt/4_hdd"} / node_filesystem_size_bytes{mountpoint="/mnt/4_hdd"})
|
||
disk_hdd4_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/mnt/4_hdd"} - node_filesystem_avail_bytes{mountpoint="/mnt/4_hdd"}) / 1099511627776
|
||
disk_hdd4_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/mnt/4_hdd"} / 1099511627776
|
||
disk_hdd5:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: 100 * (1 - node_filesystem_avail_bytes{mountpoint="/mnt/5_hdd"} / node_filesystem_size_bytes{mountpoint="/mnt/5_hdd"})
|
||
disk_hdd5_used:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: (node_filesystem_size_bytes{mountpoint="/mnt/5_hdd"} - node_filesystem_avail_bytes{mountpoint="/mnt/5_hdd"}) / 1099511627776
|
||
disk_hdd5_total:
|
||
url: ${PROMETHEUS_URL}/api/v1/query
|
||
parameters:
|
||
query: node_filesystem_size_bytes{mountpoint="/mnt/5_hdd"} / 1099511627776
|
||
template: |
|
||
<style>
|
||
.sys-stats { font-size: 0.9em; }
|
||
.top-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
|
||
.top-item { text-align: center; padding: 6px 8px; background: rgba(255,255,255,0.05); border-radius: 8px; }
|
||
.top-item.wide { grid-column: span 2; }
|
||
.top-label { font-size: 0.65em; opacity: 0.6; text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.top-value { font-size: 1.05em; font-weight: 600; margin-top: 2px; }
|
||
.section-title { font-size: 0.75em; opacity: 0.6; text-transform: uppercase; letter-spacing: 0.5px; margin: 10px 0 6px 0; }
|
||
.disk-row { display: flex; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
|
||
.disk-row:last-child { border-bottom: none; }
|
||
.disk-name { font-weight: 500; width: 42px; font-size: 0.95em; flex-shrink: 0; }
|
||
/* Set the track as a reference container */
|
||
.disk-bar {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: rgba(255,255,255,0.1);
|
||
border-radius: 4px;
|
||
margin: 0 10px;
|
||
overflow: hidden;
|
||
min-width: 0;
|
||
container-type: inline-size; /* <--- KEY CHANGE: Enables width measurement */
|
||
}
|
||
/* The fill acts as a "mask" or window */
|
||
.disk-fill {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
/* background: ... <--- REMOVED from here */
|
||
position: relative; /* Required for the pseudo-element positioning */
|
||
overflow: hidden; /* Clips the gradient to the current progress percentage */
|
||
box-shadow: 0 0 0 1px rgba(0,0,0,0.10) inset;
|
||
}
|
||
/* The gradient is applied here, locked to the full track width */
|
||
.disk-fill::before {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0; left: 0; bottom: 0;
|
||
width: 100cqw; /* <--- KEY CHANGE: 100% of the parent .disk-bar width */
|
||
background: linear-gradient(
|
||
90deg,
|
||
#60a5fa 0%, /* blue */
|
||
#60a5fa 50%, /* keep blue until 70% of the full scale */
|
||
#a78bfa 65%, /* purple transition */
|
||
#fb7185 80%, /* pink transition */
|
||
#ef4444 92% /* red at 100% */
|
||
);
|
||
z-index: -1;
|
||
}
|
||
.disk-info { font-size: 0.9em; opacity: 0.85; text-align: right; width: 125px; flex-shrink: 0; }
|
||
</style>
|
||
|
||
{{ $hostname := .JSON.String "data.result.0.metric.nodename" }}
|
||
{{ $uptimeDays := (.Subrequest "uptime_days").JSON.Float "data.result.0.value.1" }}
|
||
{{ $uptimeHours := (.Subrequest "uptime_hours").JSON.Float "data.result.0.value.1" }}
|
||
{{ $cpu := (.Subrequest "cpu").JSON.Float "data.result.0.value.1" }}
|
||
{{ $mem := (.Subrequest "memory").JSON.Float "data.result.0.value.1" }}
|
||
{{ $temp := (.Subrequest "cpu_temp").JSON.Float "data.result.0.value.1" }}
|
||
{{ $fans := (.Subrequest "fans").JSON.Array "data.result" }}
|
||
|
||
<div class="sys-stats">
|
||
<div class="top-grid">
|
||
<div class="top-item">
|
||
<div class="top-label">Host</div>
|
||
<div class="top-value">{{ $hostname }}</div>
|
||
</div>
|
||
<div class="top-item">
|
||
<div class="top-label">CPU</div>
|
||
<div class="top-value">{{ printf "%.1f" $cpu }}%</div>
|
||
</div>
|
||
<div class="top-item">
|
||
<div class="top-label">Uptime</div>
|
||
<div class="top-value">{{ printf "%.0f" $uptimeDays }}d {{ printf "%.0f" $uptimeHours }}h</div>
|
||
</div>
|
||
<div class="top-item">
|
||
<div class="top-label">Memory</div>
|
||
<div class="top-value">{{ printf "%.1f" $mem }}%</div>
|
||
</div>
|
||
<div class="top-item">
|
||
<div class="top-label">Temp</div>
|
||
<div class="top-value">{{ printf "%.0f" $temp }}°C</div>
|
||
</div>
|
||
<div class="top-item">
|
||
<div class="top-label">Fans (RPM)</div>
|
||
<div class="top-value">{{ range $i, $fan := $fans }}{{ if $i }}/{{ end }}{{ $fan.Int "value.1" }}{{ end }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section-title">Storage</div>
|
||
|
||
{{ $rootPct := (.Subrequest "disk_root").JSON.Float "data.result.0.value.1" }}
|
||
{{ $rootUsed := (.Subrequest "disk_root_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $rootTotal := (.Subrequest "disk_root_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">Root</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $rootPct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.0f" $rootUsed }} / {{ printf "%.0f" $rootTotal }} GB ({{ printf "%.0f" $rootPct }}%)</span>
|
||
</div>
|
||
|
||
{{ $ssd2Pct := (.Subrequest "disk_ssd2").JSON.Float "data.result.0.value.1" }}
|
||
{{ $ssd2Used := (.Subrequest "disk_ssd2_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $ssd2Total := (.Subrequest "disk_ssd2_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">SSD2</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $ssd2Pct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.0f" $ssd2Used }} / {{ printf "%.0f" $ssd2Total }} GB ({{ printf "%.0f" $ssd2Pct }}%)</span>
|
||
</div>
|
||
|
||
{{ $hdd1Pct := (.Subrequest "disk_hdd1").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd1Used := (.Subrequest "disk_hdd1_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd1Total := (.Subrequest "disk_hdd1_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">HDD1</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $hdd1Pct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.1f" $hdd1Used }} / {{ printf "%.1f" $hdd1Total }} TB ({{ printf "%.0f" $hdd1Pct }}%)</span>
|
||
</div>
|
||
|
||
{{ $hdd2Pct := (.Subrequest "disk_hdd2").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd2Used := (.Subrequest "disk_hdd2_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd2Total := (.Subrequest "disk_hdd2_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">HDD2</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $hdd2Pct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.1f" $hdd2Used }} / {{ printf "%.1f" $hdd2Total }} TB ({{ printf "%.0f" $hdd2Pct }}%)</span>
|
||
</div>
|
||
|
||
{{ $hdd3Pct := (.Subrequest "disk_hdd3").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd3Used := (.Subrequest "disk_hdd3_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd3Total := (.Subrequest "disk_hdd3_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">HDD3</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $hdd3Pct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.1f" $hdd3Used }} / {{ printf "%.1f" $hdd3Total }} TB ({{ printf "%.0f" $hdd3Pct }}%)</span>
|
||
</div>
|
||
|
||
{{ $hdd4Pct := (.Subrequest "disk_hdd4").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd4Used := (.Subrequest "disk_hdd4_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd4Total := (.Subrequest "disk_hdd4_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">HDD4</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $hdd4Pct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.1f" $hdd4Used }} / {{ printf "%.1f" $hdd4Total }} TB ({{ printf "%.0f" $hdd4Pct }}%)</span>
|
||
</div>
|
||
|
||
{{ $hdd5Pct := (.Subrequest "disk_hdd5").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd5Used := (.Subrequest "disk_hdd5_used").JSON.Float "data.result.0.value.1" }}
|
||
{{ $hdd5Total := (.Subrequest "disk_hdd5_total").JSON.Float "data.result.0.value.1" }}
|
||
<div class="disk-row">
|
||
<span class="disk-name">HDD5</span>
|
||
<div class="disk-bar">
|
||
<div class="disk-fill" style="width: {{ printf "%.0f" $hdd5Pct }}%"></div>
|
||
</div>
|
||
<span class="disk-info">{{ printf "%.1f" $hdd5Used }} / {{ printf "%.1f" $hdd5Total }} TB ({{ printf "%.0f" $hdd5Pct }}%)</span>
|
||
</div>
|
||
</div>
|
||
|
||
- type: bookmarks
|
||
title: Admin Tools
|
||
groups:
|
||
- links:
|
||
- title: Code-Server
|
||
url: https://code.dooplex.hu
|
||
icon: si:coder
|
||
- title: ArgoCD
|
||
url: https://argocd.dooplex.hu
|
||
icon: si:argo
|
||
- title: Gitea
|
||
url: https://gitea.dooplex.hu
|
||
icon: si:gitea
|
||
- title: Headlamp
|
||
url: https://headlamp.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/headlamp.png
|
||
- title: Authentik
|
||
url: https://authentik.dooplex.hu
|
||
icon: si:authentik
|
||
- title: Termix
|
||
url: https://termix.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/termix.png
|
||
- title: Longhorn (LAN Only)
|
||
url: http://192.168.0.209/#/dashboard
|
||
icon: https://web.dooplex.hu/static/white-icons/longhorn.png
|
||
- title: Pi-hole
|
||
url: https://pihole.dooplex.hu/admin
|
||
icon: si:pihole
|
||
|
||
- type: bookmarks
|
||
title: Useful Bookmarks
|
||
groups:
|
||
- title: ""
|
||
links:
|
||
- title: GMail
|
||
url: https://mail.google.com
|
||
icon: si:gmail
|
||
- title: Kréta
|
||
url: https://klik100566001.e-kreta.hu/Intezmeny/Faliujsag
|
||
icon: https://web.dooplex.hu/static/white-icons/kreta.png
|
||
- title: "444"
|
||
url: https://444.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/444.png
|
||
- title: Telex
|
||
url: https://telex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/telex.png
|
||
- title: Hardverapró
|
||
url: https://hardverapro.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/hardverapro.png
|
||
|
||
# ---------- CENTER COLUMN ----------
|
||
- size: full
|
||
widgets:
|
||
- type: split-column
|
||
max-columns: 3
|
||
widgets:
|
||
# Weather Widget (Időkép)
|
||
- type: custom-api
|
||
title: "Időkép – Budapest VII."
|
||
url: "http://idokep-scraper.glance-system.svc.cluster.local:8000/api?v=2"
|
||
cache: 30s
|
||
template: |
|
||
{{ $loc := .JSON.String "location.name" }}
|
||
{{ if eq $loc "" }}{{ $loc = .JSON.String "place" }}{{ end }}
|
||
{{ $curTemp := .JSON.Float "current.temp_c" }}
|
||
{{ $curIcon := .JSON.String "current.icon_url" }}
|
||
{{ $daily := .JSON.Array "daily" }}
|
||
{{ $hourly := .JSON.Array "hourly" }}
|
||
|
||
<div class="idokep">
|
||
<div class="idokep-top">
|
||
<div class="idokep-top-left">
|
||
{{ if $curIcon }}<img class="idokep-icon" src="{{ $curIcon }}" alt="" />{{ end }}
|
||
<div class="idokep-temp">{{ printf "%.0f" $curTemp }}°C</div>
|
||
</div>
|
||
<div class="idokep-top-right">
|
||
<div class="idokep-loc">{{ $loc }}</div>
|
||
<div class="idokep-src">Forrás: <a href="{{ .JSON.String "source.url" }}" target="_blank">Időkép</a></div>
|
||
</div>
|
||
</div>
|
||
|
||
{{ if gt (len $hourly) 0 }}
|
||
<div class="idokep-hourly">
|
||
{{ range $i, $h := $hourly }}
|
||
<div class="idokep-hour">
|
||
<div class="idokep-hour-time">{{ $h.String "time" }}</div>
|
||
{{ if $h.String "icon_url" }}<img class="idokep-hour-icon" src="{{ $h.String "icon_url" }}" title="{{ $h.String "condition" }}" alt="{{ $h.String "condition" }}" />{{ end }}
|
||
<div class="idokep-hour-temp">{{ printf "%.0f" ($h.Float "temp_c") }}°</div>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ end }}
|
||
|
||
{{ if gt (len $daily) 0 }}
|
||
<div class="idokep-daily">
|
||
{{ range $daily }}
|
||
<div class="idokep-row">
|
||
<div class="idokep-dow">
|
||
<span class="idokep-downame">{{ .String "dow" }}</span>
|
||
<span class="idokep-daynum">{{ .String "daynum" }}</span>
|
||
</div>
|
||
<div class="idokep-dayicon">
|
||
{{ if .String "icon_url" }}<img src="{{ .String "icon_url" }}" title="{{ .String "condition" }}" alt="{{ .String "condition" }}" />{{ end }}
|
||
</div>
|
||
<div class="idokep-min">{{ printf "%.0f" (.Float "tmin_c") }}°</div>
|
||
|
||
<div class="idokep-bar">
|
||
<div class="idokep-bar-track"></div>
|
||
|
||
{{/* We construct the style manually to bypass Go security sanitization */}}
|
||
<div class="idokep-bar-fill" style="
|
||
--l: {{ printf "%.1f" (.Float "c_l") }}%;
|
||
--w: {{ printf "%.1f" (.Float "c_w") }}%;
|
||
--gw: {{ printf "%.1f" (.Float "c_gw") }}%;
|
||
--ml: {{ printf "%.1f" (.Float "c_ml") }}%;
|
||
--s-wht: {{ printf "%.1f" (.Float "c_s1") }}%;
|
||
--s-blu: {{ printf "%.1f" (.Float "c_s2") }}%;
|
||
--s-pur: {{ printf "%.1f" (.Float "c_s3") }}%;
|
||
--s-pnk: {{ printf "%.1f" (.Float "c_s4") }}%;
|
||
--s-red: {{ printf "%.1f" (.Float "c_s5") }}%;
|
||
">
|
||
<div class="idokep-bar-gradient"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="idokep-max">{{ printf "%.0f" (.Float "tmax_c") }}°</div>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
# Calendar Widget
|
||
- type: calendar
|
||
first-day-of-week: monday
|
||
# To-Do List Widget (custom-api with persistent storage)
|
||
- type: custom-api
|
||
title: To-Do
|
||
cache: 30s
|
||
url: http://glance-helper.glance-system.svc.cluster.local:8000/userdata/kisfenyo/todos
|
||
options:
|
||
api_base: https://glance-helper.dooplex.hu
|
||
user: kisfenyo
|
||
api_key: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT
|
||
template: |
|
||
{{ $apiBase := .Options.StringOr "api_base" "https://glance-helper.dooplex.hu" }}
|
||
{{ $user := .Options.StringOr "user" "kisfenyo" }}
|
||
{{ $apiKey := .Options.StringOr "api_key" "" }}
|
||
{{ $todos := .JSON.Array "todos" }}
|
||
|
||
<style>
|
||
.todo-widget { display: flex; flex-direction: column; gap: 8px; }
|
||
.todo-add { display: flex; gap: 8px; align-items: center; }
|
||
.todo-add-btn {
|
||
background: none; border: none; color: inherit; opacity: 0.6;
|
||
cursor: pointer; font-size: 18px; padding: 4px 8px;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.todo-add-btn:hover { opacity: 1; }
|
||
.todo-add-input {
|
||
flex: 1; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 6px; padding: 8px 10px; color: inherit; font-size: 13px;
|
||
outline: none; transition: border-color 0.15s, background 0.15s;
|
||
}
|
||
.todo-add-input:focus {
|
||
background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.25);
|
||
}
|
||
.todo-add-input::placeholder { opacity: 0.5; }
|
||
.todo-list { display: flex; flex-direction: column; gap: 2px; }
|
||
.todo-item {
|
||
display: flex; align-items: center; gap: 8px; padding: 8px 10px;
|
||
border-radius: 8px; background: rgba(255,255,255,0.04);
|
||
transition: background 0.15s;
|
||
}
|
||
.todo-item:hover { background: rgba(255,255,255,0.08); }
|
||
.todo-checkbox {
|
||
width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.4);
|
||
border-radius: 4px; cursor: pointer; display: flex; align-items: center;
|
||
justify-content: center; flex-shrink: 0; transition: all 0.15s;
|
||
background: transparent;
|
||
}
|
||
.todo-checkbox:hover { border-color: rgba(255,255,255,0.7); }
|
||
.todo-checkbox.done {
|
||
background: rgba(34, 197, 94, 0.8); border-color: rgba(34, 197, 94, 0.8);
|
||
}
|
||
.todo-checkbox.done::after { content: "✓"; font-size: 11px; color: #fff; }
|
||
.todo-text { flex: 1; font-size: 13px; opacity: 0.95; line-height: 1.3; }
|
||
.todo-text.done { text-decoration: line-through; opacity: 0.5; }
|
||
.todo-delete {
|
||
opacity: 0; background: none; border: none; color: inherit;
|
||
cursor: pointer; padding: 4px 6px; font-size: 14px;
|
||
transition: opacity 0.15s, color 0.15s;
|
||
}
|
||
.todo-item:hover .todo-delete { opacity: 0.5; }
|
||
.todo-delete:hover { opacity: 1 !important; color: #ef4444; }
|
||
.todo-empty { opacity: 0.5; font-size: 13px; padding: 8px 0; text-align: center; }
|
||
</style>
|
||
|
||
<div class="todo-widget" data-api="{{ $apiBase }}" data-user="{{ $user }}" data-key="{{ $apiKey }}">
|
||
<div class="todo-add">
|
||
<button class="todo-add-btn" type="button">+</button>
|
||
<input type="text" class="todo-add-input" placeholder="Add a task">
|
||
</div>
|
||
<div class="todo-list">
|
||
{{ if eq (len $todos) 0 }}
|
||
<div class="todo-empty">No tasks yet</div>
|
||
{{ else }}
|
||
{{ range $todos }}
|
||
<div class="todo-item" data-id="{{ .String "id" }}" data-text="{{ .String "text" }}" data-done="{{ .Bool "done" }}">
|
||
<div class="todo-checkbox {{ if .Bool "done" }}done{{ end }}"></div>
|
||
<span class="todo-text {{ if .Bool "done" }}done{{ end }}">{{ .String "text" }}</span>
|
||
<button class="todo-delete" type="button" title="Delete">🗑</button>
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
# Outline Notes iframe
|
||
- type: iframe
|
||
source: https://outline.dooplex.hu/collection/dooplex-server-iTAZn04AaR/recent
|
||
height: 900
|
||
title: Documentation
|
||
|
||
# ---------- RIGHT COLUMN ----------
|
||
- size: small
|
||
widgets:
|
||
- type: custom-api
|
||
title: Meal for the Day
|
||
cache: 5m
|
||
|
||
url: http://glance-helper.glance-system.svc.cluster.local:8000/tandoor/daily
|
||
parameters:
|
||
count: 3
|
||
cooldown: 14
|
||
|
||
options:
|
||
tandoor_url: https://tandoor.dooplex.hu
|
||
|
||
template: |
|
||
{{ $tandoor := .Options.StringOr "tandoor_url" "https://tandoor.dooplex.hu" }}
|
||
{{ $items := .JSON.Array "items" }}
|
||
{{ $count := len $items }}
|
||
{{ $last := sub $count 1 }}
|
||
|
||
<style>
|
||
.mw { display:flex; flex-direction:column; gap:6px; }
|
||
.mw-meta { opacity:.65; font-size:12px; display:flex; justify-content:space-between; align-items:center; }
|
||
.mw-box { position:relative; border-radius:14px; background:rgba(255,255,255,0.04); box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset; overflow:hidden; width:100%; }
|
||
.mw-box input { display:none; }
|
||
.mw-track { display:flex; transition:transform 0.3s ease; }
|
||
.mw-s { min-width:100%; width:100%; flex-shrink:0; box-sizing:border-box; }
|
||
.mw-img { height:150px; background:rgba(0,0,0,0.15); overflow:hidden; }
|
||
.mw-img img { width:100%; height:100%; object-fit:cover; display:block; }
|
||
.mw-noimg { height:150px; display:flex; align-items:center; justify-content:center; opacity:.5; font-size:12px; }
|
||
.mw-name { padding:10px 12px 4px; font-weight:700; opacity:.95; line-height:1.3; overflow-wrap:break-word; word-break:break-word; }
|
||
.mw-stats { padding:0 12px 6px; font-size:11px; opacity:.5; }
|
||
.mw-acts { padding:0 12px 10px; display:flex; gap:10px; opacity:.8; font-size:12px; }
|
||
.mw-acts a, .mw-link { text-decoration:none; color:inherit; display:block; }
|
||
.mw-p, .mw-n { position:absolute; top:75px; transform:translateY(-50%); width:26px; height:26px; border-radius:50%; background:rgba(0,0,0,0.6); color:#fff; align-items:center; justify-content:center; font-size:11px; cursor:pointer; z-index:5; display:none; }
|
||
.mw-p { left:6px; }
|
||
.mw-n { right:6px; }
|
||
.mw-p:hover, .mw-n:hover { background:rgba(0,0,0,0.85); }
|
||
.mw-dots { display:flex; justify-content:center; gap:5px; padding:3px 0 1px; }
|
||
.mw-dot { width:6px; height:6px; border-radius:50%; background:rgba(255,255,255,0.2); cursor:pointer; }
|
||
.mw-dot:hover { background:rgba(255,255,255,0.4); }
|
||
{{ range $i, $_ := $items }}
|
||
#mr{{ $i }}:checked ~ .mw-track { transform:translateX(-{{ mul $i 100 }}%); }
|
||
#mr{{ $i }}:checked ~ .mw-dots .mw-dot:nth-child({{ add $i 1 }}) { background:rgba(255,255,255,0.85); }
|
||
{{ if gt $i 0 }}.mw-box:hover #mr{{ $i }}:checked ~ .mw-p[data-t="{{ sub $i 1 }}"] { display:flex; }{{ end }}
|
||
{{ if lt $i $last }}.mw-box:hover #mr{{ $i }}:checked ~ .mw-n[data-t="{{ add $i 1 }}"] { display:flex; }{{ end }}
|
||
{{ end }}
|
||
</style>
|
||
|
||
<div class="mw">
|
||
<div class="mw-meta">
|
||
<span>Today's picks ({{ $count }} total)</span>
|
||
<a href="{{ $tandoor }}" target="_blank" rel="noreferrer">Open Tandoor</a>
|
||
</div>
|
||
{{ if lt $count 1 }}<div class="color-negative">No recipes.</div>{{ else }}
|
||
<div class="mw-box">
|
||
{{ range $i, $_ := $items }}<input type="radio" name="mr" id="mr{{ $i }}"{{ if eq $i 0 }} checked{{ end }}>{{ end }}
|
||
<div class="mw-track">
|
||
{{ range $r := $items }}{{ $img := $r.String "image" }}{{ $url := $r.String "url" }}{{ $cook := $r.String "cook_url" }}{{ $cooked := $r.Int "cooked_count" }}
|
||
<div class="mw-s">
|
||
<a class="mw-link" href="{{ $url }}" target="_blank">
|
||
<div class="mw-img">{{ if $img }}<img src="{{ $img }}" alt="">{{ else }}<div class="mw-noimg">No image</div>{{ end }}</div>
|
||
<div class="mw-name">{{ $r.String "name" }}</div>
|
||
</a>
|
||
{{ if gt $cooked 0 }}<div class="mw-stats">Cooked {{ $cooked }}× before</div>{{ end }}
|
||
<div class="mw-acts"><a href="{{ $url }}" target="_blank">Open</a> <a href="{{ $cook }}" target="_blank">Cooked today ✔</a></div>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ if gt $count 1 }}
|
||
{{ range $i, $_ := $items }}{{ if gt $i 0 }}<label class="mw-p" for="mr{{ sub $i 1 }}" data-t="{{ sub $i 1 }}">◀</label>{{ end }}{{ if lt $i $last }}<label class="mw-n" for="mr{{ add $i 1 }}" data-t="{{ add $i 1 }}">▶</label>{{ end }}{{ end }}
|
||
<div class="mw-dots">{{ range $i, $_ := $items }}<label class="mw-dot" for="mr{{ $i }}"></label>{{ end }}</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
# Motivation Quote Widget
|
||
- type: custom-api
|
||
title: Motivation
|
||
cache: 1m
|
||
url: http://glance-helper.glance-system.svc.cluster.local:8000/userdata/kisfenyo/motivation/random
|
||
options:
|
||
api_base: https://glance-helper.dooplex.hu
|
||
user: kisfenyo
|
||
api_key: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT
|
||
template: |
|
||
{{ $apiBase := .Options.StringOr "api_base" "https://glance-helper.dooplex.hu" }}
|
||
{{ $user := .Options.StringOr "user" "kisfenyo" }}
|
||
{{ $apiKey := .Options.StringOr "api_key" "" }}
|
||
{{ $quote := .JSON.String "quote" }}
|
||
{{ $total := .JSON.Int "total" }}
|
||
|
||
<style>
|
||
.motiv-widget { position: relative; }
|
||
.motiv-quote {
|
||
font-size: 14px; line-height: 1.5; opacity: 0.9;
|
||
padding: 12px 14px; background: rgba(255,255,255,0.04);
|
||
border-radius: 10px; border-left: 3px solid rgba(96, 165, 250, 0.6);
|
||
font-style: italic;
|
||
}
|
||
.motiv-meta {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
margin-top: 8px; font-size: 11px; opacity: 0.5;
|
||
}
|
||
.motiv-gear {
|
||
cursor: pointer; padding: 2px 6px; border-radius: 4px;
|
||
transition: opacity 0.15s, background 0.15s; border: none; background: none;
|
||
color: inherit; font-size: 11px;
|
||
}
|
||
.motiv-gear:hover { opacity: 1; background: rgba(255,255,255,0.1); }
|
||
|
||
.motiv-modal-overlay {
|
||
display: none; position: fixed; inset: 0;
|
||
background: rgba(0,0,0,0.6); z-index: 99999;
|
||
align-items: flex-start; justify-content: center;
|
||
padding-top: 8vh; backdrop-filter: blur(4px);
|
||
}
|
||
.motiv-modal-overlay.active { display: flex; }
|
||
.motiv-modal {
|
||
width: min(500px, 90vw); max-height: 80vh;
|
||
background: rgba(20, 25, 35, 0.98); border-radius: 14px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.4);
|
||
overflow: hidden; display: flex; flex-direction: column;
|
||
}
|
||
.motiv-modal-header {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,0.1);
|
||
}
|
||
.motiv-modal-title { font-weight: 600; font-size: 15px; }
|
||
.motiv-modal-close {
|
||
background: none; border: none; color: inherit; cursor: pointer;
|
||
font-size: 18px; opacity: 0.6; padding: 4px 8px;
|
||
}
|
||
.motiv-modal-close:hover { opacity: 1; }
|
||
.motiv-modal-body {
|
||
flex: 1; overflow-y: auto; padding: 12px 16px;
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
}
|
||
.motiv-modal-add { display: flex; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,0.08); }
|
||
.motiv-modal-input {
|
||
flex: 1; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1);
|
||
border-radius: 6px; padding: 10px 12px; color: inherit; font-size: 13px; outline: none;
|
||
}
|
||
.motiv-modal-input:focus { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.25); }
|
||
.motiv-modal-btn {
|
||
background: rgba(96, 165, 250, 0.3); border: none; color: inherit; padding: 10px 16px;
|
||
border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500;
|
||
}
|
||
.motiv-modal-btn:hover { background: rgba(96, 165, 250, 0.5); }
|
||
.motiv-list { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; }
|
||
.motiv-list-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.5; margin-bottom: 4px; }
|
||
.motiv-item {
|
||
display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px;
|
||
background: rgba(255,255,255,0.04); border-radius: 8px;
|
||
}
|
||
.motiv-item:hover { background: rgba(255,255,255,0.08); }
|
||
.motiv-item-text { flex: 1; font-size: 13px; line-height: 1.4; opacity: 0.9; }
|
||
.motiv-item-delete {
|
||
background: none; border: none; color: inherit; cursor: pointer;
|
||
opacity: 0.4; padding: 2px 6px; font-size: 13px;
|
||
}
|
||
.motiv-item-delete:hover { opacity: 1; color: #ef4444; }
|
||
.motiv-empty { text-align: center; opacity: 0.5; padding: 20px; font-size: 13px; }
|
||
</style>
|
||
|
||
<div class="motiv-widget" data-api="{{ $apiBase }}" data-user="{{ $user }}" data-key="{{ $apiKey }}">
|
||
<div class="motiv-quote">{{ $quote }}</div>
|
||
<div class="motiv-meta">
|
||
<span>{{ $total }} quotes</span>
|
||
<button class="motiv-gear" type="button" title="Manage quotes">⚙️ Manage</button>
|
||
</div>
|
||
<div class="motiv-modal-overlay">
|
||
<div class="motiv-modal">
|
||
<div class="motiv-modal-header">
|
||
<span class="motiv-modal-title">Manage Motivation Quotes</span>
|
||
<button class="motiv-modal-close" type="button">×</button>
|
||
</div>
|
||
<div class="motiv-modal-body">
|
||
<div class="motiv-modal-add">
|
||
<input type="text" class="motiv-modal-input" placeholder="Add a new motivational quote...">
|
||
<button class="motiv-modal-btn" type="button">Add</button>
|
||
</div>
|
||
<div class="motiv-list-title">Your Quotes</div>
|
||
<div class="motiv-list"><div class="motiv-empty">Click Manage to load quotes</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
# Calendar Events Widget (Családi only)
|
||
- type: custom-api
|
||
title: Family Calendar
|
||
cache: 5m
|
||
url: http://glance-helper.glance-system.svc.cluster.local:8000/calendar/events
|
||
parameters:
|
||
count: 10
|
||
days: 14
|
||
calendars: Családi
|
||
template: |
|
||
{{ $events := .JSON.Array "events" }}
|
||
{{ $count := len $events }}
|
||
{{ $showCount := 5 }}
|
||
{{ $hasMore := gt $count $showCount }}
|
||
|
||
<style>
|
||
.cal-widget { display: flex; flex-direction: column; gap: 6px; }
|
||
.cal-meta { opacity: .65; font-size: 12px; display: flex; justify-content: space-between; align-items: center; }
|
||
.cal-empty { opacity: 0.5; font-size: 13px; padding: 8px 0; }
|
||
.cal-list { display: flex; flex-direction: column; gap: 4px; }
|
||
.cal-item {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
gap: 8px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: rgba(255,255,255,0.04);
|
||
align-items: start;
|
||
}
|
||
.cal-item:hover { background: rgba(255,255,255,0.08); }
|
||
.cal-title {
|
||
font-weight: 600;
|
||
opacity: 0.95;
|
||
font-size: 13px;
|
||
line-height: 1.3;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.cal-time {
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
line-height: 1.4;
|
||
}
|
||
.cal-date { font-weight: 600; }
|
||
.cal-hour { opacity: 0.8; }
|
||
.cal-allday {
|
||
font-size: 10px;
|
||
opacity: 0.6;
|
||
text-transform: uppercase;
|
||
}
|
||
/* Collapse/expand styling */
|
||
.cal-hidden { display: none; }
|
||
.cal-toggle {
|
||
font-size: 12px;
|
||
opacity: 0.6;
|
||
cursor: pointer;
|
||
text-align: center;
|
||
padding: 6px;
|
||
border-radius: 6px;
|
||
margin-top: 4px;
|
||
}
|
||
.cal-toggle:hover { opacity: 0.9; background: rgba(255,255,255,0.05); }
|
||
#cal-expand-fam:checked ~ .cal-list .cal-extra { display: grid; }
|
||
#cal-expand-fam:checked ~ .cal-toggle-more { display: none; }
|
||
#cal-expand-fam:not(:checked) ~ .cal-toggle-less { display: none; }
|
||
</style>
|
||
|
||
<div class="cal-widget">
|
||
{{ if lt $count 1 }}
|
||
<div class="cal-empty">No events in near future</div>
|
||
{{ else }}
|
||
{{ if $hasMore }}<input type="checkbox" id="cal-expand-fam" style="display:none;">{{ end }}
|
||
<div class="cal-list">
|
||
{{ range $i, $e := $events }}
|
||
{{ $isExtra := ge $i $showCount }}
|
||
{{ $isAllDay := $e.Bool "is_all_day" }}
|
||
{{ $title := $e.String "title" }}
|
||
{{ $startDate := $e.String "start_date_relative" }}
|
||
{{ $startTime := $e.String "start_time" }}
|
||
|
||
<div class="cal-item{{ if $isExtra }} cal-extra cal-hidden{{ end }}">
|
||
<div>
|
||
<div class="cal-title">{{ $title }}</div>
|
||
</div>
|
||
<div class="cal-time">
|
||
<div class="cal-date">{{ $startDate }}</div>
|
||
{{ if $isAllDay }}
|
||
<div class="cal-allday">Whole day</div>
|
||
{{ else }}
|
||
<div class="cal-hour">{{ $startTime }}</div>
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ if $hasMore }}
|
||
<label class="cal-toggle cal-toggle-more" for="cal-expand-fam">Show more ({{ sub $count $showCount }} additional) ▼</label>
|
||
<label class="cal-toggle cal-toggle-less" for="cal-expand-fam">Show less ▲</label>
|
||
{{ end }}
|
||
{{ end }}
|
||
</div>
|
||
|
||
- type: bookmarks
|
||
title: Productivity Self-Hosted
|
||
groups:
|
||
- title: ""
|
||
links:
|
||
- title: Nextcloud
|
||
url: https://nextcloud.dooplex.hu
|
||
icon: si:nextcloud
|
||
- title: Outline
|
||
url: https://outline.dooplex.hu
|
||
icon: si:outline
|
||
- title: Paperless
|
||
url: https://paperless.dooplex.hu
|
||
icon: si:paperlessngx
|
||
- title: Vaultwarden
|
||
url: https://vaultwarden.dooplex.hu
|
||
icon: si:bitwarden
|
||
- title: Actual Budget
|
||
url: https://actualbudget.dooplex.hu
|
||
icon: si:actualbudget
|
||
- title: Tandoor
|
||
url: https://tandoor.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/tandoor.png
|
||
- title: Bookstack
|
||
url: https://bookstack.dooplex.hu
|
||
icon: si:bookstack
|
||
- type: bookmarks
|
||
title: Other Self-Hosted
|
||
groups:
|
||
- links:
|
||
- title: AdventureLog
|
||
url: https://adventures.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/adventurelog.png
|
||
- title: Wanderer
|
||
url: https://wanderer.dooplex.hu
|
||
icon: sh:wanderer
|
||
- title: Plant-it
|
||
url: https://plantit.dooplex.hu
|
||
icon: si:leaflet
|
||
- title: Workout (wger)
|
||
url: https://workout.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/wger.png
|
||
- title: Fileshare
|
||
url: https://fileshare.dooplex.hu
|
||
icon: si:files
|
||
- title: Privatebin
|
||
url: https://privatebin.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/privatebin.png
|
||
- title: Pastes (OpenGist)
|
||
url: https://paste.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/opengist.png
|
||
- title: Zipline
|
||
url: https://zipline.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/zipline.png
|
||
|
||
# ==================== MONITORING PAGE ====================
|
||
- name: Monitoring
|
||
slug: monitoring
|
||
width: wide
|
||
columns:
|
||
- size: small
|
||
widgets:
|
||
- type: bookmarks
|
||
title: Monitoring
|
||
groups:
|
||
- links:
|
||
- title: Grafana
|
||
url: https://grafana.dooplex.hu/d/adxgb7x/overview-dashboard?orgId=1&from=now-3h&to=now&timezone=browser
|
||
icon: si:grafana
|
||
- title: Uptime Kuma
|
||
url: https://uptimekuma.dooplex.hu
|
||
icon: si:uptimekuma
|
||
- title: Prometheus (LAN Only)
|
||
url: http://prometheus.home/alerts
|
||
icon: si:prometheus
|
||
|
||
# Glance Widget: Container Version Checker (Simplified)
|
||
#
|
||
# This is a more robust version that uses only Glance's documented template functions.
|
||
# Add this widget to your glance.yml under a column's widgets section.
|
||
# Place it right after the "DooPlex Server" widget.
|
||
#
|
||
# Prerequisites:
|
||
# - Deploy version-checker from version-checker.yaml
|
||
# - Wait ~5 minutes for initial version checks to complete
|
||
|
||
- type: custom-api
|
||
title: Container Versions
|
||
cache: 5m
|
||
url: http://glance-helper.glance-system.svc.cluster.local:8000/versions
|
||
template: |
|
||
<style>
|
||
.ver-widget { font-size: 0.9em; }
|
||
.ver-summary {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
padding: 8px;
|
||
background: rgba(255,255,255,0.03);
|
||
border-radius: 8px;
|
||
}
|
||
.ver-stat {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 4px;
|
||
}
|
||
.ver-label {
|
||
font-size: 0.7em;
|
||
opacity: 0.6;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.ver-value {
|
||
font-size: 1.4em;
|
||
font-weight: 600;
|
||
margin-top: 2px;
|
||
}
|
||
.ver-ok { color: #4ade80; }
|
||
.ver-warn { color: #fbbf24; }
|
||
.ver-info { color: #5ac8d8; }
|
||
.ver-list {
|
||
max-height: 220px;
|
||
overflow-y: auto;
|
||
}
|
||
.ver-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr auto auto auto;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 5px 0;
|
||
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||
font-size: 0.82em;
|
||
}
|
||
.ver-row:last-child { border-bottom: none; }
|
||
.ver-img {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
opacity: 0.9;
|
||
}
|
||
.ver-cur {
|
||
color: #f87171;
|
||
font-family: monospace;
|
||
font-size: 0.95em;
|
||
}
|
||
.ver-arr {
|
||
color: rgba(255,255,255,0.3);
|
||
padding: 0 2px;
|
||
}
|
||
.ver-lat {
|
||
color: #4ade80;
|
||
font-family: monospace;
|
||
font-size: 0.95em;
|
||
}
|
||
.ver-allok {
|
||
text-align: center;
|
||
padding: 16px;
|
||
color: #4ade80;
|
||
opacity: 0.85;
|
||
}
|
||
.ver-nodata {
|
||
text-align: center;
|
||
padding: 16px;
|
||
opacity: 0.5;
|
||
font-size: 0.9em;
|
||
}
|
||
</style>
|
||
|
||
{{ $upToDate := .JSON.Float "summary.up_to_date" }}
|
||
{{ $outdated := .JSON.Float "summary.outdated" }}
|
||
{{ $total := .JSON.Float "summary.total" }}
|
||
{{ $updates := .JSON.Array "outdated" }}
|
||
|
||
<div class="ver-widget">
|
||
{{ if gt $total 0.0 }}
|
||
<div class="ver-summary">
|
||
<div class="ver-stat">
|
||
<div class="ver-label">Current</div>
|
||
<div class="ver-value ver-ok">{{ printf "%.0f" $upToDate }}</div>
|
||
</div>
|
||
<div class="ver-stat">
|
||
<div class="ver-label">Updates</div>
|
||
<div class="ver-value ver-warn">{{ printf "%.0f" $outdated }}</div>
|
||
</div>
|
||
<div class="ver-stat">
|
||
<div class="ver-label">Total</div>
|
||
<div class="ver-value ver-info">{{ printf "%.0f" $total }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{ if gt $outdated 0.0 }}
|
||
<div class="ver-list">
|
||
{{ range $updates }}
|
||
<div class="ver-row" title="{{ .String "image" }}">
|
||
<span class="ver-img">{{ trimPrefix "ghcr.io/" (trimPrefix "docker.io/" (trimPrefix "lscr.io/" (trimPrefix "quay.io/" (.String "image")))) }}</span>
|
||
<span class="ver-cur">{{ .String "current_version" }}</span>
|
||
<span class="ver-arr">→</span>
|
||
<span class="ver-lat">{{ .String "latest_version" }}</span>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ else }}
|
||
<div class="ver-allok">✓ All images up to date!</div>
|
||
{{ end }}
|
||
{{ else }}
|
||
<div class="ver-nodata">Waiting for version data...<br><small>Check glance-helper logs</small></div>
|
||
{{ end }}
|
||
</div>
|
||
|
||
- size: full
|
||
widgets:
|
||
# Grafana
|
||
- type: iframe
|
||
source: https://grafana.dooplex.hu/d/nazwb7z/overview-dashboard-for-glance?kiosk
|
||
height: 1140
|
||
title: Grafana Overview Dashboard
|
||
|
||
- size: small
|
||
widgets:
|
||
- type: custom-api
|
||
title: Uptime Kuma
|
||
title-url: ${UPTIME_KUMA_PUBLIC_URL}
|
||
url: ${UPTIME_KUMA_URL}/api/status-page/${UPTIME_KUMA_STATUS_SLUG}
|
||
subrequests:
|
||
heartbeats:
|
||
url: ${UPTIME_KUMA_URL}/api/status-page/heartbeat/${UPTIME_KUMA_STATUS_SLUG}
|
||
cache: 10m
|
||
template: |
|
||
{{ $hb := .Subrequest "heartbeats" }}
|
||
|
||
{{ if not (.JSON.Exists "publicGroupList") }}
|
||
<p class="color-negative">Error reading response</p>
|
||
{{ else if eq (len (.JSON.Array "publicGroupList")) 0 }}
|
||
<p>No monitors found</p>
|
||
{{ else }}
|
||
|
||
<ul class="dynamic-columns list-gap-8">
|
||
{{ range .JSON.Array "publicGroupList" }}
|
||
{{ range .Array "monitorList" }}
|
||
{{ $id := .String "id" }}
|
||
{{ $hbArray := $hb.JSON.Array (print "heartbeatList." $id) }}
|
||
<div class="flex items-center gap-12">
|
||
<a class="size-title-dynamic color-highlight text-truncate block grow" href="${UPTIME_KUMA_URL}/dashboard/{{ $id }}"
|
||
target="_blank" rel="noreferrer">
|
||
{{ .String "name" }} </a>
|
||
|
||
{{ if gt (len $hbArray) 0 }}
|
||
{{ $latest := index $hbArray (sub (len $hbArray) 1) }}
|
||
{{ if eq ($latest.Int "status") 1 }}
|
||
<div>{{ $latest.Int "ping" }}ms</div>
|
||
<div class="monitor-site-status-icon-compact" title="OK">
|
||
<svg fill="var(--color-positive)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd"
|
||
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z"
|
||
clip-rule="evenodd"></path>
|
||
</svg>
|
||
</div>
|
||
{{ else }}
|
||
<div><span class="color-negative">DOWN</span></div>
|
||
<div class="monitor-site-status-icon-compact" title="{{ if $latest.Exists "msg" }}{{ $latest.String "msg" }}{{ else
|
||
}}Error{{ end }}">
|
||
<svg fill="var(--color-negative)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd"
|
||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||
clip-rule="evenodd"></path>
|
||
</svg>
|
||
</div>
|
||
{{ end }}
|
||
{{ else }}
|
||
<div><span class="color-negative">No data</span></div>
|
||
<div class="monitor-site-status-icon-compact" title="No data available">
|
||
<svg fill="var(--color-negative)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||
<path d="M10 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16zm0-2a6 6 0 1 0 0-12 6 6 0 0 0 0 12zm-.75-8a.75.75 0 0 1 1.5 0v3a.75.75 0 0 1-1.5 0V8zm.75 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||
</svg>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
{{ end }}
|
||
{{ end }}
|
||
</ul>
|
||
{{ end }}
|
||
|
||
# ==================== MEDIA PAGE ====================
|
||
- name: Media
|
||
slug: media
|
||
width: wide
|
||
columns:
|
||
- size: small
|
||
widgets:
|
||
- type: bookmarks
|
||
title: Entertainment
|
||
groups:
|
||
- links:
|
||
- title: Plex
|
||
url: https://plex.dooplex.hu
|
||
icon: si:plex
|
||
- title: Immich (Photos)
|
||
url: https://photos.dooplex.hu
|
||
icon: si:immich
|
||
- title: AudioBookshelf
|
||
url: https://audiobookshelf.dooplex.hu
|
||
icon: si:audiobookshelf
|
||
- title: Calibre-Web (eBooks)
|
||
url: https://books.dooplex.hu
|
||
icon: si:calibreweb
|
||
- title: Arcade (Retro Games)
|
||
url: https://arcade.dooplex.hu
|
||
icon: si:retroarch
|
||
- title: Crafty Controller
|
||
url: https://crafty.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/minecraftcreeper.png
|
||
- title: YouTube
|
||
url: https://www.youtube.com
|
||
icon: si:youtube
|
||
- title: Spotify
|
||
url: https://open.spotify.com/
|
||
icon: si:spotify
|
||
|
||
- type: bookmarks
|
||
title: Media Management
|
||
groups:
|
||
- links:
|
||
- title: Sonarr (TV Shows)
|
||
url: https://sonarr.dooplex.hu
|
||
icon: si:sonarr
|
||
- title: Radarr (Movies)
|
||
url: https://radarr.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/radarr.png
|
||
- title: RadarrKids
|
||
url: https://radarrkids.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/radarrkids.png
|
||
- title: Prowlarr (Indexers)
|
||
url: https://prowlarr.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/prowlarr.png
|
||
- title: Seerr (Requests)
|
||
url: https://seerr.dooplex.hu
|
||
icon: https://web.dooplex.hu/static/white-icons/seerr.png
|
||
|
||
- type: custom-api
|
||
title: Crafty - Minecraft Server
|
||
cache: 5s
|
||
options:
|
||
base-url: ${CRAFTY_URL}
|
||
api-key: ${CRAFTY_API_TOKEN}
|
||
server-id: ${CRAFTY_SERVER_ID}
|
||
display-MOTD: true
|
||
allow-insecure: true
|
||
template: |
|
||
{{/* Required config options */}}
|
||
{{ $baseURL := .Options.StringOr "base-url" "" }}
|
||
{{ $apiKey := .Options.StringOr "api-key" "" }}
|
||
{{ $serverID := .Options.StringOr "server-id" "" }}
|
||
|
||
{{/* Optional config options */}}
|
||
{{ $displayMOTD := .Options.BoolOr "display-MOTD" true }}
|
||
|
||
{{ $serverStats := newRequest (print $baseURL "/api/v2/servers/" $serverID "/stats")
|
||
| withHeader "Authorization" (print "Bearer " $apiKey)
|
||
| withHeader "Accept" "application/json"
|
||
| getResponse }}
|
||
|
||
{{ $is_running := $serverStats.JSON.Bool "data.running" }}
|
||
{{ $online_players := $serverStats.JSON.Int "data.online" | formatNumber }}
|
||
{{ $max_players := $serverStats.JSON.Int "data.max" | formatNumber }}
|
||
{{ $name := $serverStats.JSON.String "data.world_name" }}
|
||
{{ $size := $serverStats.JSON.String "data.world_size" }}
|
||
{{ $version := $serverStats.JSON.String "data.version" }}
|
||
{{ $icon := $serverStats.JSON.String "data.icon" }}
|
||
{{ $server_ip := $serverStats.JSON.String "data.server_id.server_ip" }}
|
||
{{ $server_port := $serverStats.JSON.String "data.server_id.server_port" }}
|
||
{{ $motd := $serverStats.JSON.String "data.desc" }}
|
||
|
||
{{ $server_addr := "" }}
|
||
{{ if and ($is_running) (eq $server_ip "127.0.0.1") }}
|
||
{{ $server_addr = printf "%s:%s" (replaceMatches "https?://" "" $baseURL) $server_port }}
|
||
{{ else if $is_running }}
|
||
{{ $server_addr = printf "%s:%s" $server_ip $server_port }}
|
||
{{ end }}
|
||
|
||
{{ $starting := false }}
|
||
{{ if and ($is_running) (eq $max_players "0") (eq $version "False") }}
|
||
{{ $starting = true }}
|
||
{{ end }}
|
||
|
||
<!-- I couldn't find documentation describing the "waiting_start" state or the other booleans below. Implementation might not be correct. -->
|
||
{{ $updating := $serverStats.JSON.Bool "data.updating" }}
|
||
{{ $importing := $serverStats.JSON.Bool "data.importing" }}
|
||
{{ $crashed := $serverStats.JSON.Bool "data.crashed" }}
|
||
|
||
<div style="display:flex; align-items:center; gap:12px;">
|
||
<!-- Server Icon -->
|
||
<div style="width:40px; height:40px; flex-shrink:0; border-radius:4px; display:flex; justify-content:center; align-items:center; overflow:hidden;">
|
||
{{ if eq $icon "" }}
|
||
<img src="https://cdn.jsdelivr.net/gh/selfhst/icons/png/minecraft.png" style="width:100%; height:100%; object-fit:contain;" alt="Server icon">
|
||
{{ else }}
|
||
<img src="data:image/png;base64, {{ $icon }}" style="width:100%; height:100%; object-fit:contain;" alt="Server icon">
|
||
{{ end }}
|
||
</div>
|
||
|
||
<!-- Right side: Info -->
|
||
<div style="display:flex; flex-direction:column;">
|
||
<!-- First row: Server Name + IP -->
|
||
<div style="display:flex; align-items:center; gap:6px;">
|
||
<span class="size-h4 block text-truncate color-primary">
|
||
{{ $name }}
|
||
</span>
|
||
|
||
{{ if and ($is_running) (not $starting) (not (eq $server_addr "")) }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">
|
||
<span class="size-h6 color-secondary">
|
||
- {{ $server_addr }}
|
||
</span>
|
||
</div>
|
||
{{ end }}
|
||
</div>
|
||
|
||
<!-- Second row: MOTD & Stats if server is running, otherwise show status msg ... -->
|
||
{{ if and ($is_running) (not $starting) }}
|
||
{{ if and (not (eq $motd "")) ($displayMOTD) }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">
|
||
{{ replaceMatches "§." "" $motd }}
|
||
</div>
|
||
{{ end }}
|
||
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">
|
||
{{ $version }} - {{ $online_players }}/{{ $max_players }} players - {{ $size }}
|
||
</div>
|
||
|
||
<!-- lots of assumptions about the boolean states meanings from crafty api.. -->
|
||
{{ else if $starting }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">Server is starting up..</div>
|
||
{{ else if $importing }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">Server is being imported..</div>
|
||
{{ else if $updating }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">Server is being updated..</div>
|
||
{{ else if $crashed }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">Server has crashed!</div>
|
||
{{ else }}
|
||
<div style="font-size:0.9em; color:var(--color-secondary);">Server is offline</div>
|
||
{{ end }}
|
||
</div>
|
||
</div>
|
||
|
||
- type: custom-api
|
||
title: RomM
|
||
cache: 1d
|
||
url: http://${ROMM_URL}/api/stats
|
||
headers:
|
||
Accept: application/json
|
||
template: |
|
||
{{ $bytes := .JSON.Int "TOTAL_FILESIZE_BYTES" | toFloat }}
|
||
{{ $tb := div $bytes 1099511627776 }}
|
||
{{ $gb := div $bytes 1073741824 | toInt }}
|
||
|
||
<div class="flex justify-between text-center">
|
||
<div>
|
||
<div class="color-highlight size-h3">{{ .JSON.Int "PLATFORMS" | formatNumber }}</div>
|
||
<div class="size-h6">PLATFORMS</div>
|
||
</div>
|
||
<div>
|
||
<div class="color-highlight size-h3">{{ .JSON.Int "ROMS" | formatNumber }}</div>
|
||
<div class="size-h6">ROMS</div>
|
||
</div>
|
||
<div>
|
||
<div class="color-highlight size-h3">
|
||
{{ if ge $tb 1.0 }}
|
||
{{ printf "%.2f" $tb }}TB
|
||
{{ else }}
|
||
{{ $gb }}GB
|
||
{{ end }}
|
||
</div>
|
||
<div class="size-h6">FILESIZE</div>
|
||
</div>
|
||
</div>
|
||
|
||
- type: custom-api
|
||
title: Steam Specials
|
||
cache: 12h
|
||
url: https://store.steampowered.com/api/featuredcategories?cc=us
|
||
template: |
|
||
<ul class="list list-gap-10 collapsible-container" data-collapse-after="5">
|
||
{{ range .JSON.Array "specials.items" }}
|
||
{{ $header := .String "header_image" }}
|
||
{{ $urlPrefix := "https://store.steampowered.com/sub/" }}
|
||
{{ if findMatch "/steam/apps/" $header }}
|
||
{{ $urlPrefix = "https://store.steampowered.com/app/" }}
|
||
{{ end }}
|
||
<li>
|
||
<a class="size-h4 color-highlight block text-truncate" href="{{ $urlPrefix }}{{ .Int "id" }}/">{{ .String "name" }}</a>
|
||
<ul class="list-horizontal-text">
|
||
<li>{{ .Int "final_price" | toFloat | mul 0.01 | printf "$%.2f" }}</li>
|
||
{{ $discount := .Int "discount_percent" }}
|
||
<li{{ if ge $discount 40 }} class="color-positive"{{ end }}>{{ $discount }}% off</li>
|
||
</ul>
|
||
</li>
|
||
{{ end }}
|
||
</ul>
|
||
|
||
- size: full
|
||
widgets:
|
||
# YouTube Videos
|
||
- type: videos
|
||
title: YouTube - Gaming
|
||
channels:
|
||
- UC4Xj6emHTXnKHUq8btUuN6A #Retromation
|
||
- UC5ib5bTflXtyIkoF_l7OCHw #Olexa
|
||
- UCfWybrB-Faa30sXUE6eqMIQ #Zarfen the Loot Goblin
|
||
- UCto7D1L-MiRoOziCXK9uT5Q #Let's Game It Out
|
||
limit: 12
|
||
collapse-after: 6
|
||
- type: videos
|
||
title: YouTube - Tech
|
||
channels:
|
||
- UCXuqSBlHAE6Xw-yeJA0Tunw #LinusTechTips
|
||
- UCdngmbVKX1Tgre699-XLlUA #Techworld with Nana
|
||
- UCJa14zeVf8p6clixTOIOVyQ #Jakkuh (Jake)
|
||
- UC-2YHgc363EdcusLIBbgxzg #JoeScott
|
||
- UCY1kMZp36IQSyNx_9h4mpCg #Mark Rober
|
||
- UC7IcJI8PUf5Z3zKxnZvTBog #The School of Life
|
||
limit: 12
|
||
collapse-after: 6
|
||
- type: videos
|
||
title: YouTube - Other
|
||
channels:
|
||
- UC9qpYwK7N9EB0-SECANa23g #Jólvanezígy
|
||
- UCEFpEvuosfPGlV1VyUF6QOA #Partizán
|
||
- UC_fAjvoGnqySaNM6DRfFiCA #Pottyondi
|
||
- UCM-1sd-cXSuCsfWp8QMY_OQ #Telex.hu
|
||
- UCdPhEv5wiw2uK6J0_wkc-RA #Bűnvadászok
|
||
- UCQAeX1_gw45xb3Cg-OwEFcw #Friderikusz
|
||
- UCW5OrUZ4SeUYkUg1XqcjFYA #GeoWizard
|
||
limit: 12
|
||
collapse-after: 6
|
||
|
||
# Reddit
|
||
- type: group
|
||
title: Reddit
|
||
widgets:
|
||
- type: reddit
|
||
subreddit: hungary
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: selfhosted
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: homeserver
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: homelab
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: kubernetes
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: linux
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: sysadmin
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: technology
|
||
show-thumbnails: true
|
||
- type: reddit
|
||
subreddit: futurology
|
||
show-thumbnails: true
|
||
|
||
- size: small
|
||
widgets:
|
||
# RSS Feeds - Add your favorite feeds here
|
||
- type: rss
|
||
title: News & Feeds
|
||
limit: 15
|
||
collapse-after: 15
|
||
feeds:
|
||
- url: https://telex.hu/rss
|
||
title: telex.hu
|
||
limit: 3
|
||
- url: https://444.hu/feed
|
||
title: 444.hu
|
||
limit: 3
|
||
- url: https://444.hu/feed
|
||
title: 444.hu
|
||
limit: 3
|
||
- url: https://hvg.hu/rss
|
||
title: hvg.hu
|
||
limit: 3
|
||
|
||
# ==================== NEXTCLOUD PAGE ====================
|
||
- name: NextCloud
|
||
slug: nextcloud
|
||
width: wide
|
||
columns:
|
||
- size: full
|
||
widgets:
|
||
# Nextcloud iframe
|
||
- type: iframe
|
||
css-class: iframe-no-tint
|
||
source: https://nextcloud.dooplex.hu/apps/files/files
|
||
height: 1200
|
||
title: NextCloud
|
||
|
||
custom.css: |
|
||
/* =========================================================================
|
||
WALLPAPER VISIBLE
|
||
========================================================================= */
|
||
|
||
html, body { height: 100%; }
|
||
|
||
html {
|
||
background: url("https://web.dooplex.hu/static/wallpaper-2.jpg") center / cover no-repeat fixed !important;
|
||
}
|
||
|
||
/* Glance containers that tend to paint over the wallpaper */
|
||
body,
|
||
.page,
|
||
#page-content,
|
||
.page-content,
|
||
.content-bounds,
|
||
.page-columns,
|
||
.page-column {
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* Optional readability veil (Homepage-like) */
|
||
body::before {
|
||
content: "";
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(20, 10, 30, 0.25);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
body > * { position: relative; z-index: 1; }
|
||
|
||
/* =========================================================================
|
||
ROOT VARIABLES OVERRIDE
|
||
These override Glance's default theme colors at the CSS variable level
|
||
========================================================================= */
|
||
:root {
|
||
/* Primary color - affects many built-in elements */
|
||
--color-primary: hsl(190, 70%, 60%) !important;
|
||
|
||
/* These control various UI elements */
|
||
--color-text-highlight: #5ac8d8 !important;
|
||
--color-text-accent: #5ac8d8 !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
GLOBAL LINK COLORS
|
||
Affects all <a> tags site-wide (bookmarks, reddit links, video titles, etc.)
|
||
========================================================================= */
|
||
a {
|
||
color: #5ac8d8 !important; /* CYAN - main link color */
|
||
}
|
||
|
||
a:hover {
|
||
color: #7ed9e6 !important; /* LIGHTER CYAN - hover state */
|
||
}
|
||
|
||
/* Visited links - slightly muted */
|
||
a:visited {
|
||
color: #4ab8c8 !important; /* SLIGHTLY DARKER CYAN */
|
||
}
|
||
|
||
/* =========================================================================
|
||
HEADER / NAVIGATION
|
||
The top bar with Home, Media, NextCloud tabs
|
||
========================================================================= */
|
||
|
||
/* Push nav items to bottom of header and align properly */
|
||
.header.flex {
|
||
align-items: flex-end !important;
|
||
}
|
||
|
||
.header.flex > .nav.flex {
|
||
height: 100% !important;
|
||
align-items: flex-end !important;
|
||
padding-bottom: 0 !important; /* Remove extra padding */
|
||
}
|
||
|
||
/* Nav item text styling */
|
||
.header .nav .nav-item,
|
||
.header.flex > .nav.flex > .nav-item {
|
||
color: #5ac8d8 !important; /* CYAN - nav text color */
|
||
font-size: 20px !important;
|
||
line-height: 1 !important;
|
||
padding: 8px 14px 12px 14px !important; /* top right bottom left */
|
||
letter-spacing: 0.3px !important;
|
||
text-transform: uppercase !important;
|
||
font-weight: 500 !important;
|
||
display: flex !important;
|
||
align-items: flex-end !important;
|
||
height: auto !important; /* Let it size naturally */
|
||
}
|
||
|
||
/* Nav item hover */
|
||
.header .nav .nav-item:hover {
|
||
color: #7ed9e6 !important; /* LIGHTER CYAN on hover */
|
||
}
|
||
|
||
/* Active tab underline - position closer to text */
|
||
.header .nav .nav-item-current::after,
|
||
.header .nav .nav-item[aria-current="page"]::after {
|
||
bottom: 4px !important; /* Closer to text */
|
||
background-color: #5ac8d8 !important; /* CYAN underline */
|
||
}
|
||
|
||
.header {
|
||
min-height: 80px !important;
|
||
align-items: flex-end !important;
|
||
}
|
||
|
||
/* This matches your DOM: <div class="logo"> <img ...> */
|
||
.logo img {
|
||
max-height: 100px !important;
|
||
height: auto !important;
|
||
width: auto !important;
|
||
object-fit: contain !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
HEADER BAR - Transparent background
|
||
========================================================================= */
|
||
.header-container,
|
||
.header-container.content-bounds,
|
||
.header,
|
||
.header.flex,
|
||
div.header-container {
|
||
background: transparent !important;
|
||
background-color: transparent !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
WIDGET TITLES
|
||
The "WEATHER", "NEWS & FEEDS", etc. headers on each widget
|
||
========================================================================= */
|
||
/* The header container */
|
||
.widget-header {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: flex-start !important;
|
||
min-height: 2.2em !important;
|
||
padding: 0.5em 0 0.5em 12px !important; /* top right bottom LEFT */
|
||
box-sizing: border-box !important;
|
||
}
|
||
|
||
/* The h2 title text inside */
|
||
.widget-header h2,
|
||
.widget-header .uppercase,
|
||
h2.uppercase {
|
||
color: #5ac8d8 !important;
|
||
font-weight: 600 !important;
|
||
margin: 0 !important;
|
||
padding: 0 !important;
|
||
line-height: 1 !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
REMOVE DEFAULT BORDER & ADD CYAN LINE between header and content
|
||
========================================================================= */
|
||
|
||
/* Nuclear option - remove ALL borders from widget-content */
|
||
.widget-content,
|
||
.widget-content:not(.widget-content-frameless),
|
||
.widget-content-frame,
|
||
.widget-content:first-of-type,
|
||
.widget > .widget-content,
|
||
[class*="widget-content"] {
|
||
border: 0 !important;
|
||
border-top: 0 !important;
|
||
border-width: 0 !important;
|
||
border-style: none !important;
|
||
border-color: transparent !important;
|
||
box-shadow: none !important;
|
||
outline: none !important;
|
||
--color-widget-content-border: transparent !important;
|
||
background-color: rgba(20, 50, 70, 0.35) !important; /* DARK BLUE, semi-transparent */
|
||
}
|
||
|
||
/* Add cyan line via pseudo-element on header bottom */
|
||
.widget-header::after {
|
||
content: "" !important;
|
||
display: block !important;
|
||
position: absolute !important;
|
||
bottom: 0 !important;
|
||
left: 12px !important;
|
||
right: 12px !important;
|
||
height: 1px !important;
|
||
background: rgba(90, 200, 216, 0.4) !important; /* CYAN line */
|
||
}
|
||
|
||
/* Ensure header is positioned for the pseudo-element */
|
||
.widget-header {
|
||
position: relative !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
WIDGET BACKGROUNDS
|
||
Semi-transparent backgrounds for widget containers
|
||
========================================================================= */
|
||
|
||
/* Standard widgets (calendar, weather, to-do, RSS, etc.) */
|
||
.widget,
|
||
.widget-type-calendar,
|
||
.widget-type-weather,
|
||
.widget-type-to-do,
|
||
.widget-type-rss,
|
||
.widget-type-videos,
|
||
.widget-type-reddit,
|
||
.widget-type-bookmarks {
|
||
background-color: rgba(20, 50, 70, 0.35) !important; /* DARK BLUE, semi-transparent */
|
||
}
|
||
|
||
/* RSS feed items */
|
||
.rss-item,
|
||
.feed-item {
|
||
background-color: rgba(45, 126, 136, 0.25) !important; /* TEAL tint */
|
||
border-radius: 6px !important;
|
||
margin-bottom: 4px !important;
|
||
}
|
||
|
||
/* Video items */
|
||
.video-item,
|
||
.videos-item {
|
||
background-color: rgba(45, 126, 136, 0.25) !important; /* TEAL tint */
|
||
border-radius: 8px !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
WIDGET OVERLAYS
|
||
========================================================================= */
|
||
|
||
.widget.widget-type-iframe {
|
||
position: relative !important;
|
||
overflow: hidden !important;
|
||
border-radius: 12px !important;
|
||
}
|
||
|
||
.widget.widget-type-iframe iframe {
|
||
border-radius: 12px !important;
|
||
width: 100% !important;
|
||
border: 0 !important;
|
||
filter: sepia(0.25) saturate(1) hue-rotate(0deg) brightness(1.05) !important;
|
||
position: relative !important;
|
||
z-index: 1 !important;
|
||
}
|
||
|
||
/* Overlay ON TOP of iframe (you can’t style inside cross-origin iframes) */
|
||
.widget.widget-type-iframe::after {
|
||
content: "";
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
border-radius: 12px;
|
||
background: rgba(45, 181, 230, 0.12);
|
||
}
|
||
|
||
/* iFrames: slightly transparent + boosted contrast/brightness */
|
||
.widget.widget-type-iframe iframe {
|
||
opacity: 0.7 !important;
|
||
filter: contrast(1.15) brightness(1.3) !important;
|
||
border-radius: 12px !important;
|
||
width: 100% !important;
|
||
border: 0 !important;
|
||
position: relative !important;
|
||
z-index: 1 !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
CONTAINER VERSIONS WIDGET HEIGHT INCREASE
|
||
========================================================================= */
|
||
|
||
.widget.widget-type-custom-api:has(.ver-widget) .ver-list {
|
||
max-height: 855px !important;
|
||
}
|
||
.widget.widget-type-custom-api:has(.ver-widget) {
|
||
min-height: 975px !important;
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
WIDGET ROUNDED CORNERS
|
||
========================================================================= */
|
||
|
||
/* The parent widget container - round ALL corners and clip children */
|
||
.widget {
|
||
border-radius: 12px !important;
|
||
overflow: hidden !important; /* This clips the children to the rounded shape */
|
||
}
|
||
|
||
/* Header - round only TOP corners */
|
||
.widget-header {
|
||
border-top-left-radius: 12px !important;
|
||
border-top-right-radius: 12px !important;
|
||
border-bottom-left-radius: 0 !important;
|
||
border-bottom-right-radius: 0 !important;
|
||
}
|
||
|
||
/* Content - round only BOTTOM corners */
|
||
.widget-content,
|
||
.widget-content:not(.widget-content-frameless),
|
||
.widget-content-frame {
|
||
border-top-left-radius: 0 !important;
|
||
border-top-right-radius: 0 !important;
|
||
border-bottom-left-radius: 12px !important;
|
||
border-bottom-right-radius: 12px !important;
|
||
}
|
||
|
||
/* For widgets without headers (content only), round all corners */
|
||
.widget:not(:has(.widget-header)) .widget-content {
|
||
border-radius: 12px !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
BOOKMARK STYLING
|
||
The link cards in bookmark widgets
|
||
========================================================================= */
|
||
.bookmark-link {
|
||
background-color: rgba(30, 70, 90, 0.6) !important; /* DARK BLUE-TEAL */
|
||
border-radius: 8px !important;
|
||
transition: background-color 0.2s ease !important;
|
||
}
|
||
|
||
.bookmark-link:hover {
|
||
background-color: rgba(45, 100, 120, 0.8) !important; /* LIGHTER on hover */
|
||
}
|
||
|
||
/* =========================================================================
|
||
REDDIT WIDGET SPECIFIC
|
||
Subreddit tabs and content
|
||
========================================================================= */
|
||
|
||
/* Subreddit tabs (R/HUNGARY, R/SELFHOSTED, etc.) */
|
||
.reddit-subreddit-tabs .tab,
|
||
.subreddit-tabs button,
|
||
.subreddit-tab,
|
||
[class*="subreddit"] button,
|
||
.widget-type-reddit button {
|
||
color: #5ac8d8 !important; /* CYAN */
|
||
}
|
||
|
||
.reddit-subreddit-tabs .tab:hover,
|
||
.subreddit-tabs button:hover,
|
||
[class*="subreddit"] button:hover {
|
||
color: #7ed9e6 !important; /* LIGHTER CYAN on hover */
|
||
}
|
||
|
||
/* Active subreddit tab */
|
||
.reddit-subreddit-tabs .tab.active,
|
||
.subreddit-tabs button.active,
|
||
.subreddit-tab.active,
|
||
[class*="subreddit"] button[aria-selected="true"] {
|
||
color: #7ed9e6 !important;
|
||
border-color: #5ac8d8 !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
VIDEO WIDGET SPECIFIC
|
||
YouTube video titles and channel names
|
||
========================================================================= */
|
||
.video-title,
|
||
.videos-item-title,
|
||
.video-channel,
|
||
.videos-item-channel {
|
||
color: #5ac8d8 !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
BACKGROUNDS - TRANSPARENT
|
||
Keep page backgrounds transparent to show the space background
|
||
========================================================================= */
|
||
.content-bounds,
|
||
.body-content,
|
||
.page,
|
||
#page-content,
|
||
.page-column,
|
||
.page-columns {
|
||
background: transparent !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
SCROLLBAR STYLING
|
||
Custom scrollbar colors
|
||
========================================================================= */
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: rgba(30, 60, 90, 0.4); /* DARK BLUE track */
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: rgba(90, 200, 216, 0.5); /* CYAN thumb */
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(90, 200, 216, 0.7); /* BRIGHTER CYAN on hover */
|
||
}
|
||
|
||
/* =========================================================================
|
||
EXPAND/COLLAPSE BUTTONS
|
||
"Show more" buttons in RSS, Reddit widgets
|
||
========================================================================= */
|
||
.expand-toggle-button,
|
||
.expand-toggle-button.container-expanded,
|
||
.widget-type-rss .expand-toggle-button {
|
||
background: transparent !important;
|
||
background-color: transparent !important;
|
||
box-shadow: none !important;
|
||
border: 0 !important;
|
||
}
|
||
|
||
.widget-type-rss .expand-toggle-button {
|
||
margin-top: 8px !important;
|
||
padding: 10px 12px !important;
|
||
border-top: 1px solid rgba(90, 200, 216, 0.2) !important; /* CYAN tinted border */
|
||
color: rgba(255,255,255,0.75) !important;
|
||
}
|
||
|
||
.expand-toggle-button:hover,
|
||
.widget-type-rss .expand-toggle-button:hover {
|
||
color: #5ac8d8 !important; /* CYAN on hover */
|
||
}
|
||
|
||
/* Remove pseudo-element backgrounds */
|
||
.expand-toggle-button::before,
|
||
.expand-toggle-button::after,
|
||
.expand-toggle-button-icon::before,
|
||
.expand-toggle-button-icon::after,
|
||
.widget-type-rss .expand-toggle-button::before,
|
||
.widget-type-rss .expand-toggle-button::after {
|
||
background: transparent !important;
|
||
background-color: transparent !important;
|
||
box-shadow: none !important;
|
||
border: 0 !important;
|
||
}
|
||
|
||
.expand-toggle-button.container-expanded {
|
||
backdrop-filter: none !important;
|
||
-webkit-backdrop-filter: none !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
IFRAME STYLING
|
||
Remove tint/overlay from embedded iframes
|
||
========================================================================= */
|
||
.widget.iframe-no-tint iframe {
|
||
filter: none !important;
|
||
}
|
||
|
||
.widget.iframe-no-tint::after {
|
||
content: none !important;
|
||
display: none !important;
|
||
}
|
||
|
||
/* =========================================================================
|
||
ADDITIONAL ELEMENTS
|
||
Various UI elements that might need color overrides
|
||
========================================================================= */
|
||
|
||
/* Monitor widget status indicators */
|
||
.monitor-site-status {
|
||
color: #5ac8d8 !important;
|
||
}
|
||
|
||
/* Group widget tabs */
|
||
.group-tabs button,
|
||
.tabs button {
|
||
color: #5ac8d8 !important;
|
||
}
|
||
|
||
.group-tabs button:hover,
|
||
.tabs button:hover {
|
||
color: #7ed9e6 !important;
|
||
}
|
||
|
||
.group-tabs button.active,
|
||
.tabs button.active,
|
||
.group-tabs button[aria-selected="true"],
|
||
.tabs button[aria-selected="true"] {
|
||
color: #7ed9e6 !important;
|
||
border-color: #5ac8d8 !important;
|
||
}
|
||
/* =========================================================================
|
||
Időkép custom-api widget
|
||
========================================================================= */
|
||
.idokep { display: flex; flex-direction: column; gap: 10px; }
|
||
|
||
.idokep-top { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
|
||
.idokep-top-left { display: flex; align-items: center; gap: 10px; }
|
||
.idokep-icon { width: 42px; height: 42px; opacity: 0.95; }
|
||
.idokep-temp { font-size: 42px; font-weight: 700; letter-spacing: 0.5px; line-height: 1; }
|
||
|
||
.idokep-top-right { text-align: right; }
|
||
.idokep-loc { opacity: 0.85; font-weight: 600; }
|
||
.idokep-src { opacity: 0.6; font-size: 12px; margin-top: 2px; }
|
||
.idokep-src a { opacity: 0.9; }
|
||
|
||
.idokep-hourly { display: flex; gap: 10px; padding-top: 4px; }
|
||
.idokep-hour { width: 54px; display: flex; flex-direction: column; align-items: center; gap: 6px; opacity: 0.9; }
|
||
.idokep-hour-time { font-size: 12px; opacity: 0.65; }
|
||
.idokep-hour-icon { width: 26px; height: 26px; }
|
||
.idokep-hour-temp { font-weight: 700; }
|
||
|
||
.idokep-muted { opacity: 0.6; font-size: 12px; padding: 4px 0; }
|
||
|
||
.idokep-daily { display: flex; flex-direction: column; gap: 8px; margin-top: 2px; }
|
||
.idokep-row {
|
||
display: grid;
|
||
grid-template-columns: 44px 26px 36px 1fr 36px; /* day+date, icon, min, bar, max */
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
.idokep-dow { opacity: 0.7; font-weight: 700; }
|
||
.idokep-dayicon img { width: 22px; height: 22px; opacity: 0.95; }
|
||
.idokep-min, .idokep-max { text-align: right; font-weight: 700; opacity: 0.8; }
|
||
.idokep-dow {
|
||
display: grid;
|
||
grid-template-columns: 22px 1fr; /* dow then daynum */
|
||
column-gap: 6px;
|
||
align-items: center;
|
||
opacity: 0.8;
|
||
font-weight: 700;
|
||
}
|
||
.idokep-daynum {
|
||
text-align: right;
|
||
opacity: 0.75;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.idokep-bar {
|
||
position: relative;
|
||
height: 10px;
|
||
}
|
||
.idokep-bar-track {
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: 999px;
|
||
background: rgba(255,255,255,0.10);
|
||
}
|
||
.idokep-bar-fill {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
box-shadow: 0 0 0 1px rgba(0,0,0,0.08) inset;
|
||
|
||
/* Position controlled by Python variables */
|
||
left: var(--l, 0%);
|
||
width: var(--w, 0%);
|
||
}
|
||
/* This element holds the gradient */
|
||
.idokep-bar-gradient {
|
||
position: absolute;
|
||
top: 0;
|
||
bottom: 0;
|
||
|
||
/* Compensation geometry controlled by Python variables */
|
||
width: var(--gw, 100%);
|
||
margin-left: var(--ml, 0%);
|
||
|
||
/* The Dynamic Gradient */
|
||
background: linear-gradient(90deg,
|
||
#ffffff var(--s-wht),
|
||
#60a5fa var(--s-blu),
|
||
#a78bfa var(--s-pur),
|
||
#fb7185 var(--s-pnk),
|
||
#ef4444 var(--s-red)
|
||
);
|
||
}
|
||
|
||
/* =========================================================================
|
||
Forcing transparency on no-tint iframes
|
||
========================================================================= */
|
||
|
||
/* Force notes iframe to be transparent */
|
||
.widget.iframe-no-tint iframe {
|
||
background: transparent !important;
|
||
}
|
||
---
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: glance-kisfenyo
|
||
namespace: glance-system
|
||
labels:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
app.kubernetes.io/version: "v0.8.4"
|
||
annotations:
|
||
reloader.stakater.com/auto: "true"
|
||
spec:
|
||
replicas: 1
|
||
strategy:
|
||
type: Recreate
|
||
selector:
|
||
matchLabels:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
app.kubernetes.io/version: "v0.8.4"
|
||
spec:
|
||
securityContext:
|
||
runAsUser: 1000
|
||
runAsGroup: 1000
|
||
fsGroup: 1000
|
||
initContainers:
|
||
- name: build-bookmarks-index
|
||
image: mikefarah/yq:4.50.1
|
||
securityContext:
|
||
runAsUser: 1000
|
||
runAsGroup: 1000
|
||
allowPrivilegeEscalation: false
|
||
command: ["/bin/sh", "-c"]
|
||
args:
|
||
- |
|
||
set -eux
|
||
which yq
|
||
yq --version
|
||
|
||
mkdir -p /app/assets
|
||
|
||
yq eval -o=json '
|
||
[ .pages[] as $p
|
||
| $p.columns[]? as $c
|
||
| $c.widgets[]? as $w
|
||
| select($w.type == "bookmarks")
|
||
| $w.groups[]? as $g
|
||
| $g.links[]?
|
||
| select(.url != null and .url != "")
|
||
| {
|
||
"title": (.title // .url),
|
||
"url": .url,
|
||
"page": ($p.name // ""),
|
||
"widget": ($w.title // ""),
|
||
"group": ($g.title // "")
|
||
}
|
||
] | unique_by(.url)
|
||
' /config/glance.yml > /app/assets/bookmarks.json
|
||
|
||
echo "Bookmarks indexed: $(yq eval -r 'length' /app/assets/bookmarks.json)"
|
||
volumeMounts:
|
||
- name: config
|
||
mountPath: /config
|
||
readOnly: true
|
||
- name: assets
|
||
mountPath: /app/assets
|
||
containers:
|
||
- name: glance
|
||
image: glanceapp/glance:v0.8.4
|
||
imagePullPolicy: IfNotPresent
|
||
env:
|
||
- name: TZ
|
||
value: "Europe/Budapest"
|
||
- name: PROMETHEUS_URL
|
||
value: "http://prometheus.mon-system.svc.cluster.local:9090"
|
||
- name: CRAFTY_URL
|
||
value: "https://crafty.dooplex.hu"
|
||
- name: CRAFTY_API_TOKEN
|
||
value: "newtoken"
|
||
- name: CRAFTY_SERVER_ID
|
||
value: "837c7378-7a76-46f7-b8ea-6f35c0a314f4"
|
||
- name: ROMM_URL
|
||
value: "arcade.dooplex.hu"
|
||
- name: UPTIME_KUMA_PUBLIC_URL
|
||
value: "http://uptimekuma.dooplex.hu"
|
||
- name: UPTIME_KUMA_URL
|
||
value: "http://uptimekuma.uptimekuma-system.svc.cluster.local:3001"
|
||
- name: UPTIME_KUMA_STATUS_SLUG
|
||
value: "homepage"
|
||
ports:
|
||
- name: http
|
||
containerPort: 8080
|
||
protocol: TCP
|
||
livenessProbe:
|
||
httpGet:
|
||
path: /
|
||
port: http
|
||
initialDelaySeconds: 10
|
||
periodSeconds: 30
|
||
timeoutSeconds: 5
|
||
failureThreshold: 3
|
||
readinessProbe:
|
||
httpGet:
|
||
path: /
|
||
port: http
|
||
initialDelaySeconds: 5
|
||
periodSeconds: 10
|
||
timeoutSeconds: 5
|
||
failureThreshold: 3
|
||
resources:
|
||
requests:
|
||
cpu: 10m
|
||
memory: 32Mi
|
||
limits:
|
||
cpu: 200m
|
||
memory: 128Mi
|
||
volumeMounts:
|
||
- name: assets
|
||
mountPath: /app/assets
|
||
- name: config
|
||
mountPath: /app/config/glance.yml
|
||
subPath: glance.yml
|
||
- name: config
|
||
mountPath: /app/assets/custom.css
|
||
subPath: custom.css
|
||
volumes:
|
||
- name: config
|
||
configMap:
|
||
name: glance-config-kisfenyo
|
||
- name: assets
|
||
emptyDir: {}
|
||
---
|
||
apiVersion: v1
|
||
kind: Service
|
||
metadata:
|
||
name: glance-kisfenyo
|
||
namespace: glance-system
|
||
labels:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
spec:
|
||
type: ClusterIP
|
||
ports:
|
||
- name: http
|
||
port: 8080
|
||
targetPort: http
|
||
protocol: TCP
|
||
selector:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
---
|
||
# Ingress WITH Authentik proxy authentication
|
||
# Update the auth-url annotation with your actual outpost service name after creating in Authentik
|
||
apiVersion: networking.k8s.io/v1
|
||
kind: Ingress
|
||
metadata:
|
||
name: glance-kisfenyo
|
||
namespace: glance-system
|
||
labels:
|
||
app.kubernetes.io/name: glance-kisfenyo
|
||
app.kubernetes.io/instance: glance-kisfenyo
|
||
annotations:
|
||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||
external-dns.alpha.kubernetes.io/hostname: kisfenyo.dooplex.hu
|
||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||
nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
|
||
nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "32k"
|
||
# Authentik Forward Auth annotations
|
||
# TODO: Update 'glance-outpost' with your actual outpost name after creating in Authentik
|
||
nginx.ingress.kubernetes.io/auth-url: http://ak-outpost-glance-outpost.auth-system.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx
|
||
nginx.ingress.kubernetes.io/auth-signin: https://kisfenyo.dooplex.hu/outpost.goauthentik.io/start?rd=$escaped_request_uri
|
||
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email
|
||
nginx.ingress.kubernetes.io/auth-snippet: |
|
||
proxy_set_header X-Forwarded-Host $http_host;
|
||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||
set $geo_allowed 0;
|
||
if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; }
|
||
if ($remote_addr ~ "^10\.") { set $geo_allowed 1; }
|
||
if ($geoip2_country_code = "HU") { set $geo_allowed 1; }
|
||
if ($geo_allowed = 0) {
|
||
return 403 "Access restricted to Hungary";
|
||
}
|
||
spec:
|
||
ingressClassName: nginx-internal
|
||
rules:
|
||
- host: kisfenyo.dooplex.hu
|
||
http:
|
||
paths:
|
||
- path: /
|
||
pathType: Prefix
|
||
backend:
|
||
service:
|
||
name: glance-kisfenyo
|
||
port:
|
||
number: 8080
|
||
tls:
|
||
- hosts:
|
||
- kisfenyo.dooplex.hu
|
||
secretName: glance-kisfenyo-tls
|
||
--- |