feat: add manual recent furni resync action
CI / check (push) Successful in 24s
CI / release (push) Skipped
CI / deploy (push) Successful in 44s

This commit is contained in:
Simo committed 2026-08-02 14:59:31 +02:00
1 parent 88ac5ac1ba
commit bac2f653af
1 file changed
+120
@@ -6,6 +6,7 @@ import {
BarChart3,
Check,
CheckCheck,
DatabaseZap,
Download,
ExternalLink,
FileWarning,
@@ -53,6 +54,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
type RecentFurniResyncResult,
requestRecentFurniResync,
} from "@/lib/admin/recent-furni-resync";
import { adminFetch } from "@/lib/admin-fetch";
import type { FurniImportSource } from "@/lib/habbo-gamedata-hotel";
import { NitroEditorDialog } from "./nitro-editor-dialog";
@@ -211,6 +216,10 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
// Reorganize catalog
const [reorganizing, setReorganizing] = useState(false);
const [confirmRecentResync, setConfirmRecentResync] = useState(false);
const [recentResyncing, setRecentResyncing] = useState(false);
const [recentResyncResult, setRecentResyncResult] =
useState<RecentFurniResyncResult | null>(null);
const searchRef = useRef<HTMLInputElement>(null);
const batchAbortRef = useRef<AbortController | null>(null);
@@ -792,6 +801,30 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
}
}
async function resyncRecentImports() {
setConfirmRecentResync(false);
setRecentResyncing(true);
setRecentResyncResult(null);
try {
const result = await requestRecentFurniResync(adminFetch);
setRecentResyncResult(result);
if (result.examined === 0) {
toast.info("No imported furni found in the last 7 days");
} else if (result.failed > 0 || !result.rconOk) {
toast.warning("Furni resync completed with warnings");
} else {
toast.success(`Updated ${result.resynced} imported furni`);
}
fetchStats();
} catch (error) {
toast.error(
error instanceof Error ? error.message : "Furni resync failed",
);
} finally {
setRecentResyncing(false);
}
}
async function applyReorganize() {
setReorgPreview(null);
setReorganizing(true);
@@ -1038,6 +1071,56 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
</div>
)}
{recentResyncResult && (
<div className="rounded-lg border bg-card p-4 space-y-3">
<div className="flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold">Imported Furni Update</h3>
<Button
variant="ghost"
size="sm"
onClick={() => setRecentResyncResult(null)}
>
Dismiss
</Button>
</div>
<div className="flex flex-wrap gap-4 text-sm">
<span>{recentResyncResult.examined} examined</span>
<span className="text-[var(--admin-success)]">
{recentResyncResult.resynced} updated
</span>
<span
className={recentResyncResult.failed ? "text-destructive" : ""}
>
{recentResyncResult.failed} failed
</span>
<span
className={
recentResyncResult.rconOk
? "text-[var(--admin-success)]"
: "text-[var(--admin-warning)]"
}
>
RCON {recentResyncResult.rconOk ? "refreshed" : "not refreshed"}
</span>
</div>
{recentResyncResult.errors.length > 0 && (
<details className="text-xs">
<summary className="cursor-pointer text-muted-foreground">
View errors
</summary>
<div className="mt-2 max-h-48 space-y-1 overflow-y-auto">
{recentResyncResult.errors.map((error) => (
<p key={`${error.classname}:${error.message}`}>
<span className="font-mono">{error.classname}</span>:{" "}
{error.message}
</p>
))}
</div>
</details>
)}
</div>
)}
{/* Batch Summary */}
{batchSummary && (
<div className="rounded-lg border p-4 space-y-3 bg-card">
@@ -1535,6 +1618,19 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => setConfirmRecentResync(true)}
disabled={recentResyncing}
>
{recentResyncing ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<DatabaseZap className="mr-2 h-4 w-4" />
)}
{recentResyncing
? "Updating imported furni..."
: "Update imported furni"}
</DropdownMenuItem>
<DropdownMenuItem onClick={startReorganize} disabled={reorganizing}>
<FolderSync className="w-4 h-4 mr-2" />
{reorganizing ? "Reorganizing..." : "Reorganize Catalog"}
@@ -1936,6 +2032,30 @@ export function ImportFurniClient({ source }: { source: FurniImportSource }) {
</DialogContent>
</Dialog>
<Dialog open={confirmRecentResync} onOpenChange={setConfirmRecentResync}>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle>Update imported furni?</DialogTitle>
<DialogDescription>
This updates FurnitureData for furni imported during the last 7
days, then refreshes the emulator catalog and item caches. No
database rows or asset files are deleted.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setConfirmRecentResync(false)}
>
Cancel
</Button>
<Button onClick={resyncRecentImports} disabled={recentResyncing}>
Update last 7 days
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<Dialog
open={!!confirmDelete}