From fdbc84aee5e967b503c3b7bd0385af0d29d7b351 Mon Sep 17 00:00:00 2001 From: codey Date: Wed, 20 Aug 2025 07:32:40 -0400 Subject: [PATCH] misc --- src/assets/styles/global.css | 2 - src/components/TRip/BreadcrumbNav.jsx | 4 +- src/components/TRip/MediaRequestForm.jsx | 5 +- src/components/TRip/RequestManagement.jsx | 651 +++++++++------------- src/layouts/Base.astro | 2 +- src/pages/TRip/requests.astro | 3 +- 6 files changed, 276 insertions(+), 391 deletions(-) diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index 41c18d6..db8dc5e 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -30,8 +30,6 @@ html { @apply absolute invisible no-underline; margin-left: -1em; padding-right: 0.5em; - width: 80%; - max-width: 700px; cursor: pointer; } diff --git a/src/components/TRip/BreadcrumbNav.jsx b/src/components/TRip/BreadcrumbNav.jsx index c843603..1a83ccb 100644 --- a/src/components/TRip/BreadcrumbNav.jsx +++ b/src/components/TRip/BreadcrumbNav.jsx @@ -2,8 +2,8 @@ import React from "react"; export default function BreadcrumbNav({ currentPage }) { const pages = [ - { key: "request", label: "Request Media", href: "/qs2" }, - { key: "management", label: "Manage Requests", href: "/qs2/requests" }, + { key: "request", label: "Request Media", href: "/TRip" }, + { key: "management", label: "Manage Requests", href: "/TRip/requests" }, ]; return ( diff --git a/src/components/TRip/MediaRequestForm.jsx b/src/components/TRip/MediaRequestForm.jsx index bc27670..67cf062 100644 --- a/src/components/TRip/MediaRequestForm.jsx +++ b/src/components/TRip/MediaRequestForm.jsx @@ -400,7 +400,10 @@ export default function MediaRequestForm() { headers: { "Content-Type": "application/json; charset=utf-8", }, - body: JSON.stringify({ track_ids: allSelectedIds }), + body: JSON.stringify({ + track_ids: allSelectedIds, + target: selectedArtist.artist + }), }); if (!response.ok) { diff --git a/src/components/TRip/RequestManagement.jsx b/src/components/TRip/RequestManagement.jsx index 45632a4..bca355a 100644 --- a/src/components/TRip/RequestManagement.jsx +++ b/src/components/TRip/RequestManagement.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, Suspense, lazy } from "react"; -import { toast } from 'react-toastify'; +import React, { useState, useEffect, useRef } from "react"; +import { toast } from "react-toastify"; import { DataTable } from "primereact/datatable"; import { Column } from "primereact/column"; import { Dropdown } from "primereact/dropdown"; @@ -7,87 +7,120 @@ import { Button } from "@mui/joy"; import { Dialog } from "primereact/dialog"; import { confirmDialog, ConfirmDialog } from "primereact/confirmdialog"; import BreadcrumbNav from "./BreadcrumbNav"; +import { API_URL } from "@/config"; - -const STATUS_OPTIONS = ["Pending", "Completed", "Failed"]; -const TYPE_OPTIONS = ["Artist", "Album", "Track"]; - -const initialRequests = [ - { - id: 1, - type: "Artist", - artist: "Bring Me The Horizon", - album: "", - track: "", - status: "Pending", - details: { - requestedBy: "codey", - timestamp: "2025-07-01T12:00:00Z", - comments: "", - }, - }, - { - id: 2, - type: "Track", - artist: "We Butter The Bread With Butter", - album: "Das Album", - track: "20 km/h", - status: "Failed", - details: { - requestedBy: "codey", - timestamp: "2025-06-11T09:00:00Z", - comments: "Track not found in external database.", - }, - }, - { - id: 3, - type: "Track", - artist: "We Butter The Bread With Butter", - album: "Das Album", - track: "20 km/h", - status: "Completed", - details: { - requestedBy: "codey", - timestamp: "2025-06-11T09:00:00Z", - comments: "Track retrieved successfully.", - }, - }, -]; - +const STATUS_OPTIONS = ["queued", "started", "compressing", "finished", "failed"]; +const TAR_BASE_URL = "https://codey.lol/m/m2/"; // configurable prefix export default function RequestManagement() { - const [requests, setRequests] = useState(initialRequests); + const [requests, setRequests] = useState([]); const [filterType, setFilterType] = useState(null); const [filterStatus, setFilterStatus] = useState(null); - const [filteredRequests, setFilteredRequests] = useState(initialRequests); + const [filteredRequests, setFilteredRequests] = useState([]); const [selectedRequest, setSelectedRequest] = useState(null); const [isDialogVisible, setIsDialogVisible] = useState(false); + const pollingRef = useRef(null); + const pollingDetailRef = useRef(null); + const authFetch = async (url, options = {}) => fetch(url, { ...options, credentials: "include" }); - useEffect(() => { - let filtered = [...requests]; - if (filterType) { - filtered = filtered.filter((r) => r.type === filterType); - } - if (filterStatus) { - filtered = filtered.filter((r) => r.status === filterStatus); - } - setFilteredRequests(filtered); - }, [filterType, filterStatus, requests]); + const tarballUrl = (absPath) => { + if (!absPath) return null; + const filename = absPath.split("/").pop(); // get "SOMETHING.tar.gz" + return `${TAR_BASE_URL}${filename}`; + }; - const getStatusTextColor = (status) => { - switch (status) { - case "Pending": - return "text-yellow-500"; - case "Completed": - return "text-green-500"; - case "Failed": - return "text-red-500"; - default: - return "text-neutral-500"; + const fetchJobs = async () => { + try { + const res = await authFetch(`${API_URL}/trip/jobs/list`); + if (!res.ok) throw new Error("Failed to fetch jobs"); + const data = await res.json(); + setRequests(Array.isArray(data.jobs) ? data.jobs : []); + } catch (err) { + console.error(err); + toast.error("Failed to fetch jobs list"); } }; + const fetchJobDetail = async (jobId) => { + try { + const res = await authFetch(`${API_URL}/trip/job/${jobId}`); + if (!res.ok) throw new Error("Failed to fetch job details"); + return await res.json(); + } catch (err) { + console.error(err); + toast.error("Failed to fetch job details"); + return null; + } + }; + + useEffect(() => { fetchJobs(); }, []); + useEffect(() => { + if (isDialogVisible && selectedRequest) { + // Start polling + const poll = async () => { + const updated = await fetchJobDetail(selectedRequest.id); + if (updated) setSelectedRequest(updated); + }; + + pollingDetailRef.current = setInterval(poll, 1500); + + return () => { + if (pollingDetailRef.current) { + clearInterval(pollingDetailRef.current); + pollingDetailRef.current = null; + } + }; + } + }, [isDialogVisible, selectedRequest?.id]); + useEffect(() => { + const hasActive = requests.some((j) => ["queued", "started", "compressing"].includes(j.status)); + if (hasActive && !pollingRef.current) pollingRef.current = setInterval(fetchJobs, 1500); + else if (!hasActive && pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } + return () => { if (pollingRef.current) clearInterval(pollingRef.current); pollingRef.current = null; }; + }, [requests]); + + useEffect(() => { + let filtered = [...requests]; + if (filterType) filtered = filtered.filter((r) => r.type === filterType); + if (filterStatus) filtered = filtered.filter((r) => r.status === filterStatus); + setFilteredRequests(filtered); + }, [filterType, filterStatus, requests]); + + const getStatusColorClass = (status) => { + switch (status) { + case "queued": return "bg-yellow-500 text-black"; + case "started": + case "compressing": return "bg-blue-500 text-white"; + case "finished": return "bg-green-500 text-white"; + case "failed": return "bg-red-500 text-white"; + default: return "bg-gray-500 text-white"; + } + }; + + const statusBodyTemplate = (rowData) => ( + + {rowData.status} + + ); + + const safeText = (val) => (val === 0 ? "0" : val || "—"); + const textWithEllipsis = (val, width = "12rem") => ( + {val || "—"} + ); + + const basename = (p) => (typeof p === "string" ? p.split("/").pop() : ""); + + const formatProgress = (p) => { + if (p === null || p === undefined || p === "") return "—"; + const num = Number(p); + if (Number.isNaN(num)) return "—"; + const pct = num > 1 ? Math.round(num) : Math.round(num * 100); + return `${pct}%`; + }; const confirmDelete = (requestId) => { confirmDialog({ @@ -103,391 +136,243 @@ export default function RequestManagement() { toast.success("Request deleted"); }; - const statusBodyTemplate = (rowData) => { - let colorClass = ""; + const actionBodyTemplate = (rowData) => ( + + ); - switch (rowData.status) { - case "Pending": - colorClass = "bg-yellow-300 text-yellow-900"; - break; - case "Completed": - colorClass = "bg-green-300 text-green-900"; - break; - case "Failed": - colorClass = "bg-red-300 text-red-900"; - break; - default: - colorClass = "bg-gray-300 text-gray-900"; - } - - return ( - - {rowData.status} - - ); - }; - - const actionBodyTemplate = (rowData) => { - return ( - - ); + const handleRowClick = async (e) => { + const detail = await fetchJobDetail(e.data.id); + if (detail) { setSelectedRequest(detail); setIsDialogVisible(true); } }; return ( -
- - - `} -

- Media Request Management -

+

Media Request Management

- ({ label: t, value: t })), - ]} - onChange={(e) => setFilterType(e.value)} - placeholder="Filter by Type" - className="min-w-[180px]" - /> ({ label: s, value: s })), - ]} + options={[{ label: "All Statuses", value: null }, ...STATUS_OPTIONS.map((s) => ({ label: s, value: s }))]} onChange={(e) => setFilterStatus(e.value)} placeholder="Filter by Status" className="min-w-[180px]" />
- { - setSelectedRequest(e.data); - setIsDialogVisible(true); - }} - > - - - - - - - - +
+ + textWithEllipsis(row.target, "10rem")} /> + + formatProgress(row.progress)} style={{ width: "8rem", textAlign: "center" }} sortable /> + { + const url = tarballUrl(row.tarball); + return url ? ( + + {url.split("/").pop()} + + ) : ( + "—" + ); + }} + style={{ width: "18rem" }} + /> + +
+ + setIsDialogVisible(false)} - breakpoints={{ '960px': '95vw' }} + breakpoints={{ "960px": "95vw" }} modal dismissableMask + className="dark:bg-neutral-900 dark:text-neutral-100" > - {selectedRequest && ( + {selectedRequest ? (
-

ID: {selectedRequest.id}

-

Type: {selectedRequest.type}

-

Artist: {selectedRequest.artist}

- {selectedRequest.album &&

Album: {selectedRequest.album}

} - {selectedRequest.track &&

Track: {selectedRequest.track}

} -

- Status: +

Target: {selectedRequest.target}

+ +

+ Status:{" "} {selectedRequest.status}

+ {selectedRequest.progress !== undefined && selectedRequest.progress !== null && ( +

Progress: {formatProgress(selectedRequest.progress)}

+ )} - {selectedRequest.details && ( - <> -

Requested By: {selectedRequest.details.requestedBy}

-

Timestamp: {new Date(selectedRequest.details.timestamp).toLocaleString()}

- {selectedRequest.details.comments && ( -

Comments: {selectedRequest.details.comments}

- )} - + {selectedRequest.enqueued_at && ( +

Enqueued: {new Date(selectedRequest.enqueued_at).toLocaleString()}

+ )} + {selectedRequest.started_at && ( +

Started: {new Date(selectedRequest.started_at).toLocaleString()}

+ )} + {selectedRequest.ended_at && ( +

Ended: {new Date(selectedRequest.ended_at).toLocaleString()}

+ )} + + {selectedRequest.tarball && ( +

+ Tarball:{" "} + + {tarballUrl(selectedRequest.tarball).split("/").pop()} + +

+ )} + {Array.isArray(selectedRequest.tracks) && selectedRequest.tracks.length > 0 && ( +
+ Tracks: +
    + {selectedRequest.tracks.map((t, idx) => { + if (t && typeof t === "object") { + const tid = "track_id" in t ? t.track_id : t.id ?? idx; + const st = t.status ?? "—"; + return
  • {`${tid} — ${st}`}
  • ; + } + return
  • {String(t)}
  • ; + })} +
+
)}
+ ) : ( +

Loading...

)}
-
); } diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 970204f..b47703f 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -43,7 +43,7 @@ const { title, description, image } = Astro.props; class="antialiased flex flex-col items-center justify-center mx-auto mt-2 lg:mt-8 mb-20 lg:mb-40 scrollbar-hide">
+ class="flex-auto min-w-0 mt-2 md:mt-6 flex flex-col px-6 sm:px-4 md:px-0 max-w-3xl w-full">