fixes and improvements
This commit is contained in:
+2
-8
@@ -15,14 +15,8 @@ SMTP_TIMEOUT=30
|
||||
# Machine/service configuration now lives in the app's Settings tab.
|
||||
# The built-in local machine is seeded automatically.
|
||||
#
|
||||
# For a remote SSH machine, the backend still needs access to a private key
|
||||
# file. Compose mounts a single host key file as a Docker secret instead of the
|
||||
# whole ~/.ssh directory.
|
||||
#
|
||||
# Host-side path used by Docker Compose secret definitions:
|
||||
SSH_KEY_HOST_PATH=/absolute/path/to/id_ed25519
|
||||
# The container-side path is fixed by the app and Compose at:
|
||||
# /run/secrets/ssh_private_key
|
||||
# Remote SSH machines can store their private key and optional passphrase directly in Settings,
|
||||
# so no SSH key mount is required for normal use.
|
||||
|
||||
# For deployment with traefik
|
||||
FRONTEND_APP_NAME=manage
|
||||
|
||||
@@ -84,20 +84,19 @@ export VITE_OIDC_CLIENT_ID=manage
|
||||
export VITE_OIDC_REDIRECT_URI=https://manage.example.com/
|
||||
export VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/
|
||||
|
||||
# Optional: provide a single SSH private key file for remote machines configured later in Settings.
|
||||
# Compose mounts it as a Docker secret, so you do not need to mount your whole ~/.ssh directory.
|
||||
# If you prefer SSH agent forwarding, Paramiko will use SSH_AUTH_SOCK when you provide it.
|
||||
export SSH_KEY_HOST_PATH=$HOME/.ssh/id_ed25519
|
||||
# The container-side secret path is fixed at /run/secrets/ssh_private_key.
|
||||
# In the app's SSH machine settings, use /run/secrets as the key directory and ssh_private_key as the key name.
|
||||
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Inline one-liner example:
|
||||
|
||||
```bash
|
||||
BACKEND_APP_HOST=manage.example.com FRONTEND_APP_HOST=manage.example.com CERT_RESOLVER=letsencrypt VITE_OIDC_ISSUER=https://authentik.example/application/o/manage/ VITE_OIDC_CLIENT_ID=manage VITE_OIDC_REDIRECT_URI=https://manage.example.com/ VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/ SSH_KEY_HOST_PATH=$HOME/.ssh/id_ed25519 docker compose up --build
|
||||
BACKEND_APP_HOST=manage.example.com FRONTEND_APP_HOST=manage.example.com CERT_RESOLVER=letsencrypt VITE_OIDC_ISSUER=https://authentik.example/application/o/manage/ VITE_OIDC_CLIENT_ID=manage VITE_OIDC_REDIRECT_URI=https://manage.example.com/ VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/ docker compose up --build
|
||||
```
|
||||
|
||||
For local development, no SSH key is required unless you want to connect to remote SSH machines later:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
Example environment variables:
|
||||
@@ -120,9 +119,8 @@ SMTP_TIMEOUT=30
|
||||
# Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab.
|
||||
# The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required.
|
||||
#
|
||||
# If you still want to keep a fallback SSH key available for legacy/manual use, provide a single key file path:
|
||||
SSH_KEY_HOST_PATH=/absolute/path/to/id_ed25519
|
||||
# The container-side secret path is fixed at /run/secrets/ssh_private_key.
|
||||
# Remote SSH machines can store their private key and optional passphrase directly in Settings,
|
||||
# so no SSH key mount is required for normal use.
|
||||
|
||||
# Authentik / OIDC
|
||||
AUTH_ENABLED=true
|
||||
|
||||
+3
-10
@@ -72,10 +72,6 @@ SMTP_TIMEOUT=30
|
||||
# Jellyfin, Jellyseerr, and SSH targets are now configured per machine in the app's Settings tab.
|
||||
# The backend seeds a local machine automatically, so no global Jellyfin or SSH env vars are required.
|
||||
#
|
||||
# If you still want to keep a fallback SSH key available for legacy/manual use, provide a single key file path:
|
||||
SSH_KEY_HOST_PATH=/absolute/path/to/id_ed25519
|
||||
# The container-side secret path is fixed at /run/secrets/ssh_private_key.
|
||||
|
||||
# Authentik / OIDC
|
||||
AUTH_ENABLED=true
|
||||
OIDC_ISSUER_URL=https://authentik.example/application/o/media-library-viewer/
|
||||
@@ -118,20 +114,17 @@ export VITE_OIDC_CLIENT_ID=manage
|
||||
export VITE_OIDC_REDIRECT_URI=https://manage.example.com/
|
||||
export VITE_OIDC_POST_LOGOUT_REDIRECT_URI=https://manage.example.com/
|
||||
|
||||
# Optional: provide a single SSH private key file for remote machines configured later in Settings.
|
||||
# Compose mounts it as a Docker secret, so you do not need to mount your whole ~/.ssh directory.
|
||||
# If you prefer SSH agent forwarding, Paramiko will use SSH_AUTH_SOCK when you provide it.
|
||||
export SSH_KEY_HOST_PATH=$HOME/.ssh/id_ed25519
|
||||
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
2. After the API is running, open the app, go to **Settings**, and add machine entries:
|
||||
- **Local**: monitors the API host itself without SSH.
|
||||
- **SSH**: monitors another machine using a host, username, and a key file path stored in the container.
|
||||
- **SSH**: monitors another machine using a host, username, and a private key pasted directly into the machine settings, with an optional passphrase.
|
||||
|
||||
3. Open **Monitoring** to see one section per configured machine. Each section uses its own collector state, disk path, metrics queries, and recent action history, which are populated automatically by the backend poller.
|
||||
|
||||
For local development, `docker compose -f docker-compose.dev.yml up --build` does not require an SSH key unless you configure remote SSH machines in the Settings tab.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- `GET /api/dashboard/counts` — Movie/series/episode totals
|
||||
|
||||
@@ -14,6 +14,7 @@ import logging
|
||||
import posixpath
|
||||
import shlex
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -41,6 +42,8 @@ class RemoteSSHClient:
|
||||
username: str,
|
||||
port: int = 22,
|
||||
key_filename: str | None = None,
|
||||
private_key: str | None = None,
|
||||
private_key_passphrase: str | None = None,
|
||||
password: str | None = None,
|
||||
known_hosts_path: str | None = None,
|
||||
timeout: int = 20,
|
||||
@@ -51,6 +54,8 @@ class RemoteSSHClient:
|
||||
self.username = username
|
||||
self.port = port
|
||||
self.key_filename = key_filename or None
|
||||
self.private_key = private_key or None
|
||||
self.private_key_passphrase = private_key_passphrase or None
|
||||
self.password = password or None
|
||||
self.known_hosts_path = known_hosts_path or None
|
||||
self.timeout = timeout
|
||||
@@ -70,17 +75,32 @@ class RemoteSSHClient:
|
||||
if self.known_hosts_path and Path(self.known_hosts_path).is_file():
|
||||
client.load_host_keys(self.known_hosts_path)
|
||||
client.set_missing_host_key_policy(paramiko.RejectPolicy())
|
||||
client.connect(
|
||||
self.host,
|
||||
port=self.port,
|
||||
username=self.username,
|
||||
key_filename=self.key_filename,
|
||||
password=self.password,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
connect_kwargs: dict[str, Any] = {
|
||||
"hostname": self.host,
|
||||
"port": self.port,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"timeout": self.timeout,
|
||||
}
|
||||
if self.private_key:
|
||||
connect_kwargs["pkey"] = self._load_private_key(self.private_key, self.private_key_passphrase)
|
||||
else:
|
||||
connect_kwargs["key_filename"] = self.key_filename
|
||||
client.connect(**connect_kwargs)
|
||||
self._client = client
|
||||
return client
|
||||
|
||||
@staticmethod
|
||||
def _load_private_key(private_key: str, passphrase: str | None = None) -> paramiko.PKey:
|
||||
key_classes = [paramiko.Ed25519Key, paramiko.RSAKey, paramiko.ECDSAKey]
|
||||
last_error: Exception | None = None
|
||||
for key_class in key_classes:
|
||||
try:
|
||||
return key_class.from_private_key(StringIO(private_key), password=passphrase or None)
|
||||
except Exception as exc: # pragma: no cover - try multiple algorithms
|
||||
last_error = exc
|
||||
raise RuntimeError("Unable to load SSH private key") from last_error
|
||||
|
||||
def close(self) -> None:
|
||||
if self._client:
|
||||
self._client.close()
|
||||
|
||||
@@ -57,22 +57,24 @@ def _jellyseerr_client_for(cache_key: tuple[str, str]) -> JellyseerrClient | Non
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _ssh_client_for(cache_key: tuple[str, str, str, int, str, str | None, str | None]) -> RemoteSSHClient:
|
||||
machine_id, host, username, port, key_filename, password, known_hosts_path = cache_key
|
||||
def _ssh_client_for(cache_key: tuple[str, str, str, int, str, str | None, str | None, str | None]) -> RemoteSSHClient:
|
||||
machine_id, host, username, port, key_filename, password, private_key, known_hosts_path = cache_key
|
||||
logger.info(
|
||||
"Creating SSH client machine_id=%s host=%s user=%s port=%s key=%s password=%s",
|
||||
"Creating SSH client machine_id=%s host=%s user=%s port=%s key=%s password=%s private_key=%s",
|
||||
machine_id or "<default>",
|
||||
host or "<unset>",
|
||||
username or "<unset>",
|
||||
port,
|
||||
key_filename or "<unset>",
|
||||
"set" if password else "missing",
|
||||
"set" if private_key else "missing",
|
||||
)
|
||||
client = RemoteSSHClient(
|
||||
host=host,
|
||||
username=username,
|
||||
port=port,
|
||||
key_filename=key_filename or None,
|
||||
private_key=private_key or None,
|
||||
password=password or None,
|
||||
known_hosts_path=known_hosts_path or None,
|
||||
)
|
||||
@@ -137,17 +139,35 @@ def get_jellyseerr_client(request: Request = None) -> JellyseerrClient | None:
|
||||
|
||||
def get_ssh_client(request: Request = None) -> RemoteSSHClient:
|
||||
"""Return a cached SSH client for the selected machine or legacy env fallback."""
|
||||
machine = get_settings_store().get_machine_config(_request_machine_id(request)) or _resolve_machine("ssh", request)
|
||||
if machine and machine.get("host") and machine.get("username") and machine.get("key_directory") and machine.get("key_name"):
|
||||
store = get_settings_store()
|
||||
machine_id = _request_machine_id(request)
|
||||
machine = store.get_machine_config(machine_id) if machine_id else None
|
||||
if machine is None:
|
||||
machine_ref = _resolve_machine("ssh", request)
|
||||
machine = store.get_machine_config(machine_ref["id"]) if machine_ref else None
|
||||
if machine and machine.get("host") and machine.get("username"):
|
||||
known_hosts_path = get_settings().ssh_known_hosts_file
|
||||
ensure_known_host(str(machine.get("host")), int(machine.get("port") or 22), known_hosts_path)
|
||||
key_data = None
|
||||
key_passphrase = None
|
||||
ssh_key_id = str(machine.get("ssh_key_id") or "").strip()
|
||||
if ssh_key_id:
|
||||
ssh_key = store.get_ssh_key(ssh_key_id)
|
||||
if ssh_key:
|
||||
key_data = ssh_key.get("private_key") or None
|
||||
key_passphrase = ssh_key.get("passphrase") or None
|
||||
if not key_data and machine.get("ssh_private_key"):
|
||||
key_data = machine.get("ssh_private_key") or None
|
||||
key_passphrase = machine.get("ssh_private_key_passphrase") or None
|
||||
cache_key = (
|
||||
machine["id"],
|
||||
machine["host"],
|
||||
machine["username"],
|
||||
int(machine.get("port") or 22),
|
||||
f"{machine.get('key_directory')}/{machine.get('key_name')}",
|
||||
f"{machine.get('key_directory')}/{machine.get('key_name')}" if machine.get("key_directory") and machine.get("key_name") else "",
|
||||
machine.get("password") or None,
|
||||
key_data,
|
||||
key_passphrase,
|
||||
str(known_hosts_path),
|
||||
)
|
||||
return _ssh_client_for(cache_key)
|
||||
@@ -165,7 +185,7 @@ def get_ssh_client(request: Request = None) -> RemoteSSHClient:
|
||||
if not settings.ssh_key_path:
|
||||
raise RuntimeError("No SSH machine is configured and SSH key settings must be configured")
|
||||
ensure_known_host(settings.ssh_host, settings.ssh_port, settings.ssh_known_hosts_file)
|
||||
return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, str(settings.ssh_known_hosts_file)))
|
||||
return _ssh_client_for(("legacy", settings.ssh_host, settings.ssh_username, settings.ssh_port, settings.ssh_key_path, settings.ssh_password or None, None, None, str(settings.ssh_known_hosts_file)))
|
||||
|
||||
|
||||
def get_mail_queue() -> MailQueue:
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from media_library_viewer_api.auth import require_jwt_auth, validate_auth_settings
|
||||
from media_library_viewer_api.config import get_settings
|
||||
from media_library_viewer_api.logging_utils import configure_logging, describe_settings
|
||||
from media_library_viewer_api.routers import dashboard, monitoring, media, files, jobs, users
|
||||
from media_library_viewer_api.routers import dashboard, monitoring, media, files, jobs, users, tasks
|
||||
from media_library_viewer_api.routers.settings import router as settings_router
|
||||
from media_library_viewer_api.dependencies import get_mail_queue, get_monitoring_poller, get_settings_store
|
||||
from media_library_viewer_api.services.known_hosts import ensure_known_hosts_for_machines
|
||||
@@ -98,6 +98,7 @@ app.include_router(media.router)
|
||||
app.include_router(files.router)
|
||||
app.include_router(jobs.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(tasks.router)
|
||||
app.include_router(settings_router)
|
||||
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ services:
|
||||
OIDC_JWKS_URL: ${OIDC_JWKS_URL:-}
|
||||
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||
SSH_KNOWN_HOSTS_PATH: /app/backend/.cache/known_hosts
|
||||
secrets:
|
||||
- ssh_private_key
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
@@ -43,7 +41,3 @@ services:
|
||||
volumes:
|
||||
frontend_node_modules:
|
||||
backend_cache:
|
||||
|
||||
secrets:
|
||||
ssh_private_key:
|
||||
file: ${SSH_KEY_HOST_PATH:-./secrets/ssh/id_ed25519}
|
||||
|
||||
@@ -20,8 +20,6 @@ services:
|
||||
SMTP_USE_SSL: ${SMTP_USE_SSL:-false}
|
||||
SMTP_TIMEOUT: ${SMTP_TIMEOUT:-30}
|
||||
SSH_KNOWN_HOSTS_PATH: /app/backend/.cache/known_hosts
|
||||
secrets:
|
||||
- ssh_private_key
|
||||
volumes:
|
||||
- backend_cache:/app/backend/.cache
|
||||
restart: unless-stopped
|
||||
@@ -80,10 +78,6 @@ services:
|
||||
volumes:
|
||||
backend_cache:
|
||||
|
||||
secrets:
|
||||
ssh_private_key:
|
||||
file: ${SSH_KEY_HOST_PATH:?set SSH_KEY_HOST_PATH}
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
|
||||
+19
-10
@@ -132,12 +132,15 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo
|
||||
- Support OIDC login in the frontend using an OIDC client library, with backend JWT validation for protected API requests.
|
||||
- Persist frontend OIDC auth state across tab reloads by storing the OIDC user and request state in browser localStorage.
|
||||
- Provide Docker Compose deployment files at the repository root for production and local development.
|
||||
- Backend Docker deployment should mount a private SSH key file into the container while the app synthesizes its own managed `known_hosts` file in the backend cache volume.
|
||||
- SSH private keys should be managed as reusable saved secrets in Settings, independent of any one machine, and SSH machines should select from that saved-key list.
|
||||
- The web UI should allow both importing an existing private key and generating a new SSH keypair for that saved-key list.
|
||||
- Saved SSH keys should display their derived public key, fingerprint, and machine usage count so administrators can audit them at a glance.
|
||||
- The app should support optional SSH private key passphrases alongside the stored key material.
|
||||
- The Settings tab should include a destructive local-database reset action protected by multiple acknowledgements and a typed confirmation phrase.
|
||||
- Production compose should also pass the root `.env` into the backend container so runtime auth settings like `OIDC_ISSUER_URL` are available there, not just at compose interpolation time.
|
||||
- The backend media index should persist in a Docker volume so a container restart or image rebuild does not force a new full index build.
|
||||
- Compose deployment should not require `env_file`; required values should be supplied through environment interpolation or inline shell exports.
|
||||
- The SSH key configuration should still support separate directory/name inputs for legacy/manual setups, but Compose should prefer a single mounted key file path instead of a whole SSH directory.
|
||||
- The SSH key configuration should move from machine-local text areas to a saved SSH key registry; directory/name inputs remain only as legacy compatibility fields.
|
||||
- Show Jellyfin media counts for movies, series, and series episodes on the dashboard.
|
||||
- Show dashboard session activity from Jellyfin, including both currently playing sessions and logged-in idle sessions.
|
||||
- Activity rows should include user, media title (or `(idle)`), playback state (`playing`/`paused`/`idle`), and whether transcoding is active.
|
||||
@@ -173,14 +176,16 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo
|
||||
- The Monitoring brush selection should persist across data refreshes and tab reloads instead of resetting whenever new samples arrive, and the zoom buttons should stay in sync with the visible brush range.
|
||||
- The Monitoring brush UI should be stable and remain visible after drags or zoom changes; it should be rendered independently from the chart redraw cycle, with usable resize handles for left/right edges.
|
||||
|
||||
### Remote Jobs
|
||||
### Saved Actions / Remote Jobs
|
||||
|
||||
- Support running remote jobs over SSH using explicit templates.
|
||||
- Phase 1 jobs should be safe/read-only by default.
|
||||
- Avoid arbitrary free-form command execution in the UI.
|
||||
- Job templates should be centralized in `jobs.py` for future extension.
|
||||
- Command template values must be shell-quoted before execution.
|
||||
- Future destructive jobs should require explicit confirmation.
|
||||
- Provide an Actions tab for predefined server tasks that users can save and run later.
|
||||
- The initial task types should support shell commands and Python scripts, while keeping the design flexible for future task types.
|
||||
- Avoid arbitrary free-form command execution for ad-hoc execution; tasks should be stored records with an explicit name, type, content, enabled flag, default machine, and notes.
|
||||
- Support running tasks against either the local API host or a configured SSH machine using the same machine registry used by Monitoring.
|
||||
- Command/script content should be executed through the existing safe process helpers and shell-quoted where applicable.
|
||||
- Future destructive actions should require explicit confirmations or dry-run style safeguards.
|
||||
- Job templates should remain centralized in `jobs.py` for future extension.
|
||||
- Remote job template values must be shell-quoted before execution.
|
||||
|
||||
## Decision Log
|
||||
|
||||
@@ -219,5 +224,9 @@ Phase 1: Jellyfin media index, SSH-based remote filesystem inspection, server mo
|
||||
- 2026-05-06: The app shell now uses an Applications top-level tab with a Jellyfin subtab for media/library work and a placeholder Nextcloud subtab for future expansion.
|
||||
- 2026-05-06: The settings model now treats Jellyfin/Jellyseerr as machine-level configuration instead of global env-only values, so app hosts can be edited alongside other machine services.
|
||||
- 2026-05-06: The backend now synthesizes a managed `known_hosts` file from configured SSH machines at startup, avoiding a mounted SSH directory while keeping strict host-key verification enabled.
|
||||
- 2026-05-06: Docker Compose was simplified to mount a single SSH private key file when needed instead of a whole `~/.ssh` directory.
|
||||
- 2026-05-06: SSH credentials were moved toward reusable saved key records in Settings, so machines can point at a shared SSH key instead of storing their own duplicate private key text.
|
||||
- 2026-05-06: The Settings page now includes an SSH key registry UI with create/edit/delete flows and a generate-key action so users can make a reusable key directly in the web interface.
|
||||
- 2026-05-06: Saved SSH keys now surface a derived public key, fingerprint, and per-key machine usage count in the Settings UI for easier auditing.
|
||||
- 2026-05-06: The dev Compose stack now starts without any SSH key material at all unless a user later configures remote SSH machines.
|
||||
- 2026-05-06: The Settings page now exposes a protected local-database reset flow that requires several explicit acknowledgements and a typed confirmation phrase before it can delete the cached app databases.
|
||||
- 2026-05-06: Added an Actions tab for saved server tasks, with backend persistence, per-task run history, and support for shell/Python task types on either local or SSH machines.
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Applications } from "./pages/Applications";
|
||||
import { Settings } from "./pages/Settings";
|
||||
import { UsersPage } from "./pages/Users";
|
||||
import { FileBrowser } from "./pages/FileBrowser";
|
||||
import { Actions } from "./pages/Actions";
|
||||
import { getAppTheme } from "./theme";
|
||||
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
||||
|
||||
@@ -180,6 +181,12 @@ function Shell({
|
||||
to="/applications"
|
||||
/>
|
||||
<Tab value="/users" label="Users" component={NavLink} to="/users" />
|
||||
<Tab
|
||||
value="/actions"
|
||||
label="Actions"
|
||||
component={NavLink}
|
||||
to="/actions"
|
||||
/>
|
||||
<Tab value="/files" label="Files" component={NavLink} to="/files" />
|
||||
<Tab
|
||||
value="/settings"
|
||||
@@ -200,6 +207,7 @@ function Shell({
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
|
||||
@@ -15,6 +15,12 @@ import type {
|
||||
MonitoringStatus,
|
||||
MonitoringMetrics,
|
||||
DiskSpace,
|
||||
SSHKey,
|
||||
SSHKeyInput,
|
||||
SSHKeyGenerated,
|
||||
SavedTask,
|
||||
SavedTaskInput,
|
||||
SavedTaskRun,
|
||||
MonitoringMachine,
|
||||
MonitoringMachineInput,
|
||||
MonitoringMachineAction,
|
||||
@@ -200,11 +206,84 @@ export const restartCollector = (machineId?: string) =>
|
||||
|
||||
export const fetchMonitoringSettings = () =>
|
||||
get<MonitoringMachine[]>("/api/settings/machines");
|
||||
export const fetchSSHKeys = () => get<SSHKey[]>("/api/settings/ssh-keys");
|
||||
export const generateSSHKey = (payload: {
|
||||
name: string;
|
||||
passphrase: string;
|
||||
notes: string;
|
||||
bits?: number;
|
||||
}) => post<SSHKeyGenerated>("/api/settings/ssh-keys/generate", payload);
|
||||
export const fetchMonitoringMachineActions = (machineId: string, limit = 10) =>
|
||||
get<{ items: MonitoringMachineAction[]; total: number }>(
|
||||
`/api/monitoring/machines/${encodeURIComponent(machineId)}/actions`,
|
||||
{ limit: String(limit) },
|
||||
);
|
||||
export const saveSSHKey = (key: SSHKeyInput) =>
|
||||
fetch(
|
||||
buildUrl(
|
||||
key.id
|
||||
? `/api/settings/ssh-keys/${encodeURIComponent(key.id)}`
|
||||
: "/api/settings/ssh-keys",
|
||||
),
|
||||
{
|
||||
method: key.id ? "PUT" : "POST",
|
||||
headers: buildHeaders(true),
|
||||
body: JSON.stringify(key),
|
||||
},
|
||||
).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}: ${await readErrorDetail(response)}`);
|
||||
}
|
||||
return response.json() as Promise<SSHKey>;
|
||||
});
|
||||
export const deleteSSHKey = (keyId: string) =>
|
||||
del<{ status: string }>(
|
||||
`/api/settings/ssh-keys/${encodeURIComponent(keyId)}`,
|
||||
);
|
||||
|
||||
export const fetchSavedTasks = () => get<SavedTask[]>("/api/tasks");
|
||||
export const fetchSavedTaskRuns = (taskId: string, limit = 10) =>
|
||||
get<{ items: SavedTaskRun[]; total: number }>(
|
||||
`/api/tasks/${encodeURIComponent(taskId)}/runs`,
|
||||
{
|
||||
limit: String(limit),
|
||||
},
|
||||
);
|
||||
export const saveTask = (task: SavedTaskInput) =>
|
||||
fetch(
|
||||
buildUrl(
|
||||
task.id ? `/api/tasks/${encodeURIComponent(task.id)}` : "/api/tasks",
|
||||
),
|
||||
{
|
||||
method: task.id ? "PUT" : "POST",
|
||||
headers: buildHeaders(true),
|
||||
body: JSON.stringify(task),
|
||||
},
|
||||
).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}: ${await readErrorDetail(response)}`);
|
||||
}
|
||||
return response.json() as Promise<SavedTask>;
|
||||
});
|
||||
export const deleteTask = (taskId: string) =>
|
||||
del<{ status: string }>(`/api/tasks/${encodeURIComponent(taskId)}`);
|
||||
export const runTask = (taskId: string, machineId?: string) =>
|
||||
post<{
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
machine_id: string;
|
||||
machine_name: string;
|
||||
task_type: string;
|
||||
exit_status: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}>(
|
||||
machineId
|
||||
? `/api/tasks/run?machine_id=${encodeURIComponent(machineId)}`
|
||||
: "/api/tasks/run",
|
||||
{ task_id: taskId },
|
||||
);
|
||||
|
||||
export const saveMonitoringMachine = (machine: MonitoringMachineInput) =>
|
||||
fetch(
|
||||
buildUrl(
|
||||
|
||||
@@ -89,6 +89,73 @@ export interface NowPlayingSession {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export interface SSHKey {
|
||||
id: string;
|
||||
name: string;
|
||||
private_key_set: boolean;
|
||||
passphrase_set: boolean;
|
||||
public_key: string;
|
||||
fingerprint: string;
|
||||
usage_count: number;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface SSHKeyInput {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
private_key: string;
|
||||
passphrase: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface SSHKeyGenerated {
|
||||
name: string;
|
||||
private_key: string;
|
||||
passphrase: string;
|
||||
notes: string;
|
||||
public_key: string;
|
||||
fingerprint: string;
|
||||
usage_count: number;
|
||||
}
|
||||
|
||||
export interface SavedTask {
|
||||
id: string;
|
||||
name: string;
|
||||
task_type: "shell" | "python";
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
default_machine_id: string;
|
||||
notes: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface SavedTaskInput {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
task_type: "shell" | "python";
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
default_machine_id: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface SavedTaskRun {
|
||||
id: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
machine_id: string;
|
||||
machine_name: string;
|
||||
task_type: "shell" | "python";
|
||||
status: string;
|
||||
created_at: number;
|
||||
duration_ms: number;
|
||||
request_id: string;
|
||||
stdout_tail: string;
|
||||
stderr_tail: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface MonitoringMachine {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -100,6 +167,9 @@ export interface MonitoringMachine {
|
||||
username: string;
|
||||
key_directory: string;
|
||||
key_name: string;
|
||||
ssh_key_id: string;
|
||||
ssh_private_key_set: boolean;
|
||||
ssh_private_key_passphrase_set: boolean;
|
||||
password_set: boolean;
|
||||
media_root: string;
|
||||
path_prefix: string;
|
||||
@@ -122,6 +192,9 @@ export interface MonitoringMachineInput {
|
||||
username: string;
|
||||
key_directory: string;
|
||||
key_name: string;
|
||||
ssh_key_id: string;
|
||||
ssh_private_key: string;
|
||||
ssh_private_key_passphrase: string;
|
||||
password: string;
|
||||
media_root: string;
|
||||
path_prefix: string;
|
||||
@@ -133,6 +206,44 @@ export interface MonitoringMachineInput {
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface SavedTask {
|
||||
id: string;
|
||||
name: string;
|
||||
task_type: "shell" | "python";
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
default_machine_id: string;
|
||||
notes: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface SavedTaskInput {
|
||||
id?: string | null;
|
||||
name: string;
|
||||
task_type: "shell" | "python";
|
||||
content: string;
|
||||
enabled: boolean;
|
||||
default_machine_id: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface SavedTaskRun {
|
||||
id: string;
|
||||
task_id: string;
|
||||
task_name: string;
|
||||
machine_id: string;
|
||||
machine_name: string;
|
||||
task_type: "shell" | "python";
|
||||
status: string;
|
||||
created_at: number;
|
||||
duration_ms: number;
|
||||
request_id: string;
|
||||
stdout_tail: string;
|
||||
stderr_tail: string;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface MonitoringMachineAction {
|
||||
machine_id: string;
|
||||
machine_name: string;
|
||||
|
||||
Reference in New Issue
Block a user