This commit is contained in:
2026-08-16 18:36:56 +02:00
parent d981147ff2
commit a94a5aaad7
26 changed files with 904 additions and 507 deletions
+37 -11
View File
@@ -1,7 +1,7 @@
/**
* Admin orgs UI client — users + companies directory, staff roles, plan assign,
* clone-catalog, and Fix A1 (`POST …/fix-catalog`).
* Flash Fix A1: result.prompts / hashes / categories → flash.admin.fixA1Success.
* clone-catalog, and Sync A1 (`POST …/sync-a1`; fix-catalog is a compat alias).
* Flash Sync A1: result.prompts / hashes / categories / dump_* → flash.admin.syncA1Success.
* Contract: docs/admin-roles-support/04-contract.md · Docs: 10-admin-orgs-ui.md
*/
import { api, ApiError } from "$lib/api";
@@ -22,6 +22,8 @@ export const ADMIN_CLONE_CATALOG_PATH = (companyId: string) =>
`${ADMIN_COMPANIES_PATH}/${encodeURIComponent(companyId)}/clone-catalog`;
export const ADMIN_FIX_CATALOG_PATH = (companyId: string) =>
`${ADMIN_COMPANIES_PATH}/${encodeURIComponent(companyId)}/fix-catalog`;
export const ADMIN_SYNC_A1_PATH = (companyId: string) =>
`${ADMIN_COMPANIES_PATH}/${encodeURIComponent(companyId)}/sync-a1`;
export const ADMIN_STAFF_ROLE_PATH = (userId: string) =>
`/api/admin/users/${encodeURIComponent(userId)}/staff-role`;
@@ -230,13 +232,13 @@ export async function cloneAdminCompanyCatalog(
});
}
export type FixCatalogResult = {
export type SyncA1Result = {
company_id: string;
company_name: string;
a1_cohort?: boolean;
category_attribute_orphans_removed?: number;
category_attribute_links?: number;
/** Alias of category_prompts_updated for flash.admin.fixA1Success {prompts}. */
/** Alias of category_prompts_updated for flash.admin.syncA1Success {prompts}. */
prompts?: number;
category_prompts_updated?: number;
product_enhance_languages?: number;
@@ -258,23 +260,36 @@ export type FixCatalogResult = {
products_scanned?: number;
reprocess_needed_count?: number;
reprocess_sample_raw_product_ids?: string[];
dump_found?: boolean;
dump_path?: string;
dump_skipped_reason?: string;
dump_pairs?: number;
dump_mapped_updated?: number;
dump_processed_updated?: number;
dump_status?: string;
};
export type FixCatalogResponse = {
export type SyncA1Response = {
status: string;
result: FixCatalogResult;
result: SyncA1Result;
note?: string;
};
/** In-place Fix A1 / catalog hygiene. Never clears catalog; no mass reprocess. */
export async function fixAdminCompanyCatalog(
/** @deprecated Use SyncA1Result — kept for type aliases during UI rename. */
export type FixCatalogResult = SyncA1Result;
/** @deprecated Use SyncA1Response */
export type FixCatalogResponse = SyncA1Response;
/** Sync A1: dump category backfill (when dump on API host) + Fix hygiene. No mass reprocess. */
export async function syncAdminCompanyA1(
companyId: string,
opts?: { backfillCategories?: boolean; reprocessSampleLimit?: number }
): Promise<FixCatalogResponse> {
opts?: { backfillCategories?: boolean; reprocessSampleLimit?: number; skipDumpBackfill?: boolean }
): Promise<SyncA1Response> {
const body: {
confirm: true;
backfill_categories?: boolean;
reprocess_sample_limit?: number;
skip_dump_backfill?: boolean;
} = { confirm: true };
if (opts?.backfillCategories === false) {
body.backfill_categories = false;
@@ -282,12 +297,23 @@ export async function fixAdminCompanyCatalog(
if (typeof opts?.reprocessSampleLimit === "number") {
body.reprocess_sample_limit = opts.reprocessSampleLimit;
}
return api<FixCatalogResponse>(ADMIN_FIX_CATALOG_PATH(companyId), {
if (opts?.skipDumpBackfill) {
body.skip_dump_backfill = true;
}
return api<SyncA1Response>(ADMIN_SYNC_A1_PATH(companyId), {
method: "POST",
body
});
}
/** Compat alias — same as syncAdminCompanyA1 (fix-catalog route). */
export async function fixAdminCompanyCatalog(
companyId: string,
opts?: { backfillCategories?: boolean; reprocessSampleLimit?: number }
): Promise<SyncA1Response> {
return syncAdminCompanyA1(companyId, opts);
}
export function isStaffRoleApiUnavailable(err: unknown): boolean {
return err instanceof ApiError && (err.status === 404 || err.status === 501);
}
+10 -10
View File
@@ -812,14 +812,14 @@ export const de: MessageDict = {
"admin.users.cloneCatalogCancel": "Abbrechen",
"admin.users.cloneCatalogConfirm": "Katalog kopieren",
"admin.users.cloneDestFallback": "Ihr Sandbox-Unternehmen",
"admin.users.fixA1": "A1-Katalog reparieren",
"admin.users.fixA1Aria": "KI-Prompts, Kategorien und Enhance-Hashes für {name} reparieren",
"admin.users.fixA1Title": "Unternehmenskatalog reparieren",
"admin.users.fixA1Desc": "Wendet korrigierte KI-Prompts erneut an, löst schwache Enhance-Hashes und füllt Kategorien aus mapped_data nach.",
"admin.users.fixA1Company": "Unternehmen: {name}",
"admin.users.fixA1Warning": "Bevorzugen Sie Platform Demo oder ein explizites Unternehmen. Löscht keine Feeds, Mappings oder Rohprodukte. Schützt A1-Kohorten-Überschreibregeln.",
"admin.users.fixA1Cancel": "Abbrechen",
"admin.users.fixA1Confirm": "Katalog reparieren",
"admin.users.syncA1": "A1 synchronisieren",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1-Katalog synchronisieren",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Abbrechen",
"admin.users.syncA1Confirm": "A1 synchronisieren",
"admin.users.noUsers": "Keine Benutzer entsprechen diesem Filter.",
"admin.users.noCompanies": "Keine Unternehmen entsprechen diesem Filter.",
"admin.users.assignRoleTitle": "Mitarbeiterrolle zuweisen",
@@ -2305,8 +2305,8 @@ export const de: MessageDict = {
"flash.admin.staffRoleUnavailable": "Mitarbeiterrollen-Updates sind auf diesem Server noch nicht verfügbar.",
"flash.admin.planAssigned": "Plan {name} zugewiesen.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Katalogreparatur für {name} fehlgeschlagen.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "A1-Sync für {name} fehlgeschlagen.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssignedShort": "Plan zugewiesen.",
"flash.admin.creditsUpdated": "Credits aktualisiert.",
+10 -10
View File
@@ -839,14 +839,14 @@ export const en: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancel",
"admin.users.cloneCatalogConfirm": "Copy catalog",
"admin.users.cloneDestFallback": "your sandbox company",
"admin.users.fixA1": "Fix A1 catalog",
"admin.users.fixA1Aria": "Repair AI prompts, categories, and enhance hashes for {name}",
"admin.users.fixA1Title": "Fix company catalog",
"admin.users.fixA1Desc": "In-place repair: category attribute links, category enhance prompts, weak enhance hashes, bidirectional category backfill (mapped↔processed when data exists), attribute sanitize. No mass reprocess. Does not invent categories for unmapped SKUs.",
"admin.users.fixA1Company": "Company: {name}",
"admin.users.fixA1Warning": "Repairs existing category data only. Products UI uses mapped_data.category — Fix cannot invent missing feed categories. For full A1 coverage run seed-a1 -mode backfill-categories with the MySQL dump, then clone A1 → sandbox.",
"admin.users.fixA1Cancel": "Cancel",
"admin.users.fixA1Confirm": "Fix catalog",
"admin.users.syncA1": "Sync A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sync A1 catalog",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Cancel",
"admin.users.syncA1Confirm": "Sync A1",
"admin.users.noUsers": "No users match this filter.",
"admin.users.noCompanies": "No companies match this filter.",
"admin.users.assignRoleTitle": "Assign staff role",
@@ -2334,8 +2334,8 @@ export const en: MessageDict = {
"flash.admin.staffRoleUnavailable": "Staff role updates are not available on this server yet.",
"flash.admin.planAssigned": "Plan assigned to {name}.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} products with mapped category. Open Products — uncategorized SKUs need dump backfill or categorize, not another Fix.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} raw products have mapped categories ({taxonomy} taxonomy rows).",
"flash.admin.fixA1Error": "Catalog repair failed for {name}.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sync A1 failed for {name}.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch active company to Platform Demo (or your staff home), then clone again. Clone refuses to overwrite A1.",
"flash.admin.planAssignedShort": "Plan assigned.",
"flash.admin.creditsUpdated": "Credits updated.",
+10 -10
View File
@@ -812,14 +812,14 @@ export const es: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancelar",
"admin.users.cloneCatalogConfirm": "Copiar catálogo",
"admin.users.cloneDestFallback": "tu empresa sandbox",
"admin.users.fixA1": "Reparar catálogo A1",
"admin.users.fixA1Aria": "Reparar prompts de IA, categorías y hashes de enhance para {name}",
"admin.users.fixA1Title": "Reparar catálogo de la empresa",
"admin.users.fixA1Desc": "Vuelve a aplicar prompts de IA corregidos, limpia hashes de enhance débiles y completa categorías desde mapped_data.",
"admin.users.fixA1Company": "Empresa: {name}",
"admin.users.fixA1Warning": "Prefiera Platform Demo o una empresa explícita. No elimina feeds, mapeos ni productos en bruto. Protege las reglas de sobrescritura de la cohorte A1.",
"admin.users.fixA1Cancel": "Cancelar",
"admin.users.fixA1Confirm": "Reparar catálogo",
"admin.users.syncA1": "Sincronizar A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizar catálogo A1",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Cancelar",
"admin.users.syncA1Confirm": "Sincronizar A1",
"admin.users.noUsers": "Ningún usuario coincide con este filtro.",
"admin.users.noCompanies": "Ninguna empresa coincide con este filtro.",
"admin.users.assignRoleTitle": "Asignar rol de personal",
@@ -2304,8 +2304,8 @@ export const es: MessageDict = {
"flash.admin.staffRoleUpdated": "Rol de personal actualizado para {email}.",
"flash.admin.staffRoleUnavailable": "Las actualizaciones de rol de personal aún no están disponibles en este servidor.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Falló la reparación del catálogo de {name}.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Falló la sincronización A1 de {name}.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Plan asignado a {name}.",
"flash.admin.planAssignedShort": "Plan asignado.",
+10 -10
View File
@@ -812,14 +812,14 @@ export const fr: MessageDict = {
"admin.users.cloneCatalogCancel": "Annuler",
"admin.users.cloneCatalogConfirm": "Copier le catalogue",
"admin.users.cloneDestFallback": "votre entreprise sandbox",
"admin.users.fixA1": "Réparer le catalogue A1",
"admin.users.fixA1Aria": "Réparer les prompts IA, catégories et hashes enhance pour {name}",
"admin.users.fixA1Title": "Réparer le catalogue entreprise",
"admin.users.fixA1Desc": "Réapplique les prompts IA corrigés, efface les hashes enhance faibles et complète les catégories depuis mapped_data.",
"admin.users.fixA1Company": "Entreprise : {name}",
"admin.users.fixA1Warning": "Préférer Platform Demo ou une entreprise explicite. Ne supprime ni feeds, ni mappings, ni produits bruts. Protège les règles d'écrasement de la cohorte A1.",
"admin.users.fixA1Cancel": "Annuler",
"admin.users.fixA1Confirm": "Réparer le catalogue",
"admin.users.syncA1": "Synchroniser A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Synchroniser le catalogue A1",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Annuler",
"admin.users.syncA1Confirm": "Synchroniser A1",
"admin.users.noUsers": "Aucun utilisateur ne correspond à ce filtre.",
"admin.users.noCompanies": "Aucune entreprise ne correspond à ce filtre.",
"admin.users.assignRoleTitle": "Assigner un rôle du personnel",
@@ -2304,8 +2304,8 @@ export const fr: MessageDict = {
"flash.admin.staffRoleUpdated": "Rôle du personnel mis à jour pour {email}.",
"flash.admin.staffRoleUnavailable": "Les mises à jour de rôle personnel ne sont pas encore disponibles sur ce serveur.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Échec de la réparation du catalogue pour {name}.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Échec de la synchronisation A1 pour {name}.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Offre assignée à {name}.",
"flash.admin.planAssignedShort": "Offre assignée.",
+10 -10
View File
@@ -812,14 +812,14 @@ export const it: MessageDict = {
"admin.users.cloneCatalogCancel": "Annulla",
"admin.users.cloneCatalogConfirm": "Copia catalogo",
"admin.users.cloneDestFallback": "la tua azienda sandbox",
"admin.users.fixA1": "Ripara catalogo A1",
"admin.users.fixA1Aria": "Ripara prompt IA, categorie e hash enhance per {name}",
"admin.users.fixA1Title": "Ripara catalogo azienda",
"admin.users.fixA1Desc": "Riapplica i prompt IA corretti, cancella hash enhance deboli e completa le categorie da mapped_data.",
"admin.users.fixA1Company": "Azienda: {name}",
"admin.users.fixA1Warning": "Preferisci Platform Demo o un'azienda esplicita. Non elimina feed, mapping o prodotti grezzi. Protegge le regole di sovrascrittura della coorte A1.",
"admin.users.fixA1Cancel": "Annulla",
"admin.users.fixA1Confirm": "Ripara catalogo",
"admin.users.syncA1": "Sincronizza A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizza catalogo A1",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Annulla",
"admin.users.syncA1Confirm": "Sincronizza A1",
"admin.users.noUsers": "Nessun utente corrisponde a questo filtro.",
"admin.users.noCompanies": "Nessuna azienda corrisponde a questo filtro.",
"admin.users.assignRoleTitle": "Assegna ruolo staff",
@@ -2304,8 +2304,8 @@ export const it: MessageDict = {
"flash.admin.staffRoleUpdated": "Ruolo staff aggiornato per {email}.",
"flash.admin.staffRoleUnavailable": "Gli aggiornamenti del ruolo staff non sono ancora disponibili su questo server.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Riparazione catalogo per {name} non riuscita.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sincronizzazione A1 per {name} non riuscita.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Piano assegnato a {name}.",
"flash.admin.planAssignedShort": "Piano assegnato.",
+10 -10
View File
@@ -812,14 +812,14 @@ export const ja: MessageDict = {
"admin.users.cloneCatalogCancel": "キャンセル",
"admin.users.cloneCatalogConfirm": "カタログをコピー",
"admin.users.cloneDestFallback": "サンドボックス会社",
"admin.users.fixA1": "A1カタログを修復",
"admin.users.fixA1Aria": "{name} のAIプロンプト、カテゴリ、enhanceハッシュを修復",
"admin.users.fixA1Title": "会社カタログを修復",
"admin.users.fixA1Desc": "修正済みAIプロンプトを再適用し、弱いenhanceハッシュを消去し、mapped_dataからカテゴリを補完します。",
"admin.users.fixA1Company": "会社: {name}",
"admin.users.fixA1Warning": "Platform Demoまたは明示的な会社を優先してください。フィード、マッピング、生製品は削除しません。A1コホートの上書きルールを保護します。",
"admin.users.fixA1Cancel": "キャンセル",
"admin.users.fixA1Confirm": "カタログを修復",
"admin.users.syncA1": "A1を同期",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1カタログを同期",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "キャンセル",
"admin.users.syncA1Confirm": "A1を同期",
"admin.users.noUsers": "このフィルタに一致するユーザーはいません。",
"admin.users.noCompanies": "このフィルタに一致する会社はありません。",
"admin.users.assignRoleTitle": "スタッフロールを割り当て",
@@ -2304,8 +2304,8 @@ export const ja: MessageDict = {
"flash.admin.staffRoleUpdated": "{email} のスタッフロールを更新しました。",
"flash.admin.staffRoleUnavailable": "このサーバーではスタッフロールの更新はまだ利用できません。",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "{name} のカタログ修復に失敗しました。",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "{name} のA1同期に失敗しました。",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "{name} にプランを割り当てました。",
"flash.admin.planAssignedShort": "プランを割り当てました。",
+10 -10
View File
@@ -812,14 +812,14 @@ export const nl: MessageDict = {
"admin.users.cloneCatalogCancel": "Annuleren",
"admin.users.cloneCatalogConfirm": "Catalogus kopiëren",
"admin.users.cloneDestFallback": "je sandboxbedrijf",
"admin.users.fixA1": "A1-catalogus repareren",
"admin.users.fixA1Aria": "AI-prompts, categorieën en enhance-hashes voor {name} repareren",
"admin.users.fixA1Title": "Bedrijfscatalogus repareren",
"admin.users.fixA1Desc": "Past gecorrigeerde AI-prompts opnieuw toe, wist zwakke enhance-hashes en vult categorieën bij vanuit mapped_data.",
"admin.users.fixA1Company": "Bedrijf: {name}",
"admin.users.fixA1Warning": "Geef voorkeur aan Platform Demo of een expliciet bedrijf. Verwijdert geen feeds, mappings of ruwe producten. Beschermt A1-cohort-overschrijfregels.",
"admin.users.fixA1Cancel": "Annuleren",
"admin.users.fixA1Confirm": "Catalogus repareren",
"admin.users.syncA1": "A1 synchroniseren",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "A1-catalogus synchroniseren",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Annuleren",
"admin.users.syncA1Confirm": "A1 synchroniseren",
"admin.users.noUsers": "Geen gebruikers komen overeen met dit filter.",
"admin.users.noCompanies": "Geen bedrijven komen overeen met dit filter.",
"admin.users.assignRoleTitle": "Medewerkerrol toewijzen",
@@ -2304,8 +2304,8 @@ export const nl: MessageDict = {
"flash.admin.staffRoleUpdated": "Personeelsrol bijgewerkt voor {email}.",
"flash.admin.staffRoleUnavailable": "Personeelsrolupdates zijn op deze server nog niet beschikbaar.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Catalogusreparatie voor {name} mislukt.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "A1-synchronisatie voor {name} mislukt.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Plan toegewezen aan {name}.",
"flash.admin.planAssignedShort": "Plan toegewezen.",
+10 -10
View File
@@ -812,14 +812,14 @@ export const pl: MessageDict = {
"admin.users.cloneCatalogCancel": "Anuluj",
"admin.users.cloneCatalogConfirm": "Kopiuj katalog",
"admin.users.cloneDestFallback": "twoja firma sandbox",
"admin.users.fixA1": "Napraw katalog A1",
"admin.users.fixA1Aria": "Napraw prompty AI, kategorie i hashe enhance dla {name}",
"admin.users.fixA1Title": "Napraw katalog firmy",
"admin.users.fixA1Desc": "Ponownie stosuje poprawione prompty AI, czyści słabe hashe enhance i uzupełnia kategorie z mapped_data.",
"admin.users.fixA1Company": "Firma: {name}",
"admin.users.fixA1Warning": "Preferuj Platform Demo lub wskazaną firmę. Nie usuwa feedów, mapowań ani surowych produktów. Chroni reguły nadpisywania kohorty A1.",
"admin.users.fixA1Cancel": "Anuluj",
"admin.users.fixA1Confirm": "Napraw katalog",
"admin.users.syncA1": "Synchronizuj A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Synchronizuj katalog A1",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Anuluj",
"admin.users.syncA1Confirm": "Synchronizuj A1",
"admin.users.noUsers": "Żaden użytkownik nie pasuje do tego filtra.",
"admin.users.noCompanies": "Żadna firma nie pasuje do tego filtra.",
"admin.users.assignRoleTitle": "Przypisz rolÄ™ personelu",
@@ -2304,8 +2304,8 @@ export const pl: MessageDict = {
"flash.admin.staffRoleUpdated": "Zaktualizowano rolÄ™ personelu dla {email}.",
"flash.admin.staffRoleUnavailable": "Aktualizacje ról personelu nie są jeszcze dostępne na tym serwerze.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Naprawa katalogu dla {name} nie powiodła się.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Synchronizacja A1 dla {name} nie powiodła się.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Przypisano plan do {name}.",
"flash.admin.planAssignedShort": "Przypisano plan.",
+10 -10
View File
@@ -812,14 +812,14 @@ export const pt: MessageDict = {
"admin.users.cloneCatalogCancel": "Cancelar",
"admin.users.cloneCatalogConfirm": "Copiar catálogo",
"admin.users.cloneDestFallback": "a sua empresa sandbox",
"admin.users.fixA1": "Reparar catálogo A1",
"admin.users.fixA1Aria": "Reparar prompts de IA, categorias e hashes de enhance para {name}",
"admin.users.fixA1Title": "Reparar catálogo da empresa",
"admin.users.fixA1Desc": "Reaplica prompts de IA corrigidos, limpa hashes de enhance fracos e preenche categorias a partir de mapped_data.",
"admin.users.fixA1Company": "Empresa: {name}",
"admin.users.fixA1Warning": "Prefira Platform Demo ou uma empresa explícita. Não elimina feeds, mapeamentos ou produtos brutos. Protege as regras de substituição da coorte A1.",
"admin.users.fixA1Cancel": "Cancelar",
"admin.users.fixA1Confirm": "Reparar catálogo",
"admin.users.syncA1": "Sincronizar A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sincronizar catálogo A1",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Cancelar",
"admin.users.syncA1Confirm": "Sincronizar A1",
"admin.users.noUsers": "Nenhum utilizador corresponde a este filtro.",
"admin.users.noCompanies": "Nenhuma empresa corresponde a este filtro.",
"admin.users.assignRoleTitle": "Atribuir função de equipa",
@@ -2304,8 +2304,8 @@ export const pt: MessageDict = {
"flash.admin.staffRoleUpdated": "Função de pessoal atualizada para {email}.",
"flash.admin.staffRoleUnavailable": "As atualizações de função de pessoal ainda não estão disponíveis neste servidor.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
"flash.admin.fixA1Success": "Catalog repair finished for {name}: {prompts} prompts, {hashes} hashes, {categories} processed←mapped, {mapped_backfilled} mapped←processed. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.fixA1Error": "Falha na reparação do catálogo de {name}.",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Falha na sincronização A1 de {name}.",
"flash.admin.cloneDestMissing": "No sandbox destination. Switch to Platform Demo (staff home), then clone again.",
"flash.admin.planAssigned": "Plano atribuído a {name}.",
"flash.admin.planAssignedShort": "Plano atribuído.",
+10 -10
View File
@@ -2,15 +2,15 @@
/** Slovenian (sl) UI strings for admin Fix A1 — ready pack. Not registered in UI_LOCALES yet (avoid inventing locale switcher). */
export const sl: MessageDict = {
"admin.users.fixA1": "Popravi katalog A1",
"admin.users.fixA1Aria": "Popravi AI pozive, kategorije in zgoščevalne vrednosti za {name}",
"admin.users.fixA1Title": "Popravi katalog podjetja",
"admin.users.fixA1Desc": "Ponovno uporabi popravljene AI pozive, počisti šibke enhance zgoščevalne vrednosti in dopolni kategorije iz mapped podatkov.",
"admin.users.fixA1Company": "Podjetje: {name}",
"admin.users.fixA1Warning": "Raje Platform Demo ali izrecno podjetje. Ne briše feedov, mapiranj ali surovih izdelkov. Ščiti pravila prepisovanja A1 kohorte.",
"admin.users.fixA1Cancel": "Prekliči",
"admin.users.fixA1Confirm": "Popravi katalog",
"flash.admin.fixA1Success": "Popravilo kataloga za {name}: pozivi {prompts}, zgoščene {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Pokritost: {mapped_with}/{mapped_total} ({taxonomy} taksonomija).",
"flash.admin.fixA1Error": "Popravilo kataloga za {name} ni uspelo.",
"admin.users.syncA1": "Sinhroniziraj A1",
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
"admin.users.syncA1Title": "Sinhroniziraj katalog A1",
"admin.users.syncA1Desc": "Backfills mapped categories from the MySQL dump when present on the API host, then runs catalog hygiene (prompts, weak hashes, bidirectional category backfill, attribute sanitize). Does not wipe or reimport the full catalog.",
"admin.users.syncA1Company": "Company: {name}",
"admin.users.syncA1Warning": "Requires descrybe_new.sql on the API server (scripts/seed/ or SEED_A1_MYSQL_DUMP) for dump category coverage. Without a dump, only DB hygiene runs. No mass reprocess.",
"admin.users.syncA1Cancel": "Prekliči",
"admin.users.syncA1Confirm": "Sinhroniziraj A1",
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processed←mapped {categories}, mapped←processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
"flash.admin.syncA1Error": "Sinhronizacija A1 za {name} ni uspela.",
"flash.admin.cloneDestMissing": "Ni ciljnega sandbox podjetja. Preklopite na Platform Demo, nato klonirajte.",
};
@@ -1,5 +1,5 @@
/**
* Focused check: admin Fix A1 i18n keys exist in every UI_LOCALES pack.
/**
* Focused check: admin Sync A1 i18n keys exist in every UI_LOCALES pack.
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
@@ -9,32 +9,32 @@ import { en } from "./messages/en.ts";
import { loadAllMessages, messagesFor } from "./messages/catalog.ts";
import { sl } from "./messages/sl.ts";
const FIX_A1_KEYS = [
"admin.users.fixA1",
"admin.users.fixA1Aria",
"admin.users.fixA1Title",
"admin.users.fixA1Desc",
"admin.users.fixA1Company",
"admin.users.fixA1Warning",
"admin.users.fixA1Cancel",
"admin.users.fixA1Confirm",
"flash.admin.fixA1Success",
"flash.admin.fixA1Error"
const SYNC_A1_KEYS = [
"admin.users.syncA1",
"admin.users.syncA1Aria",
"admin.users.syncA1Title",
"admin.users.syncA1Desc",
"admin.users.syncA1Company",
"admin.users.syncA1Warning",
"admin.users.syncA1Cancel",
"admin.users.syncA1Confirm",
"flash.admin.syncA1Success",
"flash.admin.syncA1Error"
] as const;
describe("admin Fix A1 i18n keys", () => {
it("English defines every Fix A1 key", () => {
for (const key of FIX_A1_KEYS) {
describe("admin Sync A1 i18n keys", () => {
it("English defines every Sync A1 key", () => {
for (const key of SYNC_A1_KEYS) {
assert.equal(typeof en[key], "string", key);
assert.ok(en[key].trim().length > 0, key);
}
});
it("every registered UI locale has Fix A1 keys", async () => {
it("every registered UI locale has Sync A1 keys", async () => {
await loadAllMessages();
for (const { code } of UI_LOCALES) {
const pack = code === "en" ? en : messagesFor(code);
for (const key of FIX_A1_KEYS) {
for (const key of SYNC_A1_KEYS) {
const value = pack[key];
assert.equal(typeof value, "string", `${code}:${key}`);
assert.ok(String(value).trim().length > 0, `${code}:${key}`);
@@ -42,8 +42,8 @@ describe("admin Fix A1 i18n keys", () => {
}
});
it("Slovenian ready pack has Fix A1 keys (not in UI_LOCALES)", () => {
for (const key of FIX_A1_KEYS) {
it("Slovenian ready pack has Sync A1 keys (outside UI_LOCALES)", () => {
for (const key of SYNC_A1_KEYS) {
assert.equal(typeof sl[key], "string", key);
assert.ok(sl[key].trim().length > 0, key);
}
+42 -39
View File
@@ -11,7 +11,7 @@
STAFF_ROLE_OPTIONS,
assignAdminPlan,
cloneAdminCompanyCatalog,
fixAdminCompanyCatalog,
syncAdminCompanyA1,
companyPlanBadge,
isStaffRoleApiUnavailable,
listAdminCompanies,
@@ -55,7 +55,7 @@
TabsList,
TabsTrigger
} from "$lib/components/ui";
import { Building2, Copy, KeyRound, Search, Shield, UserPlus, Users, Wrench } from "@lucide/svelte";
import { Building2, Copy, KeyRound, RefreshCw, Search, Shield, UserPlus, Users } from "@lucide/svelte";
type TabKey = "users" | "companies";
@@ -94,8 +94,8 @@
let cloneOpen = $state(false);
let cloneCompany = $state<AdminOrgCompany | null>(null);
let fixOpen = $state(false);
let fixCompany = $state<AdminOrgCompany | null>(null);
let syncOpen = $state(false);
let syncCompany = $state<AdminOrgCompany | null>(null);
const usersPage = $derived(Math.floor(usersOffset / PAGE_SIZE) + 1);
const usersPages = $derived(Math.max(1, Math.ceil(usersTotal / PAGE_SIZE)));
@@ -366,28 +366,29 @@
success = "";
}
function openFixDialog(company: AdminOrgCompany) {
fixCompany = company;
fixOpen = true;
function openSyncDialog(company: AdminOrgCompany) {
syncCompany = company;
syncOpen = true;
error = "";
success = "";
}
async function confirmFixCatalog() {
if (!fixCompany) return;
async function confirmSyncA1() {
if (!syncCompany) return;
busy = true;
error = "";
success = "";
const targetName = fixCompany.name;
const targetName = syncCompany.name;
try {
const res = await fixAdminCompanyCatalog(fixCompany.id, {
const res = await syncAdminCompanyA1(syncCompany.id, {
reprocessSampleLimit: 25
});
const r = res.result;
const mappedWith = Number(r.mapped_with_category ?? 0);
const mappedWithout = Number(r.mapped_without_category ?? 0);
const mappedTotal = mappedWith + mappedWithout;
success = i18n.t("flash.admin.fixA1Success", {
const dumpStatus = String(r.dump_status ?? (r.dump_found ? "ok" : "missing"));
success = i18n.t("flash.admin.syncA1Success", {
name: targetName,
prompts: String(r.prompts ?? r.category_prompts_updated ?? 0),
hashes: String(r.hashes ?? r.weak_hashes_cleared ?? 0),
@@ -395,12 +396,14 @@
mapped_backfilled: String(r.mapped_backfilled ?? r.mapped_categories_backfilled ?? 0),
mapped_with: String(mappedWith),
mapped_total: String(mappedTotal),
taxonomy: String(r.taxonomy_categories ?? 0)
taxonomy: String(r.taxonomy_categories ?? 0),
dump_mapped: String(r.dump_mapped_updated ?? 0),
dump_status: dumpStatus
});
fixOpen = false;
fixCompany = null;
syncOpen = false;
syncCompany = null;
} catch (err) {
error = failureMessage(err, i18n.t("flash.admin.fixA1Error", { name: targetName }));
error = failureMessage(err, i18n.t("flash.admin.syncA1Error", { name: targetName }));
} finally {
busy = false;
}
@@ -827,18 +830,18 @@
<TableCell class="hidden text-muted-foreground lg:table-cell">{company.language || "—"}</TableCell>
<TableCell stickyRight>
<div class="flex flex-wrap items-center justify-end gap-1">
<Button
size="sm"
variant="outline"
onclick={() => openFixDialog(company)}
aria-label={i18n.t("admin.users.fixA1Aria", {
name: company.name
})}
data-testid="admin-fix-catalog"
>
<Wrench class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
<span class="hidden lg:inline">{i18n.t("admin.users.fixA1")}</span>
</Button>
<Button
size="sm"
variant="outline"
onclick={() => openSyncDialog(company)}
aria-label={i18n.t("admin.users.syncA1Aria", {
name: company.name
})}
data-testid="admin-sync-a1"
>
<RefreshCw class="h-3.5 w-3.5 lg:mr-1" aria-hidden="true" />
<span class="hidden lg:inline">{i18n.t("admin.users.syncA1")}</span>
</Button>
<Button
size="sm"
variant="outline"
@@ -1021,21 +1024,21 @@
</Dialog>
<Dialog
bind:open={fixOpen}
title={i18n.t("admin.users.fixA1Title")}
description={i18n.t("admin.users.fixA1Desc")}
bind:open={syncOpen}
title={i18n.t("admin.users.syncA1Title")}
description={i18n.t("admin.users.syncA1Desc")}
>
<div class="space-y-4">
{#if error}
<p class="text-sm text-destructive" role="alert">{error}</p>
{/if}
{#if fixCompany}
{#if syncCompany}
<p class="text-sm text-foreground">
{i18n.t("admin.users.fixA1Company", { name: fixCompany.name })}
{i18n.t("admin.users.syncA1Company", { name: syncCompany.name })}
</p>
{/if}
<p class="text-sm text-muted-foreground">
{i18n.t("admin.users.fixA1Warning")}
{i18n.t("admin.users.syncA1Warning")}
</p>
<div class="flex flex-wrap justify-end gap-2">
<Button
@@ -1043,14 +1046,14 @@
variant="outline"
disabled={busy}
onclick={() => {
fixOpen = false;
fixCompany = null;
syncOpen = false;
syncCompany = null;
}}
>
{i18n.t("admin.users.fixA1Cancel")}
{i18n.t("admin.users.syncA1Cancel")}
</Button>
<Button type="button" loading={busy} onclick={() => confirmFixCatalog()}>
{i18n.t("admin.users.fixA1Confirm")}
<Button type="button" loading={busy} onclick={() => confirmSyncA1()}>
{i18n.t("admin.users.syncA1Confirm")}
</Button>
</div>
</div>