Fix admin permissions bounce, prefixes APIs, and Italian UI leftovers.
Local Build and Deploy / deploy (push) Successful in 56s

Gate permissions on ACL manage + resolve super-admin from live user ranks, restore prefixes API routes, and anglicize hardcoded admin copy with nav i18n.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-15 20:33:00 +02:00
1 parent 0e89d03940
commit 3403d3b19f
38 files changed
+673 -208

No files matched your search

+5 -5
View File
@@ -70,11 +70,11 @@ export default async function SoundsPage() {
<div>
<h1 className="text-3xl font-bold tracking-tight">Song Disks</h1>
<p className="text-muted-foreground">
Carica e gestisci i file MP3 dei song disk riprodotti dai jukebox
in-game. I file sono serviti da <code>{baseUrl}</code> (configurabile
in{" "}
<a href="/admin/settings/cms" className="underline">
Impostazioni &gt; CMS
Upload and manage MP3 files for song disks played by in-game
jukeboxes. Files are served from <code>{baseUrl}</code>{" "}
(configurable in{" "}
<a href="/admin/settings" className="underline">
Settings
</a>
).
</p>
+53 -54
View File
@@ -121,8 +121,8 @@ export function SoundsClient({
if (!deleting) return;
const id = deleting.id;
run(() => deleteSoundtrack({ id }), {
successMessage: `Song disk #${id} eliminato.`,
errorMessage: `Impossibile eliminare il song disk #${id}.`,
successMessage: `Song disk #${id} deleted.`,
errorMessage: `Unable to delete song disk #${id}.`,
onSuccess: () => {
invalidateSongPickerCache();
setDeleting(null);
@@ -139,7 +139,7 @@ export function SoundsClient({
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Cerca per nome, autore, codice o id…"
placeholder="Search by name, author, code or id…"
className="pl-8"
/>
</div>
@@ -151,9 +151,9 @@ export function SoundsClient({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tutti ({soundtracks.length})</SelectItem>
<SelectItem value="all">All ({soundtracks.length})</SelectItem>
<SelectItem value="single">
Singoli MP3 ({counts.single})
Single MP3s ({counts.single})
</SelectItem>
<SelectItem value="composition">
Composition ({counts.composition})
@@ -199,15 +199,15 @@ export function SoundsClient({
{soundtracks.length === 0 ? (
<div className="flex flex-col items-center gap-2">
<Music className="h-8 w-8 opacity-50" />
<p>Nessun song disk caricato.</p>
<p>No song disks uploaded yet.</p>
{canEdit && (
<p className="text-xs">
Clicca &ldquo;Upload MP3&rdquo; per iniziare.
Click &ldquo;Upload MP3&rdquo; to get started.
</p>
)}
</div>
) : (
<>Nessun risultato per i filtri attivi.</>
<>No results for the active filters.</>
)}
</TableCell>
</TableRow>
@@ -293,7 +293,7 @@ export function SoundsClient({
className="h-8 w-8"
onClick={() => setEditing(s)}
disabled={isPending}
title="Modifica"
title="Edit"
>
<Pencil className="h-4 w-4" />
</Button>
@@ -303,7 +303,7 @@ export function SoundsClient({
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={() => setDeleting(s)}
disabled={isPending}
title="Elimina"
title="Delete"
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -345,27 +345,26 @@ export function SoundsClient({
<DialogContent>
<DialogHeader>
<DialogTitle>
Eliminare &ldquo;{deleting.name}&rdquo;?
Delete &ldquo;{deleting.name}&rdquo;?
</DialogTitle>
<DialogDescription>
{deleting.usedBy > 0 ? (
<>
Questo song disk è attualmente usato da{" "}
This song disk is currently used by{" "}
<strong>{deleting.usedBy}</strong> catalog item
{deleting.usedBy === 1 ? "" : "s"}. Eliminandolo, quei
catalog item verranno riportati a &ldquo;nessun song
disk&rdquo;.
{deleting.usedBy === 1 ? "" : "s"}. Deleting it will reset
those catalog items to &ldquo;no song disk&rdquo;.
{deleting.kind === "single" && (
<> Il file MP3 sarà rimosso dal disco.</>
<> The MP3 file will be removed from disk.</>
)}{" "}
L&apos;azione è irreversibile.
This action cannot be undone.
</>
) : (
<>
{deleting.kind === "single"
? "Il file MP3 sarà rimosso dal disco e la riga dal database."
: "La riga sarà rimossa dal database. Nessun file viene eliminato (una composition non ha MP3 dedicato)."}{" "}
L&apos;azione è irreversibile.
? "The MP3 file will be removed from disk and the row from the database."
: "The row will be removed from the database. No file is deleted (a composition has no dedicated MP3)."}{" "}
This action cannot be undone.
</>
)}
</DialogDescription>
@@ -376,7 +375,7 @@ export function SoundsClient({
onClick={() => setDeleting(null)}
disabled={isPending}
>
Annulla
Cancel
</Button>
<Button
variant="destructive"
@@ -384,7 +383,7 @@ export function SoundsClient({
disabled={isPending}
>
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
Elimina
Delete
</Button>
</DialogFooter>
</DialogContent>
@@ -420,11 +419,11 @@ function UploadDialog({
e.preventDefault();
const file = fileRef.current?.files?.[0];
if (!file) {
toast.error("Seleziona un file MP3.");
toast.error("Select an MP3 file.");
return;
}
if (!name.trim()) {
toast.error("Il nome è obbligatorio.");
toast.error("Name is required.");
return;
}
@@ -445,16 +444,16 @@ function UploadDialog({
id?: number;
};
if (!res.ok || !json.ok) {
toast.error(json.error || "Upload fallito.");
toast.error(json.error || "Upload failed.");
return;
}
toast.success(`Song disk #${json.id} caricato.`);
toast.success(`Song disk #${json.id} uploaded.`);
reset();
onOpenChange(false);
onUploaded();
} catch (err) {
console.error(err);
toast.error("Errore di rete durante l'upload.");
toast.error("Network error during upload.");
} finally {
setUploading(false);
}
@@ -471,14 +470,14 @@ function UploadDialog({
<DialogHeader>
<DialogTitle>Upload Song Disk</DialogTitle>
<DialogDescription>
Carica un file MP3. Dimensione massima 10 MB. Il codice sarà
generato automaticamente come <code>song_&#123;id&#125;</code>.
Upload an MP3 file. Max size 10 MB. The code will be generated
automatically as <code>song_&#123;id&#125;</code>.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<Label htmlFor="sound-file">File MP3</Label>
<Label htmlFor="sound-file">MP3 file</Label>
<Input
id="sound-file"
type="file"
@@ -488,23 +487,23 @@ function UploadDialog({
/>
</div>
<div className="space-y-1">
<Label htmlFor="sound-name">Nome</Label>
<Label htmlFor="sound-name">Name</Label>
<Input
id="sound-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Titolo del brano"
placeholder="Track title"
maxLength={100}
required
/>
</div>
<div className="space-y-1">
<Label htmlFor="sound-author">Autore</Label>
<Label htmlFor="sound-author">Author</Label>
<Input
id="sound-author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
placeholder="Autore (opzionale)"
placeholder="Author (optional)"
maxLength={50}
/>
</div>
@@ -516,7 +515,7 @@ function UploadDialog({
onClick={() => onOpenChange(false)}
disabled={uploading}
>
Annulla
Cancel
</Button>
<Button type="submit" disabled={uploading}>
{uploading ? (
@@ -524,7 +523,7 @@ function UploadDialog({
) : (
<Upload className="h-4 w-4 mr-1" />
)}
Carica
Upload
</Button>
</DialogFooter>
</form>
@@ -555,12 +554,12 @@ function EditDialog({
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) {
toast.error("Il nome è obbligatorio.");
toast.error("Name is required.");
return;
}
const lenNum = Number.parseInt(length, 10);
if (!Number.isFinite(lenNum) || lenNum < 0) {
toast.error("La durata deve essere un numero ≥ 0.");
toast.error("Length must be a number ≥ 0.");
return;
}
run(
@@ -573,8 +572,8 @@ function EditDialog({
length: lenNum,
}),
{
successMessage: "Song disk aggiornato.",
errorMessage: "Impossibile aggiornare il song disk.",
successMessage: "Song disk updated.",
errorMessage: "Unable to update the song disk.",
onSuccess: onSaved,
},
);
@@ -585,21 +584,21 @@ function EditDialog({
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
Modifica {isComposition ? "Trax Composition" : "Song Disk"}
Edit {isComposition ? "Trax Composition" : "Song Disk"}
</DialogTitle>
<DialogDescription>
<span className="font-mono text-xs">{soundtrack.code}</span>
{isComposition ? (
<>
{" "}
— composition di {soundtrack.samples.length} sample. Modifica il
campo <code>track</code> solo se sai cosa stai facendo.
— composition of {soundtrack.samples.length} samples. Edit the{" "}
<code>track</code> field only if you know what you are doing.
</>
) : (
<>
{" "}
— il file MP3 non può essere sostituito: elimina e ricarica se
devi cambiare audio.
— the MP3 file cannot be replaced: delete and re-upload if you
need to change the audio.
</>
)}
</DialogDescription>
@@ -607,7 +606,7 @@ function EditDialog({
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<Label htmlFor="edit-name">Nome</Label>
<Label htmlFor="edit-name">Name</Label>
<Input
id="edit-name"
value={name}
@@ -617,7 +616,7 @@ function EditDialog({
/>
</div>
<div className="space-y-1">
<Label htmlFor="edit-author">Autore</Label>
<Label htmlFor="edit-author">Author</Label>
<Input
id="edit-author"
value={author}
@@ -626,7 +625,7 @@ function EditDialog({
/>
</div>
<div className="space-y-1">
<Label htmlFor="edit-length">Durata (secondi)</Label>
<Label htmlFor="edit-length">Length (seconds)</Label>
<Input
id="edit-length"
type="number"
@@ -636,8 +635,8 @@ function EditDialog({
/>
<p className="text-xs text-muted-foreground">
{isComposition
? "Durata totale della composition. Lascia 0 se sconosciuta."
: "Impostato automaticamente dall’upload. Modifica solo se errato."}
? "Total composition length. Leave 0 if unknown."
: "Set automatically from the upload. Edit only if incorrect."}
</p>
</div>
@@ -653,8 +652,8 @@ function EditDialog({
placeholder="1:371,8;374,4;378,4;0,4;…"
/>
<p className="text-xs text-muted-foreground">
Formato: <code>canale:sample_id,durata;sample_id,durata;…</code>{" "}
— <code>0</code> = silenzio. Sample IDs rilevati:{" "}
Format: <code>channel:sample_id,length;sample_id,length;…</code>{" "}
— <code>0</code> = silence. Detected sample IDs:{" "}
<span className="font-mono">
{soundtrack.samples.join(", ") || "—"}
</span>
@@ -670,11 +669,11 @@ function EditDialog({
onClick={() => onOpenChange(false)}
disabled={isPending}
>
Annulla
Cancel
</Button>
<Button type="submit" disabled={isPending}>
{isPending && <Loader2 className="h-4 w-4 mr-1 animate-spin" />}
Salva
Save
</Button>
</DialogFooter>
</form>