portal: improve request status UX and jellyfin sync
This commit is contained in:
parent
8edc680503
commit
1cb12dd6c6
@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.";
|
||||
}
|
||||
|
||||
@ -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}`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user