style: format code biome
Local Build and Deploy / deploy (push) Failing after 46s

This commit is contained in:
openhands committed 2026-07-13 21:57:41 +02:00
1 parent 8efd032cc6
commit df38dccbf1
735 files changed
+128321 -120870

No files matched your search

+19 -19
View File
@@ -10,29 +10,29 @@ export const dynamic = "force-dynamic";
const AUTOPLAY_KEYS = ["radio_auto_play", "radio_autoplay"];
function truthy(value: string | undefined): boolean {
if (value === undefined) return false;
const s = value.trim().toLowerCase();
return s === "1" || s === "true";
if (value === undefined) return false;
const s = value.trim().toLowerCase();
return s === "1" || s === "true";
}
export async function GET(_req: Request) {
try {
const rows = await prisma.websiteSetting.findMany({
where: { key: { in: AUTOPLAY_KEYS } },
select: { key: true, value: true },
});
try {
const rows = await prisma.websiteSetting.findMany({
where: { key: { in: AUTOPLAY_KEYS } },
select: { key: true, value: true },
});
const map: Record<string, string> = {};
for (const row of rows) {
map[row.key] = row.value;
}
const map: Record<string, string> = {};
for (const row of rows) {
map[row.key] = row.value;
}
// Prefer the canonical key the settings page uses; fall back to the alias.
const autoplay = truthy(map.radio_auto_play ?? map.radio_autoplay);
// Prefer the canonical key the settings page uses; fall back to the alias.
const autoplay = truthy(map.radio_auto_play ?? map.radio_autoplay);
return apiJson({ autoplay });
} catch {
// DB unavailable — default to no autoplay rather than a 500.
return apiJson({ autoplay: false }, { status: 200 });
}
return apiJson({ autoplay });
} catch {
// DB unavailable — default to no autoplay rather than a 500.
return apiJson({ autoplay: false }, { status: 200 });
}
}
+28 -28
View File
@@ -9,37 +9,37 @@ export const dynamic = "force-dynamic";
// radio_* keys relevant to the public player widget. Mirrors what the AtomCMS
// radio-player blade requests from /api/radio/config.
const CONFIG_KEYS = new Set([
"radio_enabled",
"radio_name",
"radio_stream_url",
"radio_stream_backup_url",
"radio_auto_play",
"radio_auto_play_delay",
"radio_mute_on_start",
"radio_volume",
"radio_style",
"radio_player_type",
"radio_logo_url",
"radio_widget_enabled",
"radio_widget_show_globally",
"radio_widget_position",
"radio_enabled",
"radio_name",
"radio_stream_url",
"radio_stream_backup_url",
"radio_auto_play",
"radio_auto_play_delay",
"radio_mute_on_start",
"radio_volume",
"radio_style",
"radio_player_type",
"radio_logo_url",
"radio_widget_enabled",
"radio_widget_show_globally",
"radio_widget_position",
]);
export async function GET(_req: Request) {
try {
const rows = await prisma.websiteSetting.findMany({
where: { key: { in: Array.from(CONFIG_KEYS) } },
select: { key: true, value: true },
});
try {
const rows = await prisma.websiteSetting.findMany({
where: { key: { in: Array.from(CONFIG_KEYS) } },
select: { key: true, value: true },
});
const config: Record<string, string> = {};
for (const row of rows) {
config[row.key] = row.value;
}
const config: Record<string, string> = {};
for (const row of rows) {
config[row.key] = row.value;
}
return apiJson(config);
} catch {
// DB unavailable — serve an empty config rather than a 500.
return apiJson({}, { status: 200 });
}
return apiJson(config);
} catch {
// DB unavailable — serve an empty config rather than a 500.
return apiJson({}, { status: 200 });
}
}
+19 -19
View File
@@ -8,27 +8,27 @@ import { siteSettings } from "@/lib/services/site-settings";
export const dynamic = "force-dynamic";
export async function GET(_req: Request) {
try {
const raw = await siteSettings.get("radio_current_dj_id", "");
const id = Number(raw);
try {
const raw = await siteSettings.get("radio_current_dj_id", "");
const id = Number(raw);
// No DJ configured (empty / non-numeric / zero).
if (!raw || !Number.isFinite(id) || id <= 0) {
return apiJson({ dj: null });
}
// No DJ configured (empty / non-numeric / zero).
if (!raw || !Number.isFinite(id) || id <= 0) {
return apiJson({ dj: null });
}
const user = await prisma.user.findUnique({
where: { id },
select: { username: true, look: true },
});
const user = await prisma.user.findUnique({
where: { id },
select: { username: true, look: true },
});
if (!user) {
return apiJson({ dj: null });
}
if (!user) {
return apiJson({ dj: null });
}
return apiJson({ dj: { username: user.username, look: user.look } });
} catch {
// DB / settings unavailable — no DJ rather than a 500.
return apiJson({ dj: null }, { status: 200 });
}
return apiJson({ dj: { username: user.username, look: user.look } });
} catch {
// DB / settings unavailable — no DJ rather than a 500.
return apiJson({ dj: null }, { status: 200 });
}
}
+20 -15
View File
@@ -6,23 +6,28 @@ import { prisma } from "@/lib/prisma";
// flat { key: value } map. None of these keys are secrets.
export const dynamic = "force-dynamic";
const EMBED_KEYS = ["radio_enabled", "radio_name", "radio_stream_url", "radio_auto_play"];
const EMBED_KEYS = [
"radio_enabled",
"radio_name",
"radio_stream_url",
"radio_auto_play",
];
export async function GET(_req: Request) {
try {
const rows = await prisma.websiteSetting.findMany({
where: { key: { in: EMBED_KEYS } },
select: { key: true, value: true },
});
try {
const rows = await prisma.websiteSetting.findMany({
where: { key: { in: EMBED_KEYS } },
select: { key: true, value: true },
});
const config: Record<string, string> = {};
for (const row of rows) {
config[row.key] = row.value;
}
const config: Record<string, string> = {};
for (const row of rows) {
config[row.key] = row.value;
}
return apiJson(config);
} catch {
// DB unavailable — serve an empty config rather than a 500.
return apiJson({}, { status: 200 });
}
return apiJson(config);
} catch {
// DB unavailable — serve an empty config rather than a 500.
return apiJson({}, { status: 200 });
}
}
+78 -61
View File
@@ -9,80 +9,97 @@ export const dynamic = "force-dynamic";
const FETCH_TIMEOUT_MS = 4000;
function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v);
return typeof v === "object" && v !== null && !Array.isArray(v);
}
// Best-effort listener-count extraction across the common provider shapes
// (AzureCast nests under listeners.current/total; others expose num_listeners,
// listeners, unique_listeners, count, or a bare number).
function findCount(value: unknown, depth = 0): number | null {
if (depth > 4) return null;
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) {
return Number(value);
}
if (!isRecord(value)) return null;
if (depth > 4) return null;
if (typeof value === "number" && Number.isFinite(value)) return value;
if (
typeof value === "string" &&
value.trim() !== "" &&
Number.isFinite(Number(value))
) {
return Number(value);
}
if (!isRecord(value)) return null;
const keys = ["current", "total", "num_listeners", "listeners", "unique_listeners", "count"];
for (const key of keys) {
// eslint-disable-next-line security/detect-object-injection -- keys from hardcoded array
const v = value[key];
if (typeof v === "number" && Number.isFinite(v)) return v;
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) {
return Number(v);
}
}
for (const v of Object.values(value)) {
if (isRecord(v)) {
const found = findCount(v, depth + 1);
if (found !== null) return found;
}
}
return null;
const keys = [
"current",
"total",
"num_listeners",
"listeners",
"unique_listeners",
"count",
];
for (const key of keys) {
// eslint-disable-next-line security/detect-object-injection -- keys from hardcoded array
const v = value[key];
if (typeof v === "number" && Number.isFinite(v)) return v;
if (
typeof v === "string" &&
v.trim() !== "" &&
Number.isFinite(Number(v))
) {
return Number(v);
}
}
for (const v of Object.values(value)) {
if (isRecord(v)) {
const found = findCount(v, depth + 1);
if (found !== null) return found;
}
}
return null;
}
export async function GET(_req: Request) {
let url: string | null = null;
try {
url = (await siteSettings.get("radio_listeners_api_url", "")) || null;
} catch {
url = null;
}
let url: string | null = null;
try {
url = (await siteSettings.get("radio_listeners_api_url", "")) || null;
} catch {
url = null;
}
// Not configured — no listener data available.
if (!url) {
return apiJson({ listeners: null });
}
// Not configured — no listener data available.
if (!url) {
return apiJson({ listeners: null });
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(url, {
signal: controller.signal,
cache: "no-store",
headers: { accept: "application/json, text/plain, */*" },
});
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(url, {
signal: controller.signal,
cache: "no-store",
headers: { accept: "application/json, text/plain, */*" },
});
const raw = (await res.text()).trim();
if (raw === "") {
return apiJson({ listeners: null });
}
const raw = (await res.text()).trim();
if (raw === "") {
return apiJson({ listeners: null });
}
let parsed: unknown = raw;
try {
parsed = JSON.parse(raw);
} catch {
// leave as raw string; findCount handles numeric strings.
}
let parsed: unknown = raw;
try {
parsed = JSON.parse(raw);
} catch {
// leave as raw string; findCount handles numeric strings.
}
return apiJson({ listeners: findCount(parsed) });
} catch (e) {
const aborted = e instanceof Error && e.name === "AbortError";
return apiJson({
listeners: null,
error: aborted ? `Timed out after ${FETCH_TIMEOUT_MS / 1000}s` : "Fetch failed",
});
} finally {
clearTimeout(timer);
}
return apiJson({ listeners: findCount(parsed) });
} catch (e) {
const aborted = e instanceof Error && e.name === "AbortError";
return apiJson({
listeners: null,
error: aborted
? `Timed out after ${FETCH_TIMEOUT_MS / 1000}s`
: "Fetch failed",
});
} finally {
clearTimeout(timer);
}
}
+38 -36
View File
@@ -9,44 +9,46 @@ export const dynamic = "force-dynamic";
const FETCH_TIMEOUT_MS = 4000;
export async function GET(_req: Request) {
let url: string | null = null;
try {
url = (await siteSettings.get("radio_now_playing_api_url", "")) || null;
} catch {
url = null;
}
let url: string | null = null;
try {
url = (await siteSettings.get("radio_now_playing_api_url", "")) || null;
} catch {
url = null;
}
// Not configured — there is nothing to play.
if (!url) {
return apiJson({ nowPlaying: null });
}
// Not configured — there is nothing to play.
if (!url) {
return apiJson({ nowPlaying: null });
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(url, {
signal: controller.signal,
cache: "no-store",
headers: { accept: "application/json, text/plain, */*" },
});
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const res = await fetch(url, {
signal: controller.signal,
cache: "no-store",
headers: { accept: "application/json, text/plain, */*" },
});
const raw = (await res.text()).trim();
if (raw === "") {
return apiJson({ nowPlaying: null });
}
const raw = (await res.text()).trim();
if (raw === "") {
return apiJson({ nowPlaying: null });
}
// Return parsed JSON when the provider speaks JSON, else the raw text body.
try {
return apiJson(JSON.parse(raw));
} catch {
return apiJson({ nowPlaying: raw.slice(0, 2000) });
}
} catch (e) {
const aborted = e instanceof Error && e.name === "AbortError";
return apiJson({
error: aborted ? `Timed out after ${FETCH_TIMEOUT_MS / 1000}s` : "Fetch failed",
});
} finally {
clearTimeout(timer);
}
// Return parsed JSON when the provider speaks JSON, else the raw text body.
try {
return apiJson(JSON.parse(raw));
} catch {
return apiJson({ nowPlaying: raw.slice(0, 2000) });
}
} catch (e) {
const aborted = e instanceof Error && e.name === "AbortError";
return apiJson({
error: aborted
? `Timed out after ${FETCH_TIMEOUT_MS / 1000}s`
: "Fetch failed",
});
} finally {
clearTimeout(timer);
}
}
+33 -33
View File
@@ -7,42 +7,42 @@ import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
export async function GET(_req: Request) {
try {
// Sum points per user. Sort/slice in JS so we stay adapter-agnostic about
// aggregate ordering, then resolve the top 20 to usernames/looks.
const grouped = await prisma.radioListenerPoints.groupBy({
by: ["userId"],
_sum: { points: true },
});
try {
// Sum points per user. Sort/slice in JS so we stay adapter-agnostic about
// aggregate ordering, then resolve the top 20 to usernames/looks.
const grouped = await prisma.radioListenerPoints.groupBy({
by: ["userId"],
_sum: { points: true },
});
const ranked = grouped
.map((g) => ({ userId: g.userId, points: g._sum.points ?? 0 }))
.sort((a, b) => b.points - a.points)
.slice(0, 20);
const ranked = grouped
.map((g) => ({ userId: g.userId, points: g._sum.points ?? 0 }))
.sort((a, b) => b.points - a.points)
.slice(0, 20);
if (ranked.length === 0) {
return apiJson({ data: [] });
}
if (ranked.length === 0) {
return apiJson({ data: [] });
}
const userIds = ranked.map((r) => r.userId);
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, username: true, look: true },
});
const userById = new Map(users.map((u) => [u.id, u]));
const userIds = ranked.map((r) => r.userId);
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, username: true, look: true },
});
const userById = new Map(users.map((u) => [u.id, u]));
const data = ranked.map((r) => {
const u = userById.get(r.userId);
return {
username: u?.username ?? null,
look: u?.look ?? null,
points: r.points,
};
});
const data = ranked.map((r) => {
const u = userById.get(r.userId);
return {
username: u?.username ?? null,
look: u?.look ?? null,
points: r.points,
};
});
return apiJson({ data });
} catch {
// DB unavailable — serve an empty leaderboard rather than a 500.
return apiJson({ data: [] }, { status: 200 });
}
return apiJson({ data });
} catch {
// DB unavailable — serve an empty leaderboard rather than a 500.
return apiJson({ data: [] }, { status: 200 });
}
}
+14 -14
View File
@@ -1,24 +1,24 @@
import { apiJson, apiError } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { apiError, apiJson } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { prisma } from "@/lib/prisma";
// The Bearer-authed user's total radio listener points: the sum of all
// radio_listener_points.points rows for that user_id.
export const dynamic = "force-dynamic";
export async function GET(req: Request) {
const uid = await bearerUserId(req);
if (!uid) return apiError("Unauthorized", 401);
const uid = await bearerUserId(req);
if (!uid) return apiError("Unauthorized", 401);
try {
const agg = await prisma.radioListenerPoints.aggregate({
where: { userId: uid },
_sum: { points: true },
});
try {
const agg = await prisma.radioListenerPoints.aggregate({
where: { userId: uid },
_sum: { points: true },
});
return apiJson({ points: agg._sum.points ?? 0 });
} catch {
// DB unavailable — report zero rather than a 500.
return apiJson({ points: 0 }, { status: 200 });
}
return apiJson({ points: agg._sum.points ?? 0 });
} catch {
// DB unavailable — report zero rather than a 500.
return apiJson({ points: 0 }, { status: 200 });
}
}
+62 -59
View File
@@ -1,6 +1,6 @@
import { apiJson, apiError } from "@/lib/api";
import { prisma } from "@/lib/prisma";
import { apiError, apiJson } from "@/lib/api";
import { bearerUserId } from "@/lib/api-auth";
import { prisma } from "@/lib/prisma";
// Latest 50 radio shouts with their author's username/look resolved. Mirrors the
// query behind the public /radio/shouts page (radio_shouts ordered by created_at
@@ -11,72 +11,75 @@ export const dynamic = "force-dynamic";
const MAX_MESSAGE_LENGTH = 255;
export async function GET(_req: Request) {
try {
const shouts = await prisma.radioShouts.findMany({
orderBy: { createdAt: "desc" },
take: 50,
});
try {
const shouts = await prisma.radioShouts.findMany({
orderBy: { createdAt: "desc" },
take: 50,
});
// Resolve author usernames/looks. radio_shouts.user_id is an UnsignedBigInt
// while users.id is an Int, so narrow to Number for the lookup.
const authorIds = Array.from(new Set(shouts.map((s) => Number(s.userId))));
const authors = authorIds.length
? await prisma.user.findMany({
where: { id: { in: authorIds } },
select: { id: true, username: true, look: true },
})
: [];
const authorById = new Map(authors.map((a) => [a.id, a]));
// Resolve author usernames/looks. radio_shouts.user_id is an UnsignedBigInt
// while users.id is an Int, so narrow to Number for the lookup.
const authorIds = Array.from(new Set(shouts.map((s) => Number(s.userId))));
const authors = authorIds.length
? await prisma.user.findMany({
where: { id: { in: authorIds } },
select: { id: true, username: true, look: true },
})
: [];
const authorById = new Map(authors.map((a) => [a.id, a]));
const data = shouts.map((s) => {
const author = authorById.get(Number(s.userId));
return {
id: s.id,
userId: s.userId,
username: author?.username ?? null,
look: author?.look ?? null,
message: s.message,
createdAt: s.createdAt,
};
});
const data = shouts.map((s) => {
const author = authorById.get(Number(s.userId));
return {
id: s.id,
userId: s.userId,
username: author?.username ?? null,
look: author?.look ?? null,
message: s.message,
createdAt: s.createdAt,
};
});
return apiJson({ shouts: data });
} catch {
// DB unavailable — serve an empty list rather than a 500.
return apiJson({ shouts: [] }, { status: 200 });
}
return apiJson({ shouts: data });
} catch {
// DB unavailable — serve an empty list rather than a 500.
return apiJson({ shouts: [] }, { status: 200 });
}
}
// Post a new radio shout as the Bearer-authed user into radio_shouts.
export async function POST(req: Request) {
const uid = await bearerUserId(req);
if (!uid) return apiError("Unauthorized", 401);
const uid = await bearerUserId(req);
if (!uid) return apiError("Unauthorized", 401);
const body = (await req.json().catch(() => ({}))) as { message?: unknown };
const message = typeof body.message === "string" ? body.message.trim() : "";
const body = (await req.json().catch(() => ({}))) as { message?: unknown };
const message = typeof body.message === "string" ? body.message.trim() : "";
if (!message) {
return apiError("Message is required", 422);
}
if (message.length > MAX_MESSAGE_LENGTH) {
return apiError(`Message must be at most ${MAX_MESSAGE_LENGTH} characters`, 422);
}
if (!message) {
return apiError("Message is required", 422);
}
if (message.length > MAX_MESSAGE_LENGTH) {
return apiError(
`Message must be at most ${MAX_MESSAGE_LENGTH} characters`,
422,
);
}
try {
const now = new Date();
await prisma.radioShouts.create({
data: {
userId: BigInt(uid),
message,
createdAt: now,
updatedAt: now,
},
select: { id: true },
});
try {
const now = new Date();
await prisma.radioShouts.create({
data: {
userId: BigInt(uid),
message,
createdAt: now,
updatedAt: now,
},
select: { id: true },
});
return apiJson({ ok: true });
} catch {
// DB write failed — fail soft rather than a 500.
return apiError("Could not post shout", 503);
}
return apiJson({ ok: true });
} catch {
// DB write failed — fail soft rather than a 500.
return apiError("Could not post shout", 503);
}
}
+54 -50
View File
@@ -8,59 +8,63 @@ export const dynamic = "force-dynamic";
* updates without polling. Closes cleanly when the client disconnects.
*/
export async function GET(req: Request) {
const encoder = new TextEncoder();
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
let closed = false;
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
let closed = false;
const send = async () => {
if (closed) return;
const [nowPlaying, listeners] = await Promise.all([
fetchNowPlaying().catch(() => null),
fetchListeners().catch(() => null),
]);
try {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ nowPlaying, listeners })}\n\n`));
} catch {
closed = true;
}
};
const send = async () => {
if (closed) return;
const [nowPlaying, listeners] = await Promise.all([
fetchNowPlaying().catch(() => null),
fetchListeners().catch(() => null),
]);
try {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ nowPlaying, listeners })}\n\n`,
),
);
} catch {
closed = true;
}
};
// Initial event immediately, then on an interval.
await send();
const interval = setInterval(() => void send(), 10_000);
// SSE comment as a keep-alive ping between data events.
const ping = setInterval(() => {
if (!closed) {
try {
controller.enqueue(encoder.encode(": ping\n\n"));
} catch {
closed = true;
}
}
}, 25_000);
// Initial event immediately, then on an interval.
await send();
const interval = setInterval(() => void send(), 10_000);
// SSE comment as a keep-alive ping between data events.
const ping = setInterval(() => {
if (!closed) {
try {
controller.enqueue(encoder.encode(": ping\n\n"));
} catch {
closed = true;
}
}
}, 25_000);
const stop = () => {
closed = true;
clearInterval(interval);
clearInterval(ping);
try {
controller.close();
} catch {
/* already closed */
}
};
req.signal.addEventListener("abort", stop);
},
});
const stop = () => {
closed = true;
clearInterval(interval);
clearInterval(ping);
try {
controller.close();
} catch {
/* already closed */
}
};
req.signal.addEventListener("abort", stop);
},
});
return new Response(stream, {
headers: {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store, no-transform",
connection: "keep-alive",
"access-control-allow-origin": "*",
},
});
return new Response(stream, {
headers: {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store, no-transform",
connection: "keep-alive",
"access-control-allow-origin": "*",
},
});
}