simplifications and refactorings
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ SMTP_PORT=587
|
||||
SMTP_USERNAME=your-smtp-username
|
||||
SMTP_PASSWORD=your-smtp-password
|
||||
SMTP_FROM_ADDRESS=no-reply@example.com
|
||||
SMTP_FROM_NAME=Media Library Viewer
|
||||
SMTP_FROM_NAME=Manage
|
||||
SMTP_USE_TLS=true
|
||||
SMTP_USE_SSL=false
|
||||
SMTP_TIMEOUT=30
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Media Library Viewer
|
||||
# Manage
|
||||
|
||||
Media library management tool with Jellyfin integration, SSH file inspection, server monitoring, and safe remote job templates.
|
||||
Manage is a media and server operations tool with Jellyfin integration, SSH file inspection, server monitoring, and safe remote job templates.
|
||||
|
||||
See `docs/REQUIREMENTS.md` for the living requirements, decisions, and planning history.
|
||||
See `docs/MIGRATION_PLAN.md` for the FastAPI + React architecture plan.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Backend - Media Library Viewer API
|
||||
# Manage Backend API
|
||||
|
||||
FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.
|
||||
FastAPI backend serving the REST API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access for Manage.
|
||||
|
||||
## Project structure
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "media-library-viewer-backend"
|
||||
version = "0.1.0"
|
||||
description = "FastAPI backend for Media Library Viewer"
|
||||
description = "FastAPI backend for Manage"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"fastapi>=0.100",
|
||||
|
||||
@@ -46,7 +46,7 @@ class Settings(BaseSettings):
|
||||
smtp_username: str = ""
|
||||
smtp_password: str = ""
|
||||
smtp_from_address: str = ""
|
||||
smtp_from_name: str = "Media Library Viewer"
|
||||
smtp_from_name: str = "Manage"
|
||||
smtp_use_tls: bool = True
|
||||
smtp_use_ssl: bool = False
|
||||
smtp_timeout: int = 30
|
||||
|
||||
@@ -34,9 +34,9 @@ async def lifespan(app: FastAPI):
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Media Library Viewer API",
|
||||
title="Manage API",
|
||||
version="0.1.0",
|
||||
description="Backend API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.",
|
||||
description="Manage API for Jellyfin media browsing, SSH file inspection, server monitoring, and JWT-protected access.",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from media_library_viewer_api.dependencies import (
|
||||
get_jellyseerr_client,
|
||||
get_mail_queue,
|
||||
)
|
||||
from media_library_viewer_api.services.mailer import EmailAttachment, test_smtp_connection, validate_smtp_settings
|
||||
from media_library_viewer_api.services.mailer import EmailAttachment, validate_smtp_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -287,13 +287,6 @@ def get_user_message_status(mail_queue=Depends(get_mail_queue)) -> dict[str, Any
|
||||
return mail_queue.status()
|
||||
|
||||
|
||||
@router.post("/message/test-smtp")
|
||||
def test_user_message_smtp() -> dict[str, Any]:
|
||||
"""Test the configured SMTP connection without sending an email."""
|
||||
settings = get_settings()
|
||||
return test_smtp_connection(settings)
|
||||
|
||||
|
||||
@router.post("/message", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def post_user_message(
|
||||
recipient_ids: str = Form(...),
|
||||
|
||||
@@ -99,11 +99,6 @@ def _smtp_settings(settings: object) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def _smtp_mode_label(mode: dict[str, object]) -> str:
|
||||
transport = "SSL" if mode["use_ssl"] else "STARTTLS" if mode["use_tls"] else "plain SMTP"
|
||||
return f"{mode['smtp_host']}:{mode['smtp_port']} via {transport}"
|
||||
|
||||
|
||||
def _smtp_mode_candidates(settings: object) -> list[dict[str, Any]]:
|
||||
base = _smtp_settings(settings)
|
||||
candidates = [dict(base, mode_label="configured")]
|
||||
@@ -175,7 +170,7 @@ def _smtp_sender_not_authorized(error: Exception) -> bool:
|
||||
def _smtp_attempt_metadata(mode: dict[str, Any]) -> dict[str, Any]:
|
||||
transport = "SSL" if mode["use_ssl"] else "STARTTLS" if mode["use_tls"] else "plain SMTP"
|
||||
return {
|
||||
"label": str(mode.get("mode_label") or _smtp_mode_label(mode)),
|
||||
"label": str(mode.get("mode_label") or f"{mode["smtp_host"]}:{mode["smtp_port"]}"),
|
||||
"smtp_host": str(mode["smtp_host"]),
|
||||
"smtp_port": int(mode["smtp_port"]),
|
||||
"use_tls": bool(mode["use_tls"]),
|
||||
@@ -224,127 +219,6 @@ def describe_smtp_error(error: Exception) -> str:
|
||||
return f"SMTP delivery failed: {error}"
|
||||
|
||||
|
||||
def test_smtp_connection(settings: object) -> dict[str, object]:
|
||||
"""Validate SMTP connectivity and authentication without sending a message."""
|
||||
try:
|
||||
base = _smtp_settings(settings)
|
||||
except ValueError as exc:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(exc),
|
||||
"attempts": [],
|
||||
"selected_mode": None,
|
||||
"from_address": "",
|
||||
"from_name": str(getattr(settings, "smtp_from_name", "") or "").strip() or "Media Library Viewer",
|
||||
"smtp_host": str(getattr(settings, "smtp_host", "") or "").strip(),
|
||||
"smtp_port": int(getattr(settings, "smtp_port", 587) or 587),
|
||||
"use_tls": bool(getattr(settings, "smtp_use_tls", True)),
|
||||
"use_ssl": bool(getattr(settings, "smtp_use_ssl", False)),
|
||||
"authenticated": False,
|
||||
}
|
||||
|
||||
from_address = _from_address(settings)
|
||||
from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Media Library Viewer"
|
||||
attempts: list[dict[str, Any]] = []
|
||||
last_error = ""
|
||||
logger.info(
|
||||
"SMTP test requested from_address=%s auth_user=%s",
|
||||
from_address,
|
||||
base["smtp_username"] or "<none>",
|
||||
)
|
||||
|
||||
for mode in _smtp_mode_candidates(settings):
|
||||
meta = _smtp_attempt_metadata(mode)
|
||||
logger.info(
|
||||
"SMTP test attempting label=%s host=%s port=%s transport=%s auth_user=%s",
|
||||
meta["label"],
|
||||
meta["smtp_host"],
|
||||
meta["smtp_port"],
|
||||
meta["transport"],
|
||||
meta["auth_user"],
|
||||
)
|
||||
try:
|
||||
_probe_smtp_connection(mode)
|
||||
attempts.append(
|
||||
{
|
||||
"label": meta["label"],
|
||||
"smtp_host": meta["smtp_host"],
|
||||
"smtp_port": meta["smtp_port"],
|
||||
"use_tls": meta["use_tls"],
|
||||
"use_ssl": meta["use_ssl"],
|
||||
"status": "ok",
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
"SMTP test succeeded label=%s host=%s port=%s transport=%s auth_user=%s",
|
||||
meta["label"],
|
||||
meta["smtp_host"],
|
||||
meta["smtp_port"],
|
||||
meta["transport"],
|
||||
meta["auth_user"],
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": f"SMTP connection successful using {meta['label']}",
|
||||
"from_address": from_address,
|
||||
"from_name": from_name,
|
||||
"smtp_host": meta["smtp_host"],
|
||||
"smtp_port": meta["smtp_port"],
|
||||
"use_tls": meta["use_tls"],
|
||||
"use_ssl": meta["use_ssl"],
|
||||
"authenticated": bool(str(mode["smtp_username"])),
|
||||
"selected_mode": {
|
||||
"label": meta["label"],
|
||||
"smtp_host": meta["smtp_host"],
|
||||
"smtp_port": meta["smtp_port"],
|
||||
"use_tls": meta["use_tls"],
|
||||
"use_ssl": meta["use_ssl"],
|
||||
},
|
||||
"attempts": attempts,
|
||||
}
|
||||
except Exception as exc:
|
||||
last_error = describe_smtp_error(exc)
|
||||
attempts.append(
|
||||
{
|
||||
"label": meta["label"],
|
||||
"smtp_host": meta["smtp_host"],
|
||||
"smtp_port": meta["smtp_port"],
|
||||
"use_tls": meta["use_tls"],
|
||||
"use_ssl": meta["use_ssl"],
|
||||
"status": "failed",
|
||||
"error": last_error,
|
||||
}
|
||||
)
|
||||
logger.warning(
|
||||
"SMTP test failed label=%s host=%s port=%s transport=%s auth_user=%s error=%s",
|
||||
meta["label"],
|
||||
meta["smtp_host"],
|
||||
meta["smtp_port"],
|
||||
meta["transport"],
|
||||
meta["auth_user"],
|
||||
last_error,
|
||||
)
|
||||
|
||||
logger.error(
|
||||
"SMTP test exhausted attempts=%s error=%s",
|
||||
[attempt["label"] for attempt in attempts],
|
||||
last_error or "SMTP test failed.",
|
||||
)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": last_error or "SMTP test failed.",
|
||||
"from_address": from_address,
|
||||
"from_name": from_name,
|
||||
"smtp_host": base["smtp_host"],
|
||||
"smtp_port": base["smtp_port"],
|
||||
"use_tls": base["use_tls"],
|
||||
"use_ssl": base["use_ssl"],
|
||||
"authenticated": bool(base["smtp_username"]),
|
||||
"selected_mode": None,
|
||||
"attempts": attempts,
|
||||
}
|
||||
|
||||
|
||||
def build_email_message(
|
||||
settings: object,
|
||||
recipients: list[str],
|
||||
@@ -358,7 +232,7 @@ def build_email_message(
|
||||
) -> tuple[EmailMessage, str]:
|
||||
"""Build a MIME email message with HTML and attachments."""
|
||||
from_address = sender_address or _from_address(settings)
|
||||
from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Media Library Viewer"
|
||||
from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Manage"
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = formataddr((from_name, from_address))
|
||||
|
||||
+5
-104
@@ -132,8 +132,10 @@ def test_client(mock_jellyfin, mock_jellyseerr, mock_ssh):
|
||||
app.dependency_overrides[get_jellyseerr_client] = lambda: mock_jellyseerr
|
||||
app.dependency_overrides[get_ssh_client] = lambda: mock_ssh
|
||||
app.dependency_overrides[get_user_id] = lambda: "user123"
|
||||
client = TestClient(app)
|
||||
yield client
|
||||
auth_settings = SimpleNamespace(auth_enabled=False)
|
||||
with patch("media_library_viewer_api.auth.get_settings", return_value=auth_settings):
|
||||
client = TestClient(app)
|
||||
yield client
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@@ -245,107 +247,6 @@ class TestUsers:
|
||||
assert response.json()["state"] == "idle"
|
||||
assert response.json()["pending_count"] == 0
|
||||
|
||||
def test_users_message_test_smtp(self, test_client):
|
||||
settings = SimpleNamespace(
|
||||
smtp_host="smtp.fastmail.com",
|
||||
smtp_port=587,
|
||||
smtp_username="main@fastmail.com",
|
||||
smtp_password="app-password",
|
||||
smtp_from_address="alias@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
)
|
||||
app.dependency_overrides[get_mail_queue] = lambda: MagicMock()
|
||||
try:
|
||||
with patch("media_library_viewer_api.routers.users.get_settings", return_value=settings), patch(
|
||||
"media_library_viewer_api.routers.users.test_smtp_connection",
|
||||
return_value={
|
||||
"status": "ok",
|
||||
"message": "SMTP connection successful using Fastmail STARTTLS 587",
|
||||
"from_address": "alias@example.com",
|
||||
"from_name": "Media Library Viewer",
|
||||
"smtp_host": "smtp.fastmail.com",
|
||||
"smtp_port": 587,
|
||||
"use_tls": True,
|
||||
"use_ssl": False,
|
||||
"authenticated": True,
|
||||
"selected_mode": {
|
||||
"label": "Fastmail STARTTLS 587",
|
||||
"smtp_host": "smtp.fastmail.com",
|
||||
"smtp_port": 587,
|
||||
"use_tls": True,
|
||||
"use_ssl": False,
|
||||
},
|
||||
"attempts": [
|
||||
{
|
||||
"label": "Fastmail STARTTLS 587",
|
||||
"smtp_host": "smtp.fastmail.com",
|
||||
"smtp_port": 587,
|
||||
"use_tls": True,
|
||||
"use_ssl": False,
|
||||
"status": "ok",
|
||||
}
|
||||
],
|
||||
},
|
||||
):
|
||||
response = test_client.post("/api/users/message/test-smtp")
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_mail_queue, None)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "ok"
|
||||
assert data["smtp_host"] == "smtp.fastmail.com"
|
||||
assert data["selected_mode"]["label"] == "Fastmail STARTTLS 587"
|
||||
|
||||
def test_users_message_test_smtp_timeout_message(self, test_client):
|
||||
settings = SimpleNamespace(
|
||||
smtp_host="smtp.fastmail.com",
|
||||
smtp_port=587,
|
||||
smtp_username="main@fastmail.com",
|
||||
smtp_password="app-password",
|
||||
smtp_from_address="alias@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
)
|
||||
app.dependency_overrides[get_mail_queue] = lambda: MagicMock()
|
||||
try:
|
||||
with patch("media_library_viewer_api.routers.users.get_settings", return_value=settings), patch(
|
||||
"media_library_viewer_api.routers.users.test_smtp_connection",
|
||||
return_value={
|
||||
"status": "error",
|
||||
"message": "SMTP connection timed out while waiting for the server greeting. Check host, port, network access, and SMTP_TIMEOUT.",
|
||||
"from_address": "alias@example.com",
|
||||
"from_name": "Media Library Viewer",
|
||||
"smtp_host": "smtp.fastmail.com",
|
||||
"smtp_port": 587,
|
||||
"use_tls": True,
|
||||
"use_ssl": False,
|
||||
"authenticated": True,
|
||||
"selected_mode": None,
|
||||
"attempts": [
|
||||
{
|
||||
"label": "configured",
|
||||
"smtp_host": "smtp.fastmail.com",
|
||||
"smtp_port": 587,
|
||||
"use_tls": True,
|
||||
"use_ssl": False,
|
||||
"status": "failed",
|
||||
"error": "SMTP connection timed out while waiting for the server greeting. Check host, port, network access, and SMTP_TIMEOUT.",
|
||||
}
|
||||
],
|
||||
},
|
||||
):
|
||||
response = test_client.post("/api/users/message/test-smtp")
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_mail_queue, None)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "error"
|
||||
assert "timed out" in response.json()["message"].lower()
|
||||
|
||||
def test_users_message_is_queued(self, test_client):
|
||||
mail_queue = MagicMock()
|
||||
mail_queue.status.return_value = {
|
||||
@@ -371,7 +272,7 @@ class TestUsers:
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="mailer@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_from_name="Manage",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
|
||||
@@ -12,7 +12,6 @@ from media_library_viewer_api.services.mailer import (
|
||||
describe_smtp_error,
|
||||
html_to_text,
|
||||
send_email_message,
|
||||
test_smtp_connection as smtp_connection_probe,
|
||||
)
|
||||
|
||||
|
||||
@@ -41,7 +40,7 @@ class MailerTests(unittest.TestCase):
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="mailer@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_from_name="Manage",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
@@ -67,70 +66,9 @@ class MailerTests(unittest.TestCase):
|
||||
smtp.send_message.assert_called_once()
|
||||
message = smtp.send_message.call_args.args[0]
|
||||
self.assertEqual(message["Subject"], "Hello")
|
||||
self.assertEqual(message["From"], "Media Library Viewer <mailer@example.com>")
|
||||
self.assertEqual(message["From"], "Manage <mailer@example.com>")
|
||||
self.assertEqual(result["recipient_count"], 2)
|
||||
self.assertEqual(result["attachment_count"], 1)
|
||||
|
||||
def test_test_smtp_connection_uses_starttls_and_login(self) -> None:
|
||||
settings = SimpleNamespace(
|
||||
smtp_host="smtp.example.com",
|
||||
smtp_port=587,
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="alias@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
)
|
||||
smtp = MagicMock()
|
||||
smtp_factory = MagicMock(return_value=_SMTPContext(smtp))
|
||||
|
||||
with patch("media_library_viewer_api.services.mailer.smtplib.SMTP", smtp_factory), patch(
|
||||
"media_library_viewer_api.services.mailer.smtplib.SMTP_SSL"
|
||||
) as smtp_ssl:
|
||||
result = smtp_connection_probe(settings)
|
||||
|
||||
smtp_ssl.assert_not_called()
|
||||
smtp.ehlo.assert_called()
|
||||
smtp.starttls.assert_called_once()
|
||||
smtp.login.assert_called_once_with("mailer@example.com", "secret")
|
||||
smtp.noop.assert_called_once()
|
||||
self.assertEqual(result["status"], "ok")
|
||||
self.assertEqual(result["from_address"], "alias@example.com")
|
||||
self.assertEqual(result["smtp_host"], "smtp.example.com")
|
||||
self.assertEqual(result["selected_mode"]["label"], "configured")
|
||||
|
||||
def test_test_smtp_connection_falls_back_to_fastmail_mode(self) -> None:
|
||||
settings = SimpleNamespace(
|
||||
smtp_host="smtp.fastmail.com",
|
||||
smtp_port=465,
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="alias@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_use_tls=False,
|
||||
smtp_use_ssl=True,
|
||||
smtp_timeout=15,
|
||||
)
|
||||
smtp_ssl = MagicMock(side_effect=TimeoutError("timed out"))
|
||||
fallback_smtp = MagicMock()
|
||||
smtp_factory = MagicMock(return_value=_SMTPContext(fallback_smtp))
|
||||
|
||||
with patch("media_library_viewer_api.services.mailer.smtplib.SMTP_SSL", smtp_ssl), patch(
|
||||
"media_library_viewer_api.services.mailer.smtplib.SMTP",
|
||||
smtp_factory,
|
||||
):
|
||||
result = smtp_connection_probe(settings)
|
||||
|
||||
self.assertEqual(result["status"], "ok")
|
||||
self.assertEqual(result["selected_mode"]["label"], "Fastmail STARTTLS 587")
|
||||
self.assertEqual(len(result["attempts"]), 2)
|
||||
self.assertEqual(result["attempts"][0]["status"], "failed")
|
||||
self.assertEqual(result["attempts"][1]["status"], "ok")
|
||||
fallback_smtp.starttls.assert_called_once()
|
||||
fallback_smtp.login.assert_called_once_with("mailer@example.com", "secret")
|
||||
|
||||
def test_send_email_message_falls_back_to_fastmail_mode(self) -> None:
|
||||
settings = SimpleNamespace(
|
||||
smtp_host="smtp.fastmail.com",
|
||||
@@ -138,7 +76,7 @@ class MailerTests(unittest.TestCase):
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="alias@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_from_name="Manage",
|
||||
smtp_use_tls=False,
|
||||
smtp_use_ssl=True,
|
||||
smtp_timeout=15,
|
||||
@@ -172,7 +110,7 @@ class MailerTests(unittest.TestCase):
|
||||
smtp_username="mailer@example.com",
|
||||
smtp_password="secret",
|
||||
smtp_from_address="alias@example.com",
|
||||
smtp_from_name="Media Library Viewer",
|
||||
smtp_from_name="Manage",
|
||||
smtp_use_tls=True,
|
||||
smtp_use_ssl=False,
|
||||
smtp_timeout=15,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Media Library Viewer - Requirements and Decision Log
|
||||
# Manage - Requirements and Decision Log
|
||||
|
||||
This is a living document for the project. Update it whenever requirements, UX expectations, architecture decisions, constraints, or implementation plans change.
|
||||
|
||||
## Product Goal
|
||||
|
||||
Build a compact Streamlit application for browsing a remote Jellyfin media library and inspecting the corresponding media files on disk over SSH. The app should support library metadata review, direct file-system navigation, detailed media metadata inspection, and safe remote maintenance/job workflows.
|
||||
Build Manage, a compact web application for browsing a remote Jellyfin media library and inspecting the corresponding media files on disk over SSH. The app should support library metadata review, direct file-system navigation, detailed media metadata inspection, and safe remote maintenance/job workflows.
|
||||
|
||||
## Current Phase
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Frontend - Media Library Viewer
|
||||
# Manage Frontend
|
||||
|
||||
React + TypeScript SPA for the Media Library Viewer, consuming the FastAPI backend.
|
||||
React + TypeScript SPA for Manage, consuming the FastAPI backend.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<title>Manage</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -56,7 +56,7 @@ function Shell({
|
||||
<AppBar position="sticky" color="inherit" elevation={0}>
|
||||
<Toolbar sx={{ display: "flex", gap: 2, minHeight: 68 }}>
|
||||
<Typography variant="h6" sx={{ mr: 2, fontWeight: 700 }}>
|
||||
Media Library Viewer
|
||||
Manage
|
||||
</Typography>
|
||||
<Tabs
|
||||
value={current}
|
||||
@@ -147,7 +147,7 @@ function SignInScreen({ onSignIn }: { onSignIn: () => void }) {
|
||||
<Stack spacing={2} sx={{ alignItems: "center", textAlign: "center" }}>
|
||||
<Typography variant="h5">Sign in required</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Use your Authentik account to access the media library viewer.
|
||||
Use your Authentik account to access Manage.
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={onSignIn}>
|
||||
Sign in with OIDC
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
UserDirectoryResponse,
|
||||
UserMessageResponse,
|
||||
UserMessageQueueStatus,
|
||||
SmtpTestResponse,
|
||||
NowPlayingSession,
|
||||
MonitoringStatus,
|
||||
MonitoringMetrics,
|
||||
@@ -189,8 +188,5 @@ export const runJob = (jobKey: string, path: string) =>
|
||||
export const fetchUserMessageQueueStatus = () =>
|
||||
get<UserMessageQueueStatus>("/api/users/message/status");
|
||||
|
||||
export const testUserSmtpConnection = () =>
|
||||
post<SmtpTestResponse>("/api/users/message/test-smtp");
|
||||
|
||||
export const sendUserMessage = (formData: FormData) =>
|
||||
postForm<UserMessageResponse>("/api/users/message", formData);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { testUserSmtpConnection } from "../api/client";
|
||||
|
||||
export function useTestUserSmtp() {
|
||||
return useMutation({
|
||||
mutationFn: testUserSmtpConnection,
|
||||
});
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -43,7 +42,6 @@ import { SessionActivityPanel } from "../components/SessionActivityPanel";
|
||||
import { useUsers } from "../hooks/useUsers";
|
||||
import { useActivity } from "../hooks/useDashboard";
|
||||
import { useSendUserMessage } from "../hooks/useSendUserMessage";
|
||||
import { useTestUserSmtp } from "../hooks/useTestUserSmtp";
|
||||
import { useUserMessageQueueStatus } from "../hooks/useUserMessageQueueStatus";
|
||||
import type { UserDirectoryItem } from "../types";
|
||||
import { buildUserDrawerModel } from "../users";
|
||||
@@ -57,15 +55,13 @@ function userLabel(user: UserDirectoryItem) {
|
||||
return user.display_name || user.username || user.jellyfin_id;
|
||||
}
|
||||
|
||||
const DEFAULT_HTML_BODY =
|
||||
"<p>Hello,</p><p> </p><p>Best,<br />Media Library Viewer</p>";
|
||||
const DEFAULT_HTML_BODY = "<p>Hello,</p><p> </p><p>Best,<br />Manage</p>";
|
||||
|
||||
export function UsersPage() {
|
||||
const { data, isError, error } = useUsers();
|
||||
const { data: activity } = useActivity();
|
||||
const queueStatusQuery = useUserMessageQueueStatus();
|
||||
const sendUserMessage = useSendUserMessage();
|
||||
const testUserSmtp = useTestUserSmtp();
|
||||
const [search, setSearch] = useState("");
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||
@@ -236,7 +232,7 @@ export function UsersPage() {
|
||||
sendUserMessage.reset();
|
||||
if (!subject.trim()) {
|
||||
setSubject(
|
||||
`Media Library Viewer update for ${selectedDeliverableRows.length} user${selectedDeliverableRows.length === 1 ? "" : "s"}`,
|
||||
`Manage update for ${selectedDeliverableRows.length} user${selectedDeliverableRows.length === 1 ? "" : "s"}`,
|
||||
);
|
||||
}
|
||||
if (!htmlBody.trim()) {
|
||||
@@ -389,76 +385,6 @@ export function UsersPage() {
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{testUserSmtp.isPending ? (
|
||||
<Alert severity="info" variant="outlined">
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<CircularProgress size={16} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
Testing SMTP connection...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : testUserSmtp.isSuccess ? (
|
||||
testUserSmtp.data.status === "ok" ? (
|
||||
<Alert severity="success" variant="outlined">
|
||||
SMTP connection succeeded: {testUserSmtp.data.smtp_host}:
|
||||
{testUserSmtp.data.smtp_port} using{" "}
|
||||
{testUserSmtp.data.use_ssl
|
||||
? "SSL"
|
||||
: testUserSmtp.data.use_tls
|
||||
? "STARTTLS"
|
||||
: "plain SMTP"}
|
||||
.
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
>
|
||||
{testUserSmtp.data.message}
|
||||
</Typography>
|
||||
{testUserSmtp.data.selected_mode ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
>
|
||||
Selected mode: {testUserSmtp.data.selected_mode.label}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="warning" variant="outlined">
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{testUserSmtp.data.message}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
color="text.secondary"
|
||||
>
|
||||
Tried {testUserSmtp.data.attempts.length} mode
|
||||
{testUserSmtp.data.attempts.length === 1 ? "" : "s"}.
|
||||
</Typography>
|
||||
<Stack spacing={0.5} sx={{ mt: 1 }}>
|
||||
{testUserSmtp.data.attempts.map((attempt) => (
|
||||
<Typography
|
||||
key={`${attempt.label}-${attempt.smtp_port}`}
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
>
|
||||
{attempt.label}: {attempt.error || "failed"}
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
</Alert>
|
||||
)
|
||||
) : testUserSmtp.isError ? (
|
||||
<Alert severity="error" variant="outlined">
|
||||
SMTP test failed:{" "}
|
||||
{(testUserSmtp.error as Error)?.message || "Unknown error"}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
@@ -520,20 +446,6 @@ export function UsersPage() {
|
||||
>
|
||||
Message selected
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={
|
||||
testUserSmtp.isPending ? (
|
||||
<CircularProgress size={16} color="inherit" />
|
||||
) : (
|
||||
<SendIcon />
|
||||
)
|
||||
}
|
||||
variant="outlined"
|
||||
disabled={testUserSmtp.isPending}
|
||||
onClick={() => testUserSmtp.mutate()}
|
||||
>
|
||||
{testUserSmtp.isPending ? "Testing SMTP..." : "Test SMTP"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
disabled={!selectedRows.length}
|
||||
|
||||
@@ -78,38 +78,6 @@ export interface UserMessageQueueStatus {
|
||||
failed_count: number;
|
||||
}
|
||||
|
||||
export interface SmtpTestAttempt {
|
||||
label: string;
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
use_tls: boolean;
|
||||
use_ssl: boolean;
|
||||
status: "ok" | "failed";
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SmtpTestSelectedMode {
|
||||
label: string;
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
use_tls: boolean;
|
||||
use_ssl: boolean;
|
||||
}
|
||||
|
||||
export interface SmtpTestResponse {
|
||||
status: "ok" | "error";
|
||||
message: string;
|
||||
from_address: string;
|
||||
from_name: string;
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
use_tls: boolean;
|
||||
use_ssl: boolean;
|
||||
authenticated: boolean;
|
||||
selected_mode: SmtpTestSelectedMode | null;
|
||||
attempts: SmtpTestAttempt[];
|
||||
}
|
||||
|
||||
export interface NowPlayingSession {
|
||||
user: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user