fix
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+1912
-1911
File diff suppressed because it is too large
Load Diff
@@ -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}",
|
||||||
|
|||||||
+2470
-2469
File diff suppressed because it is too large
Load Diff
+3228
-3227
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+5179
-5178
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+3267
-3266
File diff suppressed because it is too large
Load Diff
+2676
-2675
File diff suppressed because it is too large
Load Diff
@@ -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.",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user