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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import socket
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -11,6 +12,16 @@ from ..keycloak import admin_client, require_auth, require_account_access
|
|||||||
from ..utils import random_password
|
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:
|
def register(app) -> None:
|
||||||
@app.route("/api/account/overview", methods=["GET"])
|
@app.route("/api/account/overview", methods=["GET"])
|
||||||
@require_auth
|
@require_auth
|
||||||
@ -24,10 +35,14 @@ def register(app) -> None:
|
|||||||
mailu_app_password = ""
|
mailu_app_password = ""
|
||||||
mailu_status = "ready"
|
mailu_status = "ready"
|
||||||
jellyfin_status = "ready"
|
jellyfin_status = "ready"
|
||||||
|
jellyfin_sync_status = "unknown"
|
||||||
|
jellyfin_sync_detail = ""
|
||||||
|
|
||||||
if not admin_client().ready():
|
if not admin_client().ready():
|
||||||
mailu_status = "server not configured"
|
mailu_status = "server not configured"
|
||||||
jellyfin_status = "server not configured"
|
jellyfin_status = "server not configured"
|
||||||
|
jellyfin_sync_status = "unknown"
|
||||||
|
jellyfin_sync_detail = "keycloak admin not configured"
|
||||||
elif username:
|
elif username:
|
||||||
try:
|
try:
|
||||||
user = admin_client().find_user(username) or {}
|
user = admin_client().find_user(username) or {}
|
||||||
@ -58,6 +73,8 @@ def register(app) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
mailu_status = "keycloak admin error"
|
mailu_status = "keycloak admin error"
|
||||||
jellyfin_status = "keycloak admin error"
|
jellyfin_status = "keycloak admin error"
|
||||||
|
jellyfin_sync_status = "unknown"
|
||||||
|
jellyfin_sync_detail = "keycloak admin error"
|
||||||
|
|
||||||
mailu_username = ""
|
mailu_username = ""
|
||||||
if keycloak_email and keycloak_email.lower().endswith(f"@{settings.MAILU_DOMAIN.lower()}"):
|
if keycloak_email and keycloak_email.lower().endswith(f"@{settings.MAILU_DOMAIN.lower()}"):
|
||||||
@ -65,11 +82,28 @@ def register(app) -> None:
|
|||||||
elif username:
|
elif username:
|
||||||
mailu_username = f"{username}@{settings.MAILU_DOMAIN}"
|
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(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"user": {"username": username, "email": keycloak_email, "groups": g.keycloak_groups},
|
"user": {"username": username, "email": keycloak_email, "groups": g.keycloak_groups},
|
||||||
"mailu": {"status": mailu_status, "username": mailu_username, "app_password": mailu_app_password},
|
"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("/")
|
).rstrip("/")
|
||||||
|
|
||||||
JELLYFIN_SYNC_URL = os.getenv("JELLYFIN_SYNC_URL", "").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;
|
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 {
|
.card {
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|||||||
@ -85,7 +85,18 @@
|
|||||||
<div class="card module">
|
<div class="card module">
|
||||||
<div class="module-head">
|
<div class="module-head">
|
||||||
<h2>Jellyfin</h2>
|
<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>
|
</div>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Jellyfin authentication is backed by LDAP (Keycloak is the source of truth). Use your Keycloak username and
|
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>
|
<span class="v mono">{{ jellyfin.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="jellyfin.syncDetail" class="hint mono">{{ jellyfin.syncDetail }}</div>
|
||||||
<div v-if="jellyfin.error" class="error-box">
|
<div v-if="jellyfin.error" class="error-box">
|
||||||
<div class="mono">{{ jellyfin.error }}</div>
|
<div class="mono">{{ jellyfin.error }}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -173,6 +185,8 @@ const mailu = reactive({
|
|||||||
const jellyfin = reactive({
|
const jellyfin = reactive({
|
||||||
status: "loading",
|
status: "loading",
|
||||||
username: "",
|
username: "",
|
||||||
|
syncStatus: "",
|
||||||
|
syncDetail: "",
|
||||||
error: "",
|
error: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -230,9 +244,13 @@ async function refreshOverview() {
|
|||||||
mailu.currentPassword = data.mailu?.app_password || "";
|
mailu.currentPassword = data.mailu?.app_password || "";
|
||||||
jellyfin.status = data.jellyfin?.status || "ready";
|
jellyfin.status = data.jellyfin?.status || "ready";
|
||||||
jellyfin.username = data.jellyfin?.username || auth.username;
|
jellyfin.username = data.jellyfin?.username || auth.username;
|
||||||
|
jellyfin.syncStatus = data.jellyfin?.sync_status || "";
|
||||||
|
jellyfin.syncDetail = data.jellyfin?.sync_detail || "";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
mailu.status = "unavailable";
|
mailu.status = "unavailable";
|
||||||
jellyfin.status = "unavailable";
|
jellyfin.status = "unavailable";
|
||||||
|
jellyfin.syncStatus = "";
|
||||||
|
jellyfin.syncDetail = "";
|
||||||
mailu.error = "Failed to load account status.";
|
mailu.error = "Failed to load account status.";
|
||||||
jellyfin.error = "Failed to load account status.";
|
jellyfin.error = "Failed to load account status.";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -196,13 +196,21 @@ async function copyRequestCode() {
|
|||||||
async function checkStatus() {
|
async function checkStatus() {
|
||||||
if (checking.value) return;
|
if (checking.value) return;
|
||||||
error.value = "";
|
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;
|
checking.value = true;
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/access/request/status", {
|
const resp = await fetch("/api/access/request/status", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
body: JSON.stringify({ request_code: statusForm.request_code.trim() }),
|
body: JSON.stringify({ request_code: trimmed }),
|
||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
if (!resp.ok) throw new Error(data.error || resp.statusText || `status ${resp.status}`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user