This commit is contained in:
2026-08-16 18:18:39 +02:00
parent b19373e2a4
commit d981147ff2
18 changed files with 20707 additions and 20497 deletions
+3 -23
View File
@@ -10,6 +10,7 @@ import (
"strings" "strings"
"github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/descrybe/descrybe-v2/apps/api/internal/billing"
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
@@ -34,30 +35,9 @@ type categoryBackfillResult struct {
} }
// backfillMappedCategoriesFromProcessed copies processed_products.category into // backfillMappedCategoriesFromProcessed copies processed_products.category into
// raw_products.mapped_data.category for A1 only. Legacy dumps store category on // raw_products.mapped_data.category for A1 only. Delegates to processing.
// processed rows (unique_id codes); feed mappings never mapped a category field,
// so re-processing without this backfill yields Uncategorized / grey C coverage.
func backfillMappedCategoriesFromProcessed(ctx context.Context, pg *pgxpool.Pool, companyID uuid.UUID) (int64, error) { func backfillMappedCategoriesFromProcessed(ctx context.Context, pg *pgxpool.Pool, companyID uuid.UUID) (int64, error) {
ct, err := pg.Exec(ctx, ` return processing.BackfillMappedCategoriesFromProcessed(ctx, pg, companyID)
UPDATE raw_products r
SET mapped_data = jsonb_set(
COALESCE(r.mapped_data, '{}'::jsonb),
'{category}',
to_jsonb(p.category),
true
),
updated_at = now()
FROM processed_products p
WHERE p.raw_product_id = r.id
AND p.company_id = $1
AND r.company_id = $1
AND COALESCE(NULLIF(trim(p.category), ''), '') <> ''
AND lower(trim(p.category)) <> 'none'
AND COALESCE(NULLIF(trim(r.mapped_data->>'category'), ''), '') = ''`, companyID)
if err != nil {
return 0, fmt.Errorf("backfill mapped category: %w", err)
}
return ct.RowsAffected(), nil
} }
// backfillCategoriesFromMySQLDump streams dump processed_products for the A1 // backfillCategoriesFromMySQLDump streams dump processed_products for the A1
+9
View File
@@ -358,6 +358,15 @@ func cloneCompanyCatalog(ctx context.Context, pool *pgxpool.Pool, src, dest uuid
} }
add("raw_products", ct.RowsAffected()) add("raw_products", ct.RowsAffected())
var mappedWithCat int64
if err := tx.QueryRow(ctx, `
SELECT COUNT(*) FROM raw_products
WHERE company_id = $1
AND COALESCE(NULLIF(trim(mapped_data->>'category'), ''), '') <> ''`, dest).Scan(&mappedWithCat); err != nil {
return nil, fmt.Errorf("count mapped categories: %w", err)
}
add("mapped_with_category", mappedWithCat)
ct, err = tx.Exec(ctx, ` ct, err = tx.Exec(ctx, `
INSERT INTO processed_products ( INSERT INTO processed_products (
id, company_id, user_id, product_id, name, category, description, processed_description, id, company_id, user_id, product_id, name, category, description, processed_description,
@@ -92,6 +92,67 @@ func BackfillProcessedCategoriesFromMapped(ctx context.Context, pool *pgxpool.Po
return out, nil return out, nil
} }
// BackfillMappedCategoriesFromProcessed copies processed_products.category into
// raw_products.mapped_data.category when mapped category is empty. Legacy A1 dumps
// often store category only on processed rows; feed mappings do not map category,
// so Fix Catalog / re-seed must push processed → mapped for Products UI coverage.
// Does not invent categories.
func BackfillMappedCategoriesFromProcessed(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (int64, error) {
if pool == nil {
return 0, fmt.Errorf("mapped category backfill: nil pool")
}
if companyID == uuid.Nil {
return 0, fmt.Errorf("mapped category backfill: empty company id")
}
ct, err := pool.Exec(ctx, `
UPDATE raw_products r
SET mapped_data = jsonb_set(
COALESCE(r.mapped_data, '{}'::jsonb),
'{category}',
to_jsonb(p.category),
true
),
updated_at = now()
FROM processed_products p
WHERE p.raw_product_id = r.id
AND p.company_id = $1
AND r.company_id = $1
AND COALESCE(NULLIF(trim(p.category), ''), '') <> ''
AND lower(trim(p.category)) <> 'none'
AND COALESCE(NULLIF(trim(r.mapped_data->>'category'), ''), '') = ''`, companyID)
if err != nil {
return 0, fmt.Errorf("mapped category backfill: %w", err)
}
return ct.RowsAffected(), nil
}
// CountMappedCategoryCoverage returns how many raw_products rows have a non-empty
// mapped_data.category vs empty, plus taxonomy row count for the company.
func CountMappedCategoryCoverage(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (withCat, withoutCat, taxonomy int64, err error) {
if pool == nil {
return 0, 0, 0, fmt.Errorf("mapped category coverage: nil pool")
}
err = pool.QueryRow(ctx, `
SELECT
COUNT(*) FILTER (
WHERE COALESCE(NULLIF(trim(mapped_data->>'category'), ''), '') <> ''
),
COUNT(*) FILTER (
WHERE COALESCE(NULLIF(trim(mapped_data->>'category'), ''), '') = ''
)
FROM raw_products
WHERE company_id = $1`, companyID).Scan(&withCat, &withoutCat)
if err != nil {
return 0, 0, 0, fmt.Errorf("mapped category coverage: %w", err)
}
err = pool.QueryRow(ctx, `
SELECT COUNT(*) FROM categories WHERE company_id = $1`, companyID).Scan(&taxonomy)
if err != nil {
return withCat, withoutCat, 0, fmt.Errorf("taxonomy count: %w", err)
}
return withCat, withoutCat, taxonomy, nil
}
// CountProcessedCategoryCoverage returns how many processed rows for companyID have // CountProcessedCategoryCoverage returns how many processed rows for companyID have
// a non-empty category vs empty/"none". // a non-empty category vs empty/"none".
func CountProcessedCategoryCoverage(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (withCat, withoutCat int64, err error) { func CountProcessedCategoryCoverage(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (withCat, withoutCat int64, err error) {
@@ -470,3 +470,82 @@ func TestBackfillProcessedCategoriesFromMapped(t *testing.T) {
t.Fatalf("coverage with=%d without=%d", withCat, withoutCat) t.Fatalf("coverage with=%d without=%d", withCat, withoutCat)
} }
} }
func TestBackfillMappedCategoriesFromProcessed(t *testing.T) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
t.Skip("DATABASE_URL not set")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
pg, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer pg.Close()
companyID := uuid.New()
rawID := uuid.New()
ppID := uuid.New()
gtin := "map-bf-" + companyID.String()[:8]
if _, err := pg.Exec(ctx, `INSERT INTO companies (id, name) VALUES ($1, $2)`, companyID, "mapped-backfill-test"); err != nil {
t.Fatal(err)
}
defer func() {
_, _ = pg.Exec(context.Background(), `DELETE FROM processed_products WHERE company_id = $1`, companyID)
_, _ = pg.Exec(context.Background(), `DELETE FROM raw_products WHERE company_id = $1`, companyID)
_, _ = pg.Exec(context.Background(), `DELETE FROM companies WHERE id = $1`, companyID)
}()
if _, err := pg.Exec(ctx, `
INSERT INTO raw_products (id, company_id, gtin, raw_data, mapped_data, is_processed, processing_status)
VALUES ($1, $2, $3, '{}'::jsonb, '{"name":"Widget"}'::jsonb, true, 'processed')`,
rawID, companyID, gtin); err != nil {
t.Fatal(err)
}
if _, err := pg.Exec(ctx, `
INSERT INTO processed_products (
id, company_id, product_id, name, category, description, processed_name, processed_description,
raw_product_id, status, attributes, processed_attributes, field_sources
) VALUES (
$1, $2, $3, 'Widget', '42', 'x', 'Widget', 'x',
$4, 'needs_review', '{}'::jsonb, '{}'::jsonb, '{}'::jsonb
)`, ppID, companyID, gtin, rawID); err != nil {
t.Fatal(err)
}
n, err := BackfillMappedCategoriesFromProcessed(ctx, pg, companyID)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("updated=%d want 1", n)
}
var mappedCat string
if err := pg.QueryRow(ctx, `
SELECT COALESCE(mapped_data->>'category', '') FROM raw_products WHERE id = $1`, rawID).Scan(&mappedCat); err != nil {
t.Fatal(err)
}
if mappedCat != "42" {
t.Fatalf("mapped category=%q want 42", mappedCat)
}
n2, err := BackfillMappedCategoriesFromProcessed(ctx, pg, companyID)
if err != nil {
t.Fatal(err)
}
if n2 != 0 {
t.Fatalf("second backfill updated=%d want 0", n2)
}
withCat, withoutCat, taxonomy, err := CountMappedCategoryCoverage(ctx, pg, companyID)
if err != nil {
t.Fatal(err)
}
if withCat != 1 || withoutCat != 0 || taxonomy != 0 {
t.Fatalf("mapped coverage with=%d without=%d taxonomy=%d", withCat, withoutCat, taxonomy)
}
}
@@ -41,9 +41,19 @@ type FixCompanyCatalogResult struct {
WeakHashesCleared int `json:"weak_hashes_cleared"` WeakHashesCleared int `json:"weak_hashes_cleared"`
CategoriesBackfilled int `json:"categories_backfilled"` CategoriesBackfilled int `json:"categories_backfilled"`
// MappedCategoriesBackfilled is processed → mapped_data.category copies.
MappedCategoriesBackfilled int `json:"mapped_categories_backfilled"`
// Coverage after Fix (Products UI reads mapped_data.category for raw rows).
TaxonomyCategories int64 `json:"taxonomy_categories"`
MappedWithCategory int64 `json:"mapped_with_category"`
MappedWithoutCategory int64 `json:"mapped_without_category"`
ProcessedWithCategory int64 `json:"processed_with_category"`
ProcessedWithoutCategory int64 `json:"processed_without_category"`
// Hashes / Categories alias the long names for flash.admin.fixA1Success. // Hashes / Categories alias the long names for flash.admin.fixA1Success.
Hashes int `json:"hashes"` Hashes int `json:"hashes"`
Categories int `json:"categories"` Categories int `json:"categories"`
// MappedBackfilled aliases mapped_categories_backfilled for flash {mapped_backfilled}.
MappedBackfilled int `json:"mapped_backfilled"`
DescriptionsNormalized int `json:"descriptions_normalized"` DescriptionsNormalized int `json:"descriptions_normalized"`
DescriptionsBackfilled int `json:"descriptions_backfilled"` DescriptionsBackfilled int `json:"descriptions_backfilled"`
@@ -106,6 +116,14 @@ func FixCompanyCatalog(ctx context.Context, pool *pgxpool.Pool, companyID uuid.U
out.MetaBackfilled = hygiene.MetaBackfilled out.MetaBackfilled = hygiene.MetaBackfilled
out.ProductsScanned = hygiene.ProductsScanned out.ProductsScanned = hygiene.ProductsScanned
if opts.BackfillCategories {
mappedN, err := BackfillMappedCategoriesFromProcessed(ctx, pool, companyID)
if err != nil {
return out, fmt.Errorf("mapped category backfill: %w", err)
}
out.MappedCategoriesBackfilled = int(mappedN)
}
sanitized, err := BackfillCompanyProcessedAttributes(ctx, pool, companyID) sanitized, err := BackfillCompanyProcessedAttributes(ctx, pool, companyID)
if err != nil { if err != nil {
return out, fmt.Errorf("sanitize attributes: %w", err) return out, fmt.Errorf("sanitize attributes: %w", err)
@@ -119,9 +137,24 @@ func FixCompanyCatalog(ctx context.Context, pool *pgxpool.Pool, companyID uuid.U
out.ReprocessNeededCount = needed out.ReprocessNeededCount = needed
out.ReprocessSampleRawProductIDs = sample out.ReprocessSampleRawProductIDs = sample
// Short keys for flash.admin.fixA1Success {prompts,hashes,categories}. mWith, mWithout, taxonomy, err := CountMappedCategoryCoverage(ctx, pool, companyID)
if err != nil {
return out, err
}
out.MappedWithCategory = mWith
out.MappedWithoutCategory = mWithout
out.TaxonomyCategories = taxonomy
pWith, pWithout, err := CountProcessedCategoryCoverage(ctx, pool, companyID)
if err != nil {
return out, err
}
out.ProcessedWithCategory = pWith
out.ProcessedWithoutCategory = pWithout
// Short keys for flash.admin.fixA1Success {prompts,hashes,categories,mapped_backfilled}.
out.Prompts = out.CategoryPromptsUpdated out.Prompts = out.CategoryPromptsUpdated
out.Hashes = out.WeakHashesCleared out.Hashes = out.WeakHashesCleared
out.Categories = out.CategoriesBackfilled out.Categories = out.CategoriesBackfilled
out.MappedBackfilled = out.MappedCategoriesBackfilled
return out, nil return out, nil
} }
+18 -1
View File
@@ -598,6 +598,7 @@ func (p *Pipeline) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
return err return err
} }
if !IsProcessableJobStatus(status) { if !IsProcessableJobStatus(status) {
log.Printf("processing: skip job=%s status=%s reason=not_processable", jobID, status)
return nil return nil
} }
@@ -625,6 +626,9 @@ func (p *Pipeline) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
modeLabel = label modeLabel = label
} }
log.Printf("processing: start job=%s company=%s type=%s mode=%s prior_processed=%d",
jobID, companyID, processingType, modeLabel, alreadyProcessed)
progress := InitialStepProgress(processingType) progress := InitialStepProgress(processingType)
if len(progress) > 0 { if len(progress) > 0 {
progress[0].Status = "running" progress[0].Status = "running"
@@ -648,6 +652,11 @@ func (p *Pipeline) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
batch := resolveBatchSize(p.BatchSize) batch := resolveBatchSize(p.BatchSize)
progressEvery := resolveProgressEvery(p.ProgressEvery) progressEvery := resolveProgressEvery(p.ProgressEvery)
jobCache := p.loadJobScopedCache(ctx, companyID, jobID) jobCache := p.loadJobScopedCache(ctx, companyID, jobID)
if !jobCache.stepPolicy().AllowAI {
log.Printf("processing: ai_skip job=%s reason=entitlement_can_use_ai", jobID)
} else if !jobEngine.CompleterEnabled() {
log.Printf("processing: ai_skip job=%s reason=openai_not_configured", jobID)
}
processed := alreadyProcessed processed := alreadyProcessed
failed := 0 failed := 0
tokenTotal := 0 tokenTotal := 0
@@ -672,6 +681,7 @@ func (p *Pipeline) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
flushProgress(true) flushProgress(true)
if errors.Is(err, errJobCancelled) { if errors.Is(err, errJobCancelled) {
_ = p.reclaimOrphanedProcessingItems(ctx, jobID) _ = p.reclaimOrphanedProcessingItems(ctx, jobID)
log.Printf("processing: cancelled job=%s", jobID)
return nil return nil
} }
return fmt.Errorf("processing: check cancel job=%s: %w", jobID, err) return fmt.Errorf("processing: check cancel job=%s: %w", jobID, err)
@@ -728,6 +738,7 @@ func (p *Pipeline) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
flushProgress(true) flushProgress(true)
if errors.Is(err, errJobCancelled) { if errors.Is(err, errJobCancelled) {
_ = p.reclaimOrphanedProcessingItems(ctx, jobID) _ = p.reclaimOrphanedProcessingItems(ctx, jobID)
log.Printf("processing: cancelled job=%s", jobID)
return nil return nil
} }
return fmt.Errorf("processing: check cancel job=%s: %w", jobID, err) return fmt.Errorf("processing: check cancel job=%s: %w", jobID, err)
@@ -807,7 +818,8 @@ func (p *Pipeline) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
current_step = $6, step_progress = $7::jsonb, current_step = $6, step_progress = $7::jsonb,
ai_provider_mode = $8 ai_provider_mode = $8
WHERE id = $1 AND status = 'running'`, jobID, finalStatus, processed, errMsg, tokenTotal, finalStep, finalJSON, finalMode) WHERE id = $1 AND status = 'running'`, jobID, finalStatus, processed, errMsg, tokenTotal, finalStep, finalJSON, finalMode)
log.Printf("processing: finished job=%s status=%s processed=%d failed=%d mode=%s", jobID, finalStatus, processed, failed, finalMode) log.Printf("processing: finished job=%s status=%s processed=%d failed=%d mode=%s event=%s",
jobID, finalStatus, processed, failed, finalMode, finalStatus)
return err return err
} }
@@ -1556,7 +1568,12 @@ func (p *Pipeline) processOne(ctx context.Context, companyID, jobID uuid.UUID, i
// name tokens are not wiped. Empty taxonomy set skips filtering (unit tests). // name tokens are not wiped. Empty taxonomy set skips filtering (unit tests).
if cache != nil { if cache != nil {
coerceCategoryToCompanyUniqueID(&result, cache.categoryNamesByUID, cache.categoryUniqueIDs) coerceCategoryToCompanyUniqueID(&result, cache.categoryNamesByUID, cache.categoryUniqueIDs)
catPreFilter := strings.TrimSpace(result.Category)
filterCategoryIfInvalid(&result, cache.categoryUniqueIDs) filterCategoryIfInvalid(&result, cache.categoryUniqueIDs)
if catPreFilter != "" && strings.TrimSpace(result.Category) == "" {
log.Printf("processing: category_empty job=%s raw=%s rejected=%s reason=unknown_unique_id",
jobID, it.RawID, catPreFilter)
}
syncCategoryName(&result, cache.categoryNamesByUID) syncCategoryName(&result, cache.categoryNamesByUID)
} }
// Re-apply after category validation so allowlist matches the persisted category. // Re-apply after category validation so allowlist matches the persisted category.
+8
View File
@@ -246,6 +246,14 @@ export type FixCatalogResult = {
/** Alias of categories_backfilled for flash {categories}. */ /** Alias of categories_backfilled for flash {categories}. */
categories?: number; categories?: number;
categories_backfilled?: number; categories_backfilled?: number;
/** Alias of mapped_categories_backfilled for flash {mapped_backfilled}. */
mapped_backfilled?: number;
mapped_categories_backfilled?: number;
taxonomy_categories?: number;
mapped_with_category?: number;
mapped_without_category?: number;
processed_with_category?: number;
processed_without_category?: number;
attributes_sanitized?: number; attributes_sanitized?: number;
products_scanned?: number; products_scanned?: number;
reprocess_needed_count?: number; reprocess_needed_count?: number;
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -842,9 +842,9 @@ export const en: MessageDict = {
"admin.users.fixA1": "Fix A1 catalog", "admin.users.fixA1": "Fix A1 catalog",
"admin.users.fixA1Aria": "Repair AI prompts, categories, and enhance hashes for {name}", "admin.users.fixA1Aria": "Repair AI prompts, categories, and enhance hashes for {name}",
"admin.users.fixA1Title": "Fix company catalog", "admin.users.fixA1Title": "Fix company catalog",
"admin.users.fixA1Desc": "In-place repair: category attribute links, category enhance prompts (with attrs), weak enhance hashes, optional category backfill, attribute sanitize. No mass reprocess.", "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.fixA1Company": "Company: {name}",
"admin.users.fixA1Warning": "Prefer Platform Demo; A1 may be targeted in place with confirm. Never clears the catalog or uses A1 as a clone destination.", "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.fixA1Cancel": "Cancel",
"admin.users.fixA1Confirm": "Fix catalog", "admin.users.fixA1Confirm": "Fix catalog",
"admin.users.noUsers": "No users match this filter.", "admin.users.noUsers": "No users match this filter.",
@@ -2333,9 +2333,10 @@ export const en: MessageDict = {
"flash.admin.staffRoleUpdated": "Staff role updated for {email}.", "flash.admin.staffRoleUpdated": "Staff role updated for {email}.",
"flash.admin.staffRoleUnavailable": "Staff role updates are not available on this server yet.", "flash.admin.staffRoleUnavailable": "Staff role updates are not available on this server yet.",
"flash.admin.planAssigned": "Plan assigned to {name}.", "flash.admin.planAssigned": "Plan assigned to {name}.",
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} categories. Open Products and process catalog GTINs (EAN-only API calls need a matching feed product).", "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} category prompts updated, {hashes} hashes cleared, {categories} categories backfilled.", "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.fixA1Error": "Catalog repair 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.planAssignedShort": "Plan assigned.",
"flash.admin.creditsUpdated": "Credits updated.", "flash.admin.creditsUpdated": "Credits updated.",
"flash.admin.cyclesProcessed": "Billing cycles processed: {count}", "flash.admin.cyclesProcessed": "Billing cycles processed: {count}",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -7
View File
@@ -1,15 +1,16 @@
import type { MessageDict } from "./types.ts"; import type { MessageDict } from "./types.ts";
/** Slovenian (sl) UI strings for admin Fix A1 ready pack. Not registered in UI_LOCALES yet (avoid inventing locale switcher). */ /** Slovenian (sl) UI strings for admin Fix A1 — ready pack. Not registered in UI_LOCALES yet (avoid inventing locale switcher). */
export const sl: MessageDict = { export const sl: MessageDict = {
"admin.users.fixA1": "Popravi katalog A1", "admin.users.fixA1": "Popravi katalog A1",
"admin.users.fixA1Aria": "Popravi AI pozive, kategorije in zgoščevalne vrednosti za {name}", "admin.users.fixA1Aria": "Popravi AI pozive, kategorije in zgoščevalne vrednosti za {name}",
"admin.users.fixA1Title": "Popravi katalog podjetja", "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.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.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.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.fixA1Cancel": "Prekliči",
"admin.users.fixA1Confirm": "Popravi katalog", "admin.users.fixA1Confirm": "Popravi katalog",
"flash.admin.fixA1Success": "Popravilo kataloga za {name} končano: posodobljenih pozivov {prompts}, počiščenih zgoščevalnih vrednosti {hashes}, dopolnjenih kategorij {categories}.", "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.", "flash.admin.fixA1Error": "Popravilo kataloga za {name} ni uspelo.",
"flash.admin.cloneDestMissing": "Ni ciljnega sandbox podjetja. Preklopite na Platform Demo, nato klonirajte.",
}; };
+15 -2
View File
@@ -384,11 +384,18 @@
reprocessSampleLimit: 25 reprocessSampleLimit: 25
}); });
const r = res.result; 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", { success = i18n.t("flash.admin.fixA1Success", {
name: targetName, name: targetName,
prompts: String(r.prompts ?? r.category_prompts_updated ?? 0), prompts: String(r.prompts ?? r.category_prompts_updated ?? 0),
hashes: String(r.hashes ?? r.weak_hashes_cleared ?? 0), hashes: String(r.hashes ?? r.weak_hashes_cleared ?? 0),
categories: String(r.categories ?? r.categories_backfilled ?? 0) categories: String(r.categories ?? r.categories_backfilled ?? 0),
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)
}); });
fixOpen = false; fixOpen = false;
fixCompany = null; fixCompany = null;
@@ -410,17 +417,23 @@
me?.staff_home_company_id?.trim() || me?.staff_home_company_id?.trim() ||
me?.staff_home_company?.id?.trim() || me?.staff_home_company?.id?.trim() ||
undefined; undefined;
if (!destId && me?.company?.id && me.company.id === cloneCompany.id) {
error = i18n.t("flash.admin.cloneDestMissing");
return;
}
const res = await cloneAdminCompanyCatalog(cloneCompany.id, { const res = await cloneAdminCompanyCatalog(cloneCompany.id, {
destCompanyId: destId destCompanyId: destId
}); });
const products = Number(res.counts?.raw_products ?? 0); const products = Number(res.counts?.raw_products ?? 0);
const categories = Number(res.counts?.categories ?? 0); const categories = Number(res.counts?.categories ?? 0);
const mappedWith = Number(res.counts?.mapped_with_category ?? 0);
const activeDest = (res.active_company_id || res.dest_company_id || "").trim(); const activeDest = (res.active_company_id || res.dest_company_id || "").trim();
success = i18n.t("flash.admin.catalogCloned", { success = i18n.t("flash.admin.catalogCloned", {
source: cloneCompany.name, source: cloneCompany.name,
dest: cloneDestLabel, dest: cloneDestLabel,
products: String(products), products: String(products),
categories: String(categories) categories: String(categories),
mapped_with: String(mappedWith)
}); });
cloneOpen = false; cloneOpen = false;
cloneCompany = null; cloneCompany = null;