diff --git a/src/components/admin/studio/studio-client.tsx b/src/components/admin/studio/studio-client.tsx
index dd5513aa80..4e77a1af36 100644
--- a/src/components/admin/studio/studio-client.tsx
+++ b/src/components/admin/studio/studio-client.tsx
@@ -881,14 +881,14 @@ export function StudioClient({
className="gap-1.5"
disabled={organizing || !!batchProgress}
onClick={organizeAll}
- title="Alle meubels automatisch in catalogus-pagina's ordenen (pagina's worden aangemaakt indien nodig)"
+ title="Alle meubels uit de Import-sectie automatisch ordenen in catalogus-pagina's (pagina's worden aangemaakt indien nodig)"
>
{organizing ? (
) : (
)}
- Organiseer alles
+ Organiseer import
{selectedNonImported > 0 && (
diff --git a/src/lib/services/catalog-repair.ts b/src/lib/services/catalog-repair.ts
index 6e4fc372ea..c3c52d1d18 100644
--- a/src/lib/services/catalog-repair.ts
+++ b/src/lib/services/catalog-repair.ts
@@ -1,14 +1,11 @@
-import { readdir } from "node:fs/promises";
import { sql } from "drizzle-orm";
import { db } from "@/lib/db";
-import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
import {
readFurniData,
withFurniDataLock,
writeFurniData,
} from "@/lib/services/furni-data";
import {
- allocateCatalogItemId,
autoPriceFurni,
CATEGORY_PAGE,
classifyFurni,
@@ -17,7 +14,6 @@ import {
resetCatalogPageCache,
} from "@/lib/services/furni-import";
import {
- assetNameCandidates,
classifyCatalogItem,
NO_CATALOG_KINDS,
} from "@/lib/services/item-kind";
@@ -466,18 +462,17 @@ export interface OrganizeCatalogResult {
}
/**
- * Fully automatic one-shot organization of the entire `items_base` table into
- * the catalog. For every furniture item it:
- * 1. ensures the "Imported Furniture" parent page and the correct target
- * sub-page exist (creating it if missing). The target is the item's
- * furniture line (e.g. "weebz", "habbox") when known, otherwise its
- * auto-detected category (e.g. "Room & Building", "Rares"),
- * 2. creates a catalog_items entry if the item isn't in the catalog yet
- * (placed on its line/category sub-page with the auto price),
- * 3. re-homes and re-prices any existing catalog entry so it sits on its
+ * Fully automatic one-shot organization of everything that already lives under
+ * the "Imported Furniture" section of the catalog. For each imported item it:
+ * 1. ensures the correct target sub-page exists (creating it if missing).
+ * The target is the item's furniture line (e.g. "weebz", "habbox") when
+ * known and large enough, otherwise its auto-detected category
+ * (e.g. "Room & Building", "Rares"),
+ * 2. re-homes and re-prices any existing catalog entry so it sits on its
* correct sub-page with the canonical auto-detected price.
*
- * No manual selection required — runs over the entire database.
+ * Only items already present in the Imported section are touched — no new
+ * items are pulled in from `items_base`. No manual selection required.
*/
export async function organizeCatalogAll(): Promise {
// 1) Parent page (sub-pages are created on demand below).
@@ -577,7 +572,6 @@ export async function organizeCatalogAll(): Promise {
unknown,
];
- const idsInCatalog = new Set();
const catalogPageByItem = new Map();
const catalogCreditsByItem = new Map();
for (const row of catalogRows) {
@@ -586,7 +580,6 @@ export async function organizeCatalogAll(): Promise {
.map((s) => Number(s.trim()))
.filter(Boolean);
for (const id of ids) {
- idsInCatalog.add(id);
if (!catalogPageByItem.has(id)) {
catalogPageByItem.set(id, Number(row.page_id));
catalogCreditsByItem.set(id, Number(row.cost_credits));
@@ -598,11 +591,33 @@ export async function organizeCatalogAll(): Promise {
`)) as unknown as [Array<{ id: number }>, unknown];
const validPageIds = new Set(validPages.map((r) => Number(r.id)));
- // Count items per furniture line so we only spin up a dedicated line page
- // for lines that actually have enough items. Everything else falls back to
- // the category page.
+ // Only items already living under the Imported Furniture section are
+ // reorganized. An item counts as "in import" when its catalog entry sits on
+ // a sub-page (or the parent) of the imported parent page.
+ const [importedPages] = (await db.execute(sql`
+ SELECT id FROM catalog_pages
+ WHERE parent_id = ${parentPageId} OR id = ${parentPageId}
+ `)) as unknown as [Array<{ id: number }>, unknown];
+ const importedPageIds = new Set(importedPages.map((r) => Number(r.id)));
+ const importedItemIds = new Set();
+ for (const row of catalogRows) {
+ if (!importedPageIds.has(Number(row.page_id))) continue;
+ for (const id of (row.item_ids ?? "")
+ .split(";")
+ .map((s) => Number(s.trim()))
+ .filter(Boolean)) {
+ importedItemIds.add(id);
+ }
+ }
+ const itemById = new Map(items.map((it) => [Number(it.id), it]));
+
+ // Count imported items per furniture line so we only spin up a dedicated
+ // line page for lines that actually have enough items. Everything else
+ // falls back to the category page.
const lineCounts = new Map();
- for (const item of items) {
+ for (const id of importedItemIds) {
+ const item = itemById.get(id);
+ if (!item) continue;
if (NO_CATALOG_KINDS.has(classifyCatalogItem(item))) continue;
const fl = resolveFurniline(item.item_name);
if (fl) lineCounts.set(fl, (lineCounts.get(fl) ?? 0) + 1);
@@ -612,12 +627,7 @@ export async function organizeCatalogAll(): Promise {
if (count >= LINE_PAGE_MIN_ITEMS) qualifyingLines.add(line);
}
- // Nitro presence (pets are purchasable without a furni .nitro bundle).
- const { nitroDir } = await getFurniAssetDirs();
- const nitroFiles = await readdir(nitroDir).catch(() => [] as string[]);
- const nitroSet = new Set(nitroFiles);
-
- let catalogAdded = 0;
+ const catalogAdded = 0;
let catalogRehomed = 0;
for (const item of items) {
@@ -625,17 +635,10 @@ export async function organizeCatalogAll(): Promise {
if (NO_CATALOG_KINDS.has(kind)) continue;
const itemId = Number(item.id);
- const inCatalog = idsInCatalog.has(itemId);
- // Items not yet in the catalog are only added when they are actually
- // purchasable furniture (have a .nitro or are a pet).
- if (!inCatalog) {
- const hasNitro = assetNameCandidates(item.item_name).nitro.some((n) =>
- nitroSet.has(n),
- );
- const purchasable = kind === "pet" || hasNitro;
- if (!purchasable) continue;
- }
+ // Only reorganize items already in the Imported Furniture section.
+ // Never pull new items in from items_base.
+ if (!importedItemIds.has(itemId)) continue;
// Target page: the item's furniture line (e.g. "weebz", "habbox") when
// it qualifies for its own page, otherwise its auto-detected category.
@@ -662,17 +665,6 @@ export async function organizeCatalogAll(): Promise {
const pageId = await ensureSubPage(captionSave, caption);
const price = autoPriceFurni(item.item_name);
- if (!inCatalog) {
- await allocateCatalogItemId(async (nextId) => {
- await db.execute(sql`
- INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)
- VALUES (${nextId}, ${pageId}, ${itemId}, ${item.item_name}, ${price.credits}, ${price.points}, ${price.pointsType}, 1, 1, ${itemId}, '')
- `);
- });
- catalogAdded++;
- continue;
- }
-
// Re-classify onto the correct sub-page and re-apply the auto price if
// either drifted away from the canonical values.
const currentPage = catalogPageByItem.get(itemId);