portal: improve request status UX and jellyfin sync

This commit is contained in:
Brad Stein 2026-01-02 04:27:44 -03:00
parent 8edc680503
commit 1cb12dd6c6
5 changed files with 76 additions and 3 deletions

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import socket
import time
from typing import Any
@ -11,6 +12,16 @@ from ..keycloak import admin_client, require_auth, require_account_access
from ..utils import random_password
def _tcp_check(host: str, port: int, timeout_sec: float) -> bool:
if not host or port <= 0:
return False
try:
with socket.create_connection((host, port), timeout=timeout_sec):
return True
except OSError:
return False
def register(app) -> None:
@app.route("/api/account/overview", methods=["GET"])
@require_auth
@ -24,10 +35,14 @@ def register(app) -> None:
mailu_app_password = ""
mailu_status = "ready"
jellyfin_status = "ready"
jellyfin_sync_status = "unknown"
jellyfin_sync_detail = ""
if not admin_client().ready():
mailu_status = "server not configured"
jellyfin_status = "server not configured"
jellyfin_sync_status = "unknown"
jellyfin_sync_detail = "keycloak admin not configured"
elif username:
try:
user = admin_client().find_user(username) or {}
@ -58,6 +73,8 @@ def register(app) -> None:
except Exception:
mailu_status = "keycloak admin error"
jellyfin_status = "keycloak admin error"
jellyfin_sync_status = "unknown"
jellyfin_sync_detail = "keycloak admin error"
mailu_username = ""
if keycloak_email and keycloak_email.lower().endswith(f"@{settings.MAILU_DOMAIN.lower()}"):
@ -65,11 +82,28 @@ def register(app) -> None:
elif username:
mailu_username = f"{username}@{settings.MAILU_DOMAIN}"
if jellyfin_status == "ready":
if _tcp_check(
settings.JELLYFIN_LDAP_HOST,
settings.JELLYFIN_LDAP_PORT,
settings.JELLYFIN_LDAP_CHECK_TIMEOUT_SEC,
):
jellyfin_sync_status = "ok"
jellyfin_sync_detail = "LDAP reachable"
else:
jellyfin_sync_status = "degraded"
jellyfin_sync_detail = "LDAP unreachable"
return jsonify(
{
"user": {"username": username, "email": keycloak_email, "groups": g.keycloak_groups},
"mailu": {"status": mailu_status, "username": mailu_username, "app_password": mailu_app_password},
"jellyfin": {"status": jellyfin_status, "username": username},
"jellyfin": {
"status": jellyfin_status,
"username": username,
"sync_status": jellyfin_sync_status,
"sync_detail": jellyfin_sync_detail,
},
}
)

View File

@ -77,3 +77,6 @@ MAILU_SYNC_URL = os.getenv(
).rstrip("/")
JELLYFIN_SYNC_URL = os.getenv("JELLYFIN_SYNC_URL", "").rstrip("/")
JELLYFIN_LDAP_HOST = os.getenv("JELLYFIN_LDAP_HOST", "openldap.sso.svc.cluster.local").strip()
JELLYFIN_LDAP_PORT = int(os.getenv("JELLYFIN_LDAP_PORT", "389"))
JELLYFIN_LDAP_CHECK_TIMEOUT_SEC = float(os.getenv("JELLYFIN_LDAP_CHECK_TIMEOUT_SEC", "1"))

View File

@ -29,6 +29,16 @@
font-size: 13px;
}
.pill-ok {
border-color: rgba(120, 255, 160, 0.35);
color: rgba(170, 255, 215, 0.92);
}
.pill-warn {
border-color: rgba(255, 220, 120, 0.35);
color: rgba(255, 230, 170, 0.92);
}
.card {
background: var(--bg-panel);
border: 1px solid var(--border);

View File

@ -85,7 +85,18 @@
<div class="card module">
<div class="module-head">
<h2>Jellyfin</h2>
<span class="pill mono">{{ jellyfin.status }}</span>
<span
class="pill mono"
:class="jellyfin.syncStatus === 'ok' ? 'pill-ok' : jellyfin.syncStatus === 'degraded' ? 'pill-warn' : ''"
>
{{
jellyfin.syncStatus === "ok"
? "in sync"
: jellyfin.syncStatus === "degraded"
? "check sync"
: jellyfin.status
}}
</span>
</div>
<p class="muted">
Jellyfin authentication is backed by LDAP (Keycloak is the source of truth). Use your Keycloak username and
@ -101,6 +112,7 @@
<span class="v mono">{{ jellyfin.username }}</span>
</div>
</div>
<div v-if="jellyfin.syncDetail" class="hint mono">{{ jellyfin.syncDetail }}</div>
<div v-if="jellyfin.error" class="error-box">
<div class="mono">{{ jellyfin.error }}</div>
</div>
@ -173,6 +185,8 @@ const mailu = reactive({
const jellyfin = reactive({
status: "loading",
username: "",
syncStatus: "",
syncDetail: "",
error: "",
});
@ -230,9 +244,13 @@ async function refreshOverview() {
mailu.currentPassword = data.mailu?.app_password || "";
jellyfin.status = data.jellyfin?.status || "ready";
jellyfin.username = data.jellyfin?.username || auth.username;
jellyfin.syncStatus = data.jellyfin?.sync_status || "";
jellyfin.syncDetail = data.jellyfin?.sync_detail || "";
} catch (err) {
mailu.status = "unavailable";
jellyfin.status = "unavailable";
jellyfin.syncStatus = "";
jellyfin.syncDetail = "";
mailu.error = "Failed to load account status.";
jellyfin.error = "Failed to load account status.";
}

View File

@ -196,13 +196,21 @@ async function copyRequestCode() {
async function checkStatus() {
if (checking.value) return;
error.value = "";
const trimmed = statusForm.request_code.trim();
if (!trimmed) return;
if (!trimmed.includes("~")) {
error.value = "Request code should look like username~XXXXXXXXXX. Copy it from the submit step.";
status.value = "unknown";
onboardingUrl.value = "";
return;
}
checking.value = true;
try {
const resp = await fetch("/api/access/request/status", {
method: "POST",
headers: { "Content-Type": "application/json" },
cache: "no-store",
body: JSON.stringify({ request_code: statusForm.request_code.trim() }),
body: JSON.stringify({ request_code: trimmed }),
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);