Files
homelab-manifests/glance-system/glance-orsi.yaml
T
2026-01-23 08:47:25 +01:00

1366 lines
50 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Glance Dashboard for Orsi
# Namespace: glance-system
# Domain: orsi.dooplex.hu
# Version: v0.8.4
#
# Features:
# - Custom background image (purple theme matching Homepage)
# - 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 Orsi"
# 2. Create Provider: Proxy Provider with external host https://orsi.dooplex.hu
# 3. Create Outpost: glance-outpost
# 4. Update auth-url annotation with actual outpost service name
---
apiVersion: v1
kind: Namespace
metadata:
name: glance-system
labels:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: glance-config
namespace: glance-system
labels:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
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 => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[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();
}
});
})();
</script>
branding:
logo-url: https://web.dooplex.hu/static/dooplex_logo_orsi_3.png
favicon-url: https://web.dooplex.hu/static/dooplex_favicon_orsi.png
app-name: "Orsi's Home"
app-icon-url: https://web.dooplex.hu/static/dooplex_favicon_orsi.png
app-background-color: "#2d1f3d"
hide-footer: true
theme:
disable-picker: true
background-color: 280 30 15
primary-color: 280 60 70
positive-color: 120 50 50
negative-color: 0 70 60
contrast-multiplier: 1.2
text-saturation-multiplier: 0.8
custom-css-file: /assets/custom.css
pages:
# ==================== HOME PAGE ====================
- name: Home
slug: home
width: wide
columns:
# ---------- LEFT 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>
# Quick Links - Productivity
- type: bookmarks
title: Productivity
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
# Quick Links - File Sharing
- type: bookmarks
title: File Sharing
groups:
- title: ""
links:
- 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
# ---------- 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
- type: to-do
title: Tasks
# Outline Notes iframe
- type: iframe
source: https://outline.dooplex.hu/collection/notes-VVby8kTDMn/recent
height: 500
title: Recent Notes
# ---------- RIGHT COLUMN ----------
- size: small
widgets:
- type: rss
title: News & Feeds
limit: 15
collapse-after: 8
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
- url: http://www.socialpsychology.org/headlines.rss
title: socialpsychology.org
limit: 3
- url: https://youarenotsosmart.com/feed/
title: youarenotsosmart.com
limit: 3
# ==================== TEACHING/LEARNING PAGE ====================
- name: Teaching & Learning
slug: teaching
width: wide
columns:
- size: small
widgets:
- type: bookmarks
title: Links for Teaching
groups:
- links:
- title: Plex
url: https://plex.dooplex.hu
icon: si:plex
- type: bookmarks
title: Links for Learning
groups:
- links:
- title: Plex
url: https://plex.dooplex.hu
icon: si:plex
# ---------- CENTER COLUMN ----------
- size: full
widgets:
# Cal.com Booking iframe
- type: iframe
source: https://booking.dooplex.hu/bookings/upcoming
height: 500
title: Upcoming Bookings
# Google Calendar iframe
- type: iframe
source: https://calendar.google.com/calendar/embed?src=b2884faf3db792ac082a6206057552c79080716efd5f966e169a41fc500e1c1c%40group.calendar.google.com&ctz=Europe%2FBudapest
height: 500
title: Calendar
# ==================== 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
- 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
- size: full
widgets:
# YouTube Videos
- type: videos
title: YouTube
channels:
- UCir93b_ftqInEaDpsWYbo_g #Practical Psychology - @practicalpsychologytips
- UCEwsbtepqts935wXykReKxg #CounsellingTutor - @Counsellingtutor1
- UCUdettijNYvLAm4AixZv4RA #SciShow Psych - @SciShowPsych
- UClHVl2N3jPEbkNJVx-ItQIQ #HealthyGamerGG - @HealthyGamerGG
- UCAvfZQ3r24F-V1JYqn2pfXg #The Psychology Podcast - @ThePsychologyPodcast
- UC6Unpcb3T4QijIBs8hPfeyA #Psych Explained - @PsychExplained
- UCl8TEoIOnMq_5ntJOYMq-Zg #DrJulie - @DrJulie
limit: 12
collapse-after: 6
# Reddit
- type: group
title: Reddit
widgets:
- type: reddit
subreddit: psychology
show-thumbnails: true
- type: reddit
subreddit: psychologystudents
show-thumbnails: true
- type: reddit
subreddit: psychologytalk
show-thumbnails: true
- type: reddit
subreddit: psychologists
show-thumbnails: true
- type: reddit
subreddit: psychologyresearch
show-thumbnails: true
- type: reddit
subreddit: academicpsychology
show-thumbnails: true
- type: reddit
subreddit: social_psychology
show-thumbnails: true
- size: small
widgets:
- type: bookmarks
title: Other Apps
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
# ==================== FILES PAGE ====================
- name: Files
slug: files
width: wide
columns:
- size: full
widgets:
# Filebrowser iframe
- type: iframe
css-class: iframe-no-tint
source: https://orsi-files.dooplex.hu/files/
height: 1200
title: My Files
# ==================== NECTCLOUD 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: |
/* Custom CSS for Orsi's Glance Dashboard */
/* Purple theme with background image matching Homepage */
/* ===== Wallpaper visible ===== */
html, body { height: 100%; }
html {
background: url("https://web.dooplex.hu/static/wallpaper-orsi.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; }
/* ===== Glassy widgets like Homepage ===== */
.widget {
background: rgba(45, 31, 61, 0.55) !important;
border: 1px solid rgba(255,255,255,0.08) !important;
border-radius: 10px !important;
backdrop-filter: blur(6px) !important;
-webkit-backdrop-filter: blur(6px) !important;
}
/* ===== Header + bigger logo ===== */
.header-container {
background: rgba(45, 31, 61, 0.55) !important;
backdrop-filter: blur(10px) !important;
-webkit-backdrop-filter: blur(10px) !important;
border-bottom: 1px solid rgba(255,255,255,0.08) !important;
}
.header {
min-height: 150px !important;
align-items: flex-end !important;
padding: 16px 18px !important;
}
/* This matches your DOM: <div class="logo"> <img ...> */
.logo img {
max-height: 170px !important;
height: auto !important;
width: auto !important;
object-fit: contain !important;
}
/* ===== iFrame purple tint + visible overlay ===== */
.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(3) hue-rotate(250deg) brightness(1.02) !important;
position: relative !important;
z-index: 1 !important;
}
/* Overlay ON TOP of iframe (you cant 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(226, 146, 255, 0.16);
}
/* Widget content area */
.widget-content {
background-color: transparent !important;
}
/* Header fully transparent */
.header-container,
.header,
.page-header,
.nav,
.navigation {
background-color: transparent !important;
background: transparent !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
box-shadow: none !important;
border: 0 !important;
}
/* --- Header: align tabs to bottom + larger font --- */
.header {
display: flex !important;
align-items: flex-end !important; /* bottom-align children */
gap: 18px !important;
}
/* Push the nav to the bottom edge */
.header .nav {
align-self: flex-end !important;
padding-bottom: 10px !important; /* tweak this value if you want it lower/higher */
}
/* Bigger, cleaner tab text */
.header .nav .nav-item {
font-size: 18px !important; /* increase */
line-height: 1.1 !important;
padding: 10px 14px !important; /* makes it feel less “floaty” */
letter-spacing: 0.2px !important;
}
/* Optional: make current tab underline sit nicely at the bottom */
.header .nav .nav-item-current::after,
.header .nav .nav-item[aria-current="page"]::after {
bottom: -6px !important;
}
.content-bounds,
.body-content,
.page,
#page-content {
background: transparent !important;
}
/* Column containers */
.page-column, .page-columns {
background-color: transparent !important;
}
/* Links styling - purple accent */
a {
color: #e292ff !important;
}
a:hover {
color: #f0b8ff !important;
}
/* Bookmark items */
.bookmark-link {
background-color: rgba(60, 40, 80, 0.6) !important;
border-radius: 8px !important;
transition: background-color 0.2s ease !important;
}
.bookmark-link:hover {
background-color: rgba(80, 50, 110, 0.8) !important;
}
.widget-type-calendar,
.widget-type-weather,
.widget-type-to-do {
background-color: rgba(45, 31, 61, 0.55) !important;
}
/* RSS feed items */
.rss-item, .feed-item {
background-color: rgba(60, 40, 80, 0.5) !important;
border-radius: 6px !important;
margin-bottom: 4px !important;
}
/* Video thumbnails */
.video-item, .videos-item {
background-color: rgba(60, 40, 80, 0.5) !important;
border-radius: 8px !important;
}
/* Widget titles */
.widget-header, .widget-title {
color: #e292ff !important;
font-weight: 600 !important;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(45, 31, 61, 0.5);
}
::-webkit-scrollbar-thumb {
background: rgba(226, 146, 255, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(226, 146, 255, 0.7);
}
/* Fix "Show more" bar background in RSS widget */
.widget-type-rss .expand-toggle-button {
background: transparent !important;
box-shadow: none !important;
border: 0 !important;
}
/* Glance often draws that ugly bar via a pseudo-element */
.widget-type-rss .expand-toggle-button::before,
.widget-type-rss .expand-toggle-button::after,
.widget-type-rss .expand-toggle-button-icon::before,
.widget-type-rss .expand-toggle-button-icon::after {
background: transparent !important;
box-shadow: none !important;
border: 0 !important;
}
/* Optional: make it feel "intentional" instead of floating */
.widget-type-rss .expand-toggle-button {
margin-top: 8px !important;
padding: 10px 12px !important;
border-top: 1px solid rgba(255,255,255,0.08) !important;
color: rgba(255,255,255,0.75) !important;
}
.widget-type-rss .expand-toggle-button:hover {
color: rgba(255,255,255,0.95) !important;
}
/* --- Force header nav to sit at the bottom (override utility classes) --- */
.header.flex {
align-items: flex-end !important;
}
/* Make the nav fill the header height and align its items to the bottom */
.header.flex > .nav.flex {
height: 100% !important;
align-items: flex-end !important;
padding-bottom: 14px !important; /* increase/decrease to taste */
}
/* Bigger tab text */
.header.flex > .nav.flex > .nav-item {
font-size: 20px !important;
line-height: 1.1 !important;
padding: 10px 14px !important;
}
/* Tabs: bottom-align text inside full-height .nav-item */
.header.flex > .nav.flex > .nav-item {
display: flex !important;
align-items: flex-end !important;
/* keep them full-height, but move the text down */
height: 100% !important;
padding: 0 14px 18px 14px !important; /* bottom padding controls “how low” */
/* kill Glance's vertical centering */
line-height: 1 !important;
font-size: 20px !important;
}
/* Expand buttons: force transparent everywhere (RSS, Reddit, etc) */
.expand-toggle-button,
.expand-toggle-button.container-expanded {
background: transparent !important;
background-color: transparent !important;
box-shadow: none !important;
border: 0 !important;
}
/* Some themes draw the bar via pseudo elements */
.expand-toggle-button::before,
.expand-toggle-button::after,
.expand-toggle-button-icon::before,
.expand-toggle-button-icon::after {
background: transparent !important;
background-color: transparent !important;
box-shadow: none !important;
}
/* If the purple comes from a wrapper around the button, kill that too */
.widget-content > button.expand-toggle-button,
.widget-content > .expand-toggle-button,
.widget-content .expand-toggle-button {
background: transparent !important;
background-color: transparent !important;
}
/* If its "sticking" to bottom and picking up background from parent */
.expand-toggle-button.container-expanded {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
.widget.iframe-no-tint iframe {
filter: none !important;
}
.widget.iframe-no-tint::after {
content: none !important;
display: none !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)
);
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: glance-orsi
namespace: glance-system
labels:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
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-orsi
app.kubernetes.io/instance: glance-orsi
template:
metadata:
labels:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
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"
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
- name: assets
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: glance-orsi
namespace: glance-system
labels:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
spec:
type: ClusterIP
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
selector:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
---
# 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-orsi
namespace: glance-system
labels:
app.kubernetes.io/name: glance-orsi
app.kubernetes.io/instance: glance-orsi
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
external-dns.alpha.kubernetes.io/hostname: orsi.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-home-outpost' with your actual outpost name after creating in Authentik
nginx.ingress.kubernetes.io/auth-url: http://ak-outpost-glance-orsi-outpost.auth-system.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx
nginx.ingress.kubernetes.io/auth-signin: https://orsi.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: orsi.dooplex.hu
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: glance-orsi
port:
number: 8080
tls:
- hosts:
- orsi.dooplex.hu
secretName: glance-orsi-tls
---