dev #3
@@ -29,10 +29,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mobile-menu-dropdown.open {
|
.mobile-menu-dropdown.open {
|
||||||
max-height: 500px;
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-menu-dropdown a {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
padding: 0.6rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.mobile-menu-dropdown {
|
.mobile-menu-dropdown {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ interface RequestJob {
|
|||||||
tracks: number;
|
tracks: number;
|
||||||
quality: string;
|
quality: string;
|
||||||
status: string;
|
status: string;
|
||||||
progress: number;
|
progress: number | string | null;
|
||||||
type?: string;
|
type?: string;
|
||||||
tarball_path?: string;
|
tarball?: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
@@ -40,9 +40,13 @@ export default function RequestManagement() {
|
|||||||
const pollingDetailRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollingDetailRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
const resolveTarballPath = (job: RequestJob) => job.tarball;
|
||||||
|
|
||||||
const tarballUrl = (absPath: string | undefined, quality: string) => {
|
const tarballUrl = (absPath: string | undefined, quality: string) => {
|
||||||
if (!absPath) return null;
|
if (!absPath) return null;
|
||||||
const filename = absPath.split("/").pop(); // get "SOMETHING.tar.gz"
|
const filename = absPath.split("/").pop(); // get "SOMETHING.tar.gz"
|
||||||
|
// If the backend already stores a fully qualified URL, return as-is
|
||||||
|
if (/^https?:\/\//i.test(absPath)) return absPath;
|
||||||
return `${TAR_BASE_URL}/${quality}/${filename}`;
|
return `${TAR_BASE_URL}/${quality}/${filename}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,24 +176,21 @@ export default function RequestManagement() {
|
|||||||
|
|
||||||
const formatProgress = (p: unknown) => {
|
const formatProgress = (p: unknown) => {
|
||||||
if (p === null || p === undefined || p === "") return "—";
|
if (p === null || p === undefined || p === "") return "—";
|
||||||
const num = Number(p);
|
const pct = computePct(p);
|
||||||
if (Number.isNaN(num)) return "—";
|
|
||||||
const pct = num > 1 ? Math.round(num) : num;
|
|
||||||
return `${pct}%`;
|
return `${pct}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const computePct = (p: unknown) => {
|
const computePct = (p: unknown) => {
|
||||||
if (p === null || p === undefined || p === "") return 0;
|
if (p === null || p === undefined || p === "") return 0;
|
||||||
const num = Number(p);
|
const num = Number(p);
|
||||||
if (Number.isNaN(num)) return 0;
|
if (!Number.isFinite(num)) return 0;
|
||||||
return Math.min(100, Math.max(0, num > 1 ? Math.round(num) : Math.round(num * 100)));
|
const normalized = num > 1 ? num : num * 100;
|
||||||
|
return Math.min(100, Math.max(0, Math.round(normalized)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const progressBarTemplate = (rowData: RequestJob) => {
|
const progressBarTemplate = (rowData: RequestJob) => {
|
||||||
const p = rowData.progress;
|
const p = rowData.progress;
|
||||||
if (p === null || p === undefined || p === 0) return "—";
|
if (p === null || p === undefined || p === "") return "—";
|
||||||
const num = Number(p);
|
|
||||||
if (Number.isNaN(num)) return "—";
|
|
||||||
const pct = computePct(p);
|
const pct = computePct(p);
|
||||||
|
|
||||||
const getProgressColor = () => {
|
const getProgressColor = () => {
|
||||||
@@ -341,7 +342,7 @@ export default function RequestManagement() {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
body={(row: RequestJob) => {
|
body={(row: RequestJob) => {
|
||||||
const url = tarballUrl(row.tarball_path, row.quality || "FLAC");
|
const url = tarballUrl(resolveTarballPath(row as RequestJob), row.quality || "FLAC");
|
||||||
if (!url) return "—";
|
if (!url) return "—";
|
||||||
const encodedURL = encodeURI(url);
|
const encodedURL = encodeURI(url);
|
||||||
|
|
||||||
@@ -409,18 +410,25 @@ export default function RequestManagement() {
|
|||||||
<strong>Progress:</strong>
|
<strong>Progress:</strong>
|
||||||
<div className="rm-progress-container mt-2">
|
<div className="rm-progress-container mt-2">
|
||||||
<div className="rm-progress-track rm-progress-track-lg">
|
<div className="rm-progress-track rm-progress-track-lg">
|
||||||
|
{(() => {
|
||||||
|
const pctDialog = computePct(selectedRequest.progress);
|
||||||
|
const status = selectedRequest.status;
|
||||||
|
const fillColor = status === "Failed" ? "bg-red-500" : status === "Finished" ? "bg-green-500" : "bg-blue-500";
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rm-progress-fill ${selectedRequest.status === "Failed" ? "bg-red-500" : selectedRequest.status === "Finished" ? "bg-green-500" : "bg-blue-500"}`}
|
className={`rm-progress-fill ${fillColor}`}
|
||||||
style={{
|
style={{
|
||||||
['--rm-progress' as string]: (computePct(selectedRequest.progress) / 100).toString(),
|
['--rm-progress' as string]: (pctDialog / 100).toString(),
|
||||||
borderTopRightRadius: computePct(selectedRequest.progress) >= 100 ? '999px' : 0,
|
borderTopRightRadius: pctDialog >= 100 ? '999px' : 0,
|
||||||
borderBottomRightRadius: computePct(selectedRequest.progress) >= 100 ? '999px' : 0
|
borderBottomRightRadius: pctDialog >= 100 ? '999px' : 0
|
||||||
}}
|
}}
|
||||||
data-pct={computePct(selectedRequest.progress)}
|
data-pct={pctDialog}
|
||||||
aria-valuenow={Math.min(100, Math.max(0, Number(selectedRequest.progress) > 1 ? Math.round(selectedRequest.progress) : selectedRequest.progress * 100))}
|
aria-valuenow={pctDialog}
|
||||||
aria-valuemin={0}
|
aria-valuemin={0}
|
||||||
aria-valuemax={100}
|
aria-valuemax={100}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<span className="rm-progress-text">{formatProgress(selectedRequest.progress)}</span>
|
<span className="rm-progress-text">{formatProgress(selectedRequest.progress)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -437,17 +445,17 @@ export default function RequestManagement() {
|
|||||||
|
|
||||||
{/* --- Tarball Card --- */}
|
{/* --- Tarball Card --- */}
|
||||||
{
|
{
|
||||||
selectedRequest.tarball_path && (
|
selectedRequest.tarball && (
|
||||||
<div className="p-3 bg-gray-100 dark:bg-neutral-800 rounded-md">
|
<div className="p-3 bg-gray-100 dark:bg-neutral-800 rounded-md">
|
||||||
<p>
|
<p>
|
||||||
<strong>Tarball:</strong>{" "}
|
<strong>Tarball:</strong>{" "}
|
||||||
<a
|
<a
|
||||||
href={encodeURI(tarballUrl(selectedRequest.tarball_path, selectedRequest.quality) || "")}
|
href={encodeURI(tarballUrl(resolveTarballPath(selectedRequest), selectedRequest.quality) || "")}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-500 hover:underline"
|
className="text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
{tarballUrl(selectedRequest.tarball_path, selectedRequest.quality)?.split("/").pop()}
|
{tarballUrl(resolveTarballPath(selectedRequest), selectedRequest.quality)?.split("/").pop()}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ interface ChannelRow {
|
|||||||
export async function GET({ request }: APIContext): Promise<Response> {
|
export async function GET({ request }: APIContext): Promise<Response> {
|
||||||
// Rate limit check
|
// Rate limit check
|
||||||
const rateCheck = checkRateLimit(request, {
|
const rateCheck = checkRateLimit(request, {
|
||||||
limit: 20,
|
limit: 60,
|
||||||
windowMs: 1000,
|
windowMs: 1000,
|
||||||
burstLimit: 100,
|
burstLimit: 240,
|
||||||
burstWindowMs: 10_000,
|
burstWindowMs: 10_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -143,9 +143,9 @@ async function getChannelVisibleMembers(channelId: string, guildId: string): Pro
|
|||||||
export async function GET({ request }) {
|
export async function GET({ request }) {
|
||||||
// Rate limit check
|
// Rate limit check
|
||||||
const rateCheck = checkRateLimit(request, {
|
const rateCheck = checkRateLimit(request, {
|
||||||
limit: 20,
|
limit: 80,
|
||||||
windowMs: 1000,
|
windowMs: 1000,
|
||||||
burstLimit: 100,
|
burstLimit: 320,
|
||||||
burstWindowMs: 10_000,
|
burstWindowMs: 10_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ function getSafeImageUrl(originalUrl: string | null, baseUrl: string): string |
|
|||||||
export async function GET({ request }: APIContext) {
|
export async function GET({ request }: APIContext) {
|
||||||
// Rate limit check
|
// Rate limit check
|
||||||
const rateCheck = checkRateLimit(request, {
|
const rateCheck = checkRateLimit(request, {
|
||||||
limit: 30,
|
limit: 100,
|
||||||
windowMs: 1000,
|
windowMs: 1000,
|
||||||
burstLimit: 150,
|
burstLimit: 400,
|
||||||
burstWindowMs: 10_000,
|
burstWindowMs: 10_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Root from "@/components/AppLayout.jsx";
|
|||||||
const user = Astro.locals.user as any;
|
const user = Astro.locals.user as any;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base>
|
<Base title="Lighting">
|
||||||
<section class="page-section">
|
<section class="page-section">
|
||||||
<Root child="Lighting" user?={user} client:only="react" />
|
<Root child="Lighting" user?={user} client:only="react" />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const accessDenied = Astro.locals.accessDenied || false;
|
|||||||
const requiredRoles = Astro.locals.requiredRoles || [];
|
const requiredRoles = Astro.locals.requiredRoles || [];
|
||||||
|
|
||||||
---
|
---
|
||||||
<Base>
|
<Base title="Login">
|
||||||
<section class="page-section">
|
<section class="page-section">
|
||||||
<Root child="LoginPage" loggedIn={isLoggedIn} accessDenied={accessDenied} requiredRoles={requiredRoles} client:only="react" />
|
<Root child="LoginPage" loggedIn={isLoggedIn} accessDenied={accessDenied} requiredRoles={requiredRoles} client:only="react" />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Root from "../components/AppLayout.jsx";
|
|||||||
import "@styles/MemeGrid.css";
|
import "@styles/MemeGrid.css";
|
||||||
---
|
---
|
||||||
|
|
||||||
<Base hideFooter>
|
<Base hideFooter title="Memes">
|
||||||
<section class="page-section">
|
<section class="page-section">
|
||||||
<Root child="Memes" client:only="react" />
|
<Root child="Memes" client:only="react" />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user