Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ jobs:
test -f css/styles.css
test -f js/app.js
test -f img/505675340-c40b22c9-33da-47b7-bc4c-ce69bb5cc174.png
test -f img/muse-logo.png
test -f CNAME
grep -qx 'adiker.eu' CNAME

node --check js/app.js
python3 -m unittest discover -s ops/muse-status -p 'test_*.py'

python3 -m http.server 18080 --bind 127.0.0.1 > /tmp/adiker-http.log 2>&1 &
server_pid="$!"
Expand All @@ -62,6 +64,7 @@ jobs:
curl --fail --silent --output /dev/null http://127.0.0.1:18080/css/styles.css
curl --fail --silent --output /dev/null http://127.0.0.1:18080/js/app.js
curl --fail --silent --output /dev/null http://127.0.0.1:18080/img/505675340-c40b22c9-33da-47b7-bc4c-ce69bb5cc174.png
curl --fail --silent --output /dev/null http://127.0.0.1:18080/img/muse-logo.png

deploy:
name: Deploy to GitHub Pages
Expand Down
85 changes: 77 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ This repository contains a static single-page application used as a front page f
- **FileBrowser Quantum**
- **autobrr**
- **OpenSpeedTest**
- **Muse** (Discord music bot)

The page is intentionally framework-free: plain HTML, CSS, and vanilla JavaScript.
That keeps it fast, portable, and easy to host on GitHub Pages or any static
Expand All @@ -49,6 +50,7 @@ server.
- Live service dashboard with online count, last check time, and latency labels
- Browser-local recent status history with mini timelines and average latency
- Health checks for all configured services
- Local Muse readiness probe with Discord gateway connectivity check
- Polling with warm-up measurement and failure backoff
- EN/PL language switcher
- Dark / Light / OLED themes
Expand All @@ -66,12 +68,15 @@ server.
├── css/
│ └── styles.css # Styling, themes, responsive rules
├── img/
│ └── 505675340-c40b22c9-33da-47b7-bc4c-ce69bb5cc174.png
│ # Local FileBrowser Quantum logo asset
│ ├── 505675340-c40b22c9-33da-47b7-bc4c-ce69bb5cc174.png
│ │ # Local FileBrowser Quantum logo asset
│ └── muse-logo.png # Local Muse logo asset
├── index.html # Main page markup
├── index_old.html # Legacy page backup
└── js/
└── app.js # UI logic, i18n, status polling
├── js/
│ └── app.js # UI logic, i18n, status polling
└── ops/
└── muse-status/ # Host probe, systemd/Caddy templates, and tests
```

### Run Locally
Expand All @@ -92,6 +97,36 @@ Service endpoints are defined in `js/app.js`:
- `https://files.adiker.eu/health`
- `https://autobrr.adiker.eu/api/healthz/liveness`
- `https://speedtest.adiker.eu/health`
- `https://muse.adiker.eu/health`

The Muse endpoint returns `200 {"status":"online"}` only when the local
container is running, has logged `Ready!` after its current start, and has an
established TCP connection to `gateway.discord.gg`. Otherwise it returns
`503 {"status":"offline"}`. It intentionally publishes no Discord, channel,
track, container, or failure details.

The local logo is copied from the [official Muse repository](https://github.com/museofficial/muse/blob/master/.github/logo.png).

The local bridge is installed from `ops/muse-status/`. It listens only on a
Unix socket and requires the `adiker` account to be a member of the Docker
group:

```bash
sudo install -Dm755 ops/muse-status/muse_status.py /usr/local/lib/muse-status/muse_status.py
sudo install -Dm644 ops/muse-status/muse-status.service /etc/systemd/system/muse-status.service
# Back up an existing Caddy snippet before replacing it.
sudo install -Dm644 ops/muse-status/Caddyfile /etc/caddy/conf.d/muse-status.caddy
sudo systemctl daemon-reload
sudo systemctl enable --now muse-status.service
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Run the probe tests with:

```bash
python3 -m unittest discover -s ops/muse-status -p 'test_*.py'
```

User preferences are stored in `localStorage`:

Expand Down Expand Up @@ -167,6 +202,7 @@ główna dla:
- **FileBrowser Quantum**
- **autobrr**
- **OpenSpeedTest**
- **Muse** (bot muzyczny Discorda)

Strona celowo nie używa frameworków: to czysty HTML, CSS i vanilla JavaScript.
Dzięki temu pozostaje szybka, przenośna i łatwa w hostowaniu na GitHub Pages
Expand All @@ -178,6 +214,7 @@ albo dowolnym serwerze statycznym.
- Dashboard statusów z liczbą usług online, czasem ostatniego sprawdzenia i opóźnieniami
- Lokalna historia ostatnich statusów z mini-timeline i średnim opóźnieniem
- Health checki dla wszystkich skonfigurowanych usług
- Lokalna sonda gotowości Muse ze sprawdzeniem połączenia z gateway Discorda
- Odpytywanie z pomiarem rozgrzewkowym i backoffem przy błędach
- Przełącznik języka EN/PL
- Motywy Dark / Light / OLED
Expand All @@ -195,12 +232,15 @@ albo dowolnym serwerze statycznym.
├── css/
│ └── styles.css # Style, motywy, reguły responsywne
├── img/
│ └── 505675340-c40b22c9-33da-47b7-bc4c-ce69bb5cc174.png
│ # Lokalny zasób logo FileBrowser Quantum
│ ├── 505675340-c40b22c9-33da-47b7-bc4c-ce69bb5cc174.png
│ │ # Lokalny zasób logo FileBrowser Quantum
│ └── muse-logo.png # Lokalny zasób logo Muse
├── index.html # Główny markup strony
├── index_old.html # Kopia starszej wersji strony
└── js/
└── app.js # Logika UI, i18n, odpytywanie statusów
├── js/
│ └── app.js # Logika UI, i18n, odpytywanie statusów
└── ops/
└── muse-status/ # Sonda hosta, szablony systemd/Caddy i testy
```

### Uruchom lokalnie
Expand All @@ -221,6 +261,35 @@ Endpointy usług są zdefiniowane w `js/app.js`:
- `https://files.adiker.eu/health`
- `https://autobrr.adiker.eu/api/healthz/liveness`
- `https://speedtest.adiker.eu/health`
- `https://muse.adiker.eu/health`

Endpoint Muse zwraca `200 {"status":"online"}` tylko wtedy, gdy lokalny
kontener działa, po bieżącym uruchomieniu zalogował `Ready!` i ma aktywne
połączenie TCP z `gateway.discord.gg`. W przeciwnym razie zwraca
`503 {"status":"offline"}`. Publiczna odpowiedź nie ujawnia serwera Discord,
kanału, utworu, kontenera ani przyczyny awarii.

Lokalne logo pochodzi z [oficjalnego repozytorium Muse](https://github.com/museofficial/muse/blob/master/.github/logo.png).

Lokalny most instaluje się z `ops/muse-status/`. Nasłuchuje wyłącznie na
sockecie Unix, a konto `adiker` musi należeć do grupy `docker`:

```bash
sudo install -Dm755 ops/muse-status/muse_status.py /usr/local/lib/muse-status/muse_status.py
sudo install -Dm644 ops/muse-status/muse-status.service /etc/systemd/system/muse-status.service
# Przed zastąpieniem istniejącego snippetu Caddy wykonaj jego kopię zapasową.
sudo install -Dm644 ops/muse-status/Caddyfile /etc/caddy/conf.d/muse-status.caddy
sudo systemctl daemon-reload
sudo systemctl enable --now muse-status.service
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```

Testy sondy uruchomisz poleceniem:

```bash
python3 -m unittest discover -s ops/muse-status -p 'test_*.py'
```

Preferencje użytkownika zapisywane są w `localStorage`:

Expand Down
Binary file added img/muse-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 36 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>adiker.eu — Homepage</title>
<meta content="Adiker • Jellyfin" name="description" />
<meta content="Adiker • Self-hosted services" name="description" />
<meta content="#0b0f14" name="theme-color" />
<meta content="adiker.eu" property="og:title" />
<meta content="Jellyfin access and utilities" property="og:description" />
<meta content="Self-hosted services and live status" property="og:description" />
<meta content="website" property="og:type" />
<link
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230b0f14'/%3E%3Ccircle cx='22' cy='22' r='8' fill='%230ea5e9'/%3E%3Cpath d='M38 18l16 14-16 14z' fill='%237cc4ff'/%3E%3C/svg%3E"
Expand Down Expand Up @@ -40,6 +40,8 @@ <h1 id="title">adiker.eu</h1>
type="button">Autobrr</button>
<button aria-controls="section-st" aria-selected="false" class="tab" id="tab-speedtest" role="tab"
type="button">OpenSpeedTest</button>
<button aria-controls="section-muse" aria-selected="false" class="tab" id="tab-muse" role="tab"
type="button">Muse</button>
<button class="tab" id="tab-more" type="button">More services (soon)</button>
</nav>
<div class="controls">
Expand All @@ -60,12 +62,13 @@ <h1 id="title">adiker.eu</h1>
<h2 id="dash-title" style="margin:0 0 6px">📊 Service dashboard</h2>
<p class="tag" id="dash-sub" style="margin:0 0 12px">Live status overview</p>
<div class="dashboard-grid">
<span class="pill" id="dash-overall" aria-live="polite">0/4 online</span>
<span class="pill" id="dash-overall" aria-live="polite">0/5 online</span>
<span class="pill" id="dash-lastcheck" aria-live="polite">Last check: --</span>
<span class="pill" id="dash-jf-latency">Jellyfin: -- ms</span>
<span class="pill" id="dash-fb-latency">FileBrowser: -- ms</span>
<span class="pill" id="dash-ab-latency">autobrr: -- ms</span>
<span class="pill" id="dash-st-latency">OpenSpeedTest: -- ms</span>
<span class="pill" id="dash-mu-latency">Muse: -- ms</span>
</div>
<div class="status-history" id="status-history">
<h3 id="history-title">Recent checks</h3>
Expand Down Expand Up @@ -106,6 +109,15 @@ <h3 id="history-title">Recent checks</h3>
</div>
<span class="history-avg" id="history-st-avg">Avg: -- ms</span>
</div>
<div class="history-row">
<div class="history-meta">
<span class="history-name" id="history-mu-name">Muse</span>
<span class="history-summary" id="history-mu-summary">No data yet</span>
</div>
<div class="history-line" id="history-mu-line" role="img" aria-label="Muse: no data yet">
</div>
<span class="history-avg" id="history-mu-avg">Avg: -- ms</span>
</div>
</div>
</div>
</section>
Expand Down Expand Up @@ -226,6 +238,25 @@ <h2 id="st-title" style="margin:0 0 6px">🚀 OpenSpeedTest</h2>
</div>
</div>
</section>
<section class="card service-section" id="section-muse" role="tabpanel" aria-labelledby="tab-muse" hidden>
<div class="j-wrap">
<div class="j-left">
<div aria-hidden="true" class="j-icon">
<img alt="Muse logo" src="img/muse-logo.png" />
</div>
<div>
<h2 id="mu-title" style="margin:0 0 6px">🎧 Muse</h2>
<p class="tag" id="mu-sub" style="margin:0">Discord music bot</p>
<div class="status-wrap" style="margin-top:8px">
<span class="pill" id="mu-pill">
<span class="dot" id="mu-dot" style="background:var(--bad)"></span>
<span id="mu-text">Offline</span>
</span>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<span class="brand-year">© <span id="year"></span> adiker.eu •</span>
Expand All @@ -240,8 +271,8 @@ <h2 id="st-title" style="margin:0 0 6px">🚀 OpenSpeedTest</h2>
</footer>
</div>
<div aria-atomic="true" aria-live="polite" class="toast" id="toast"></div>
<link href="css/styles.css?v=20260621-review-fixes" rel="stylesheet" />
<script src="js/app.js?v=20260621-review-fixes"></script>
<link href="css/styles.css?v=20260913-muse-status" rel="stylesheet" />
<script src="js/app.js?v=20260913-muse-status"></script>
<script src="https://keepandroidopen.org/banner.js?size=mini"></script>
</body>

Expand Down
19 changes: 15 additions & 4 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ const SERVICE_STATE = {
jf: { online: false, latency: null, failCount: 0, nextDelay: 5000, warmedUp: false, latencySamples: [], history: [] },
fb: { online: false, latency: null, failCount: 0, nextDelay: 5000, warmedUp: false, latencySamples: [], history: [] },
ab: { online: false, latency: null, failCount: 0, nextDelay: 5000, warmedUp: false, latencySamples: [], history: [] },
st: { online: false, latency: null, failCount: 0, nextDelay: 5000, warmedUp: false, latencySamples: [], history: [] }
st: { online: false, latency: null, failCount: 0, nextDelay: 5000, warmedUp: false, latencySamples: [], history: [] },
mu: { online: false, latency: null, failCount: 0, nextDelay: 5000, warmedUp: false, latencySamples: [], history: [] }
};

const SERVICES = [
{ key: 'jf', url: 'https://jellyfin.adiker.eu/health' },
{ key: 'fb', url: 'https://files.adiker.eu/health' },
{ key: 'ab', url: 'https://autobrr.adiker.eu/api/healthz/liveness' },
{ key: 'st', url: 'https://speedtest.adiker.eu/health' }
{ key: 'st', url: 'https://speedtest.adiker.eu/health' },
{ key: 'mu', url: 'https://muse.adiker.eu/health' }
];

const STR = {
Expand All @@ -33,6 +35,7 @@ const STR = {
filebrowser: 'Filebrowser',
autobrr: 'Autobrr',
openspeedtest: 'OpenSpeedTest',
muse: 'Muse',
more: 'More services (soon)'
},
subtitle: 'My Playground',
Expand Down Expand Up @@ -63,6 +66,7 @@ const STR = {
fb: { title: 'FileBrowser Quantum', sub: 'Your file manager', open: 'Open FileBrowser Quantum', short: 'FileBrowser' },
ab: { title: 'autobrr', sub: 'Automated torrent management', open: 'Open autobrr', short: 'autobrr' },
st: { title: 'OpenSpeedTest', sub: 'Network speed test', open: 'Open OpenSpeedTest', short: 'OpenSpeedTest' },
mu: { title: 'Muse', sub: 'Discord music bot', short: 'Muse' },
status: { online: 'Online', offline: 'Offline' },
pc: {
on: 'My PC is on :)',
Expand Down Expand Up @@ -109,6 +113,7 @@ const STR = {
filebrowser: 'Filebrowser',
autobrr: 'Autobrr',
openspeedtest: 'OpenSpeedTest',
muse: 'Muse',
more: 'Więcej usług (wkrótce)'
},
subtitle: 'Mój plac zabaw',
Expand Down Expand Up @@ -139,6 +144,7 @@ const STR = {
fb: { title: 'FileBrowser Quantum', sub: 'Twój menedżer plików', open: 'Otwórz FileBrowsera Quantum', short: 'FileBrowser' },
ab: { title: 'autobrr', sub: 'Automatyzacja torrentów', open: 'Otwórz autobrr', short: 'autobrr' },
st: { title: 'OpenSpeedTest', sub: 'Test prędkości sieci', open: 'Otwórz OpenSpeedTest', short: 'OpenSpeedTest' },
mu: { title: 'Muse', sub: 'Bot muzyczny Discorda', short: 'Muse' },
status: { online: 'Online', offline: 'Offline' },
pc: {
on: 'Mój PC jest włączony :)',
Expand Down Expand Up @@ -480,6 +486,7 @@ function renderDashboard(lang = getLang()) {
setText('dash-fb-latency', `${L.fb.short}: ${SERVICE_STATE.fb.latency ?? '--'} ms`);
setText('dash-ab-latency', `${L.ab.short}: ${SERVICE_STATE.ab.latency ?? '--'} ms`);
setText('dash-st-latency', `${L.st.short}: ${SERVICE_STATE.st.latency ?? '--'} ms`);
setText('dash-mu-latency', `${L.mu.short}: ${SERVICE_STATE.mu.latency ?? '--'} ms`);
setText('history-title', L.dashboard.historyTitle);

SERVICES.forEach((service) => renderHistoryTimeline(service.key, lang, L));
Expand All @@ -501,6 +508,8 @@ function applyLang(lang) {
setText('st-title', '🚀 ' + L.st.title);
setText('st-sub', L.st.sub);
setText('st-btn-text', L.st.open);
setText('mu-title', '🎧 ' + L.mu.title);
setText('mu-sub', L.mu.sub);

setText('subtitle', L.subtitle);
setText('availability', L.availability);
Expand All @@ -519,6 +528,7 @@ function applyLang(lang) {
setText('tab-filebrowser', L.nav.filebrowser);
setText('tab-autobrr', L.nav.autobrr);
setText('tab-speedtest', L.nav.openspeedtest);
setText('tab-muse', L.nav.muse);
setText('tab-more', L.nav.more);

const tabsNav = document.querySelector('nav.tabs');
Expand Down Expand Up @@ -650,12 +660,13 @@ async function refreshService(service) {
}

function initTabs() {
const tabIds = ['tab-jellyfin', 'tab-filebrowser', 'tab-autobrr', 'tab-speedtest'];
const tabIds = ['tab-jellyfin', 'tab-filebrowser', 'tab-autobrr', 'tab-speedtest', 'tab-muse'];
const map = {
'tab-jellyfin': 'section-jf',
'tab-filebrowser': 'section-fb',
'tab-autobrr': 'section-ab',
'tab-speedtest': 'section-st'
'tab-speedtest': 'section-st',
'tab-muse': 'section-muse'
};

const tabs = tabIds.map((id) => document.getElementById(id)).filter(Boolean);
Expand Down
40 changes: 40 additions & 0 deletions ops/muse-status/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
muse.adiker.eu {
@health-options {
path /health
method OPTIONS
}

handle @health-options {
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Access-Control-Allow-Headers "Content-Type"
Cache-Control "no-store"
}
respond "" 204
}

@health path /health
handle @health {
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Access-Control-Allow-Headers "Content-Type"
Cache-Control "no-store"
}
reverse_proxy unix//run/muse-status/status.sock
}

respond 404

handle_errors {
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Access-Control-Allow-Headers "Content-Type"
Cache-Control "no-store"
Content-Type "application/json; charset=utf-8"
}
respond "{\"status\":\"offline\"}" 503
}
}
Loading