Surface Slovenian A1 content in prompt page; purge English defaults
Three bugs hid the split A1 data behind English junk:
1. /api/auth/me company was only {id, name} — me.company.language never
existed, so the prompt/formula pages ALWAYS fell back to English as
the primary content language and never showed the Slovenian ("sl")
prompt sections by default. handleMe now enriches the active company
with language + content_languages from the companies table.
2. The unified prompt page injected English default texts: meta title /
meta description fields were pre-filled via getDefaultMetaTitle/
Description (and silently SAVED on Save), new description sections
started with English instructions, and type changes overwrote user
text with English defaults. All removed — empty stays empty
(placeholders only), stored tenant content is shown exactly as-is,
meta-only templates now save (sections no longer required).
3. Sync A1 / repair left English formula artifacts on categories with no
seed content ("Audio video" etc.): earlier repairs backfilled
DefaultRetailTitleFormula and cats.json-style English description
sections onto every category. Repair now (a) never invents a default
title formula — formulas derive ONLY from legacy naming rules — and
(b) with the seed as source of truth clears title_template and
description_template on non-seed categories (new
title/description_template_cleared counters).
Locally verified end-to-end: corrupted Platform Demo Monitorji (legacy
combined blob) + Audio video (English junk), called the real admin
POST /companies/{id}/sync-a1 as the platform admin — Monitorji restored
to the exact split Slovenian Title/Description/Meta + formulas +
Slovenian meta title/description, Audio video fully cleared; repair
idempotent (0 on re-run); me.company.language returns sl for A1;
zero English-default templates remain in A1 + Platform Demo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,10 @@ type RepairCategoryEnhancePromptsResult struct {
|
|||||||
FallbackTemplate int `json:"fallback_template"`
|
FallbackTemplate int `json:"fallback_template"`
|
||||||
TitleTemplateBackfill int `json:"title_template_backfill"`
|
TitleTemplateBackfill int `json:"title_template_backfill"`
|
||||||
DescTemplateBackfill int `json:"description_template_backfill"`
|
DescTemplateBackfill int `json:"description_template_backfill"`
|
||||||
|
// *_Cleared count non-seed categories whose leftover formula artifacts
|
||||||
|
// (English defaults from earlier repairs / cats.json / migration) were removed.
|
||||||
|
TitleTemplateCleared int `json:"title_template_cleared"`
|
||||||
|
DescTemplateCleared int `json:"description_template_cleared"`
|
||||||
ByCompany map[string]int `json:"by_company"`
|
ByCompany map[string]int `json:"by_company"`
|
||||||
DryRun bool `json:"dry_run"`
|
DryRun bool `json:"dry_run"`
|
||||||
SeedPath string `json:"seed_path,omitempty"`
|
SeedPath string `json:"seed_path,omitempty"`
|
||||||
@@ -524,7 +528,19 @@ func repairCompanyCategoryEnhancePrompts(
|
|||||||
if needDesc && strings.TrimSpace(legacyParts.DescriptionRules) == "" && strings.TrimSpace(legacyParts.MetaRules) == "" {
|
if needDesc && strings.TrimSpace(legacyParts.DescriptionRules) == "" && strings.TrimSpace(legacyParts.MetaRules) == "" {
|
||||||
needDesc = false
|
needDesc = false
|
||||||
}
|
}
|
||||||
if !needPrompt && !needTitle && !needDesc {
|
// Never invent a default title formula: formulas are derived ONLY from
|
||||||
|
// legacy naming rules. (Earlier repairs backfilled DefaultRetailTitleFormula
|
||||||
|
// with English labels onto every category — that junk gets cleared below.)
|
||||||
|
if strings.TrimSpace(titleRules) == "" {
|
||||||
|
needTitle = false
|
||||||
|
}
|
||||||
|
// Seed is the source of truth: categories with no seed/legacy content must
|
||||||
|
// not keep formula artifacts (English default sections/formulas from
|
||||||
|
// earlier repairs, cats.json seeding, or migration).
|
||||||
|
noLegacyContent := seedLegacy == "" && !legacyParts.WasLegacy && strings.TrimSpace(titleRules) == ""
|
||||||
|
clearTitleTpl := forceFromSeed && noLegacyContent && jsonbTemplatePresent(titleTpl)
|
||||||
|
clearDescTpl := forceFromSeed && noLegacyContent && jsonbTemplatePresent(descTpl)
|
||||||
|
if !needPrompt && !needTitle && !needDesc && !clearTitleTpl && !clearDescTpl {
|
||||||
if company.HasAnyPrompt(m) {
|
if company.HasAnyPrompt(m) {
|
||||||
out.AlreadyOK++
|
out.AlreadyOK++
|
||||||
} else {
|
} else {
|
||||||
@@ -564,6 +580,12 @@ func repairCompanyCategoryEnhancePrompts(
|
|||||||
if needDesc {
|
if needDesc {
|
||||||
out.DescTemplateBackfill++
|
out.DescTemplateBackfill++
|
||||||
}
|
}
|
||||||
|
if clearTitleTpl {
|
||||||
|
out.TitleTemplateCleared++
|
||||||
|
}
|
||||||
|
if clearDescTpl {
|
||||||
|
out.DescTemplateCleared++
|
||||||
|
}
|
||||||
updatedHere++
|
updatedHere++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -640,6 +662,26 @@ func repairCompanyCategoryEnhancePrompts(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if clearTitleTpl {
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
UPDATE categories
|
||||||
|
SET title_template = NULL, updated_at = now()
|
||||||
|
WHERE id = $1 AND company_id = $2`, id, companyID); err != nil {
|
||||||
|
return updatedHere, fmt.Errorf("clear title_template category=%s: %w", id, err)
|
||||||
|
}
|
||||||
|
out.TitleTemplateCleared++
|
||||||
|
}
|
||||||
|
|
||||||
|
if clearDescTpl {
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
UPDATE categories
|
||||||
|
SET description_template = NULL, updated_at = now()
|
||||||
|
WHERE id = $1 AND company_id = $2`, id, companyID); err != nil {
|
||||||
|
return updatedHere, fmt.Errorf("clear description_template category=%s: %w", id, err)
|
||||||
|
}
|
||||||
|
out.DescTemplateCleared++
|
||||||
|
}
|
||||||
|
|
||||||
out.Updated++
|
out.Updated++
|
||||||
updatedHere++
|
updatedHere++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,6 +340,24 @@ func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
|
|||||||
out["company"] = c
|
out["company"] = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Enrich with content language settings: the dashboard (category prompt /
|
||||||
|
// formula pages, language switchers) reads me.company.language as the
|
||||||
|
// primary content language — auth.Company alone is only {id, name},
|
||||||
|
// which silently fell back to English for every tenant.
|
||||||
|
if c, ok := out["company"].(auth.Company); ok {
|
||||||
|
var lang string
|
||||||
|
var contentLangs []string
|
||||||
|
if err := s.Pool.QueryRow(r.Context(), `
|
||||||
|
SELECT COALESCE(language, 'en'), COALESCE(content_languages, '{}')
|
||||||
|
FROM companies WHERE id = $1`, cid).Scan(&lang, &contentLangs); err == nil {
|
||||||
|
out["company"] = map[string]any{
|
||||||
|
"id": c.ID,
|
||||||
|
"name": c.Name,
|
||||||
|
"language": lang,
|
||||||
|
"content_languages": contentLangs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if credits, err := s.Billing.CreditsOverview(r.Context(), cid, s.Config.LowCreditsThreshold); err == nil {
|
if credits, err := s.Billing.CreditsOverview(r.Context(), cid, s.Config.LowCreditsThreshold); err == nil {
|
||||||
out["credits"] = credits
|
out["credits"] = credits
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@
|
|||||||
buildTemplateToSave,
|
buildTemplateToSave,
|
||||||
elementId,
|
elementId,
|
||||||
generatePreviewElements,
|
generatePreviewElements,
|
||||||
getDefaultInstructions,
|
|
||||||
getDefaultMetaDescription,
|
|
||||||
getDefaultMetaTitle,
|
|
||||||
parseDescriptionTemplate,
|
parseDescriptionTemplate,
|
||||||
parseTemplateToFormula
|
parseTemplateToFormula
|
||||||
} from "$lib/categories/formula";
|
} from "$lib/categories/formula";
|
||||||
@@ -137,10 +134,13 @@
|
|||||||
let confirmOpen = $state(false);
|
let confirmOpen = $state(false);
|
||||||
let itemToDelete = $state<{ type: string; id: string } | null>(null);
|
let itemToDelete = $state<{ type: string; id: string } | null>(null);
|
||||||
|
|
||||||
// Description formula (categories.description_template) — same editor as /description-formula.
|
// Description formula (categories.description_template) — same editor as
|
||||||
|
// /description-formula, but NEVER pre-filled with English default texts:
|
||||||
|
// empty stays empty (placeholders only) so stored Slovenian/tenant content
|
||||||
|
// is shown exactly as-is and defaults are never silently saved.
|
||||||
let descSections = $state<DescriptionSection[]>([]);
|
let descSections = $state<DescriptionSection[]>([]);
|
||||||
let metaTitle = $state(getDefaultMetaTitle());
|
let metaTitle = $state("");
|
||||||
let metaDescription = $state(getDefaultMetaDescription());
|
let metaDescription = $state("");
|
||||||
let dragIndex = $state<number | null>(null);
|
let dragIndex = $state<number | null>(null);
|
||||||
|
|
||||||
const langLabel = $derived(
|
const langLabel = $derived(
|
||||||
@@ -224,8 +224,14 @@
|
|||||||
titleFormula = parseTemplateToFormula(cat.title_template, customVariables);
|
titleFormula = parseTemplateToFormula(cat.title_template, customVariables);
|
||||||
const parsedDesc = parseDescriptionTemplate(cat.description_template);
|
const parsedDesc = parseDescriptionTemplate(cat.description_template);
|
||||||
descSections = parsedDesc.sections;
|
descSections = parsedDesc.sections;
|
||||||
metaTitle = parsedDesc.metaTitle;
|
// Raw stored values only — parseDescriptionTemplate injects English
|
||||||
metaDescription = parsedDesc.metaDescription;
|
// defaults for missing meta fields, which must never be displayed or saved.
|
||||||
|
const rawTpl = (cat.description_template ?? {}) as {
|
||||||
|
metaTitle?: unknown;
|
||||||
|
metaDescription?: unknown;
|
||||||
|
};
|
||||||
|
metaTitle = typeof rawTpl.metaTitle === "string" ? rawTpl.metaTitle : "";
|
||||||
|
metaDescription = typeof rawTpl.metaDescription === "string" ? rawTpl.metaDescription : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLangChange(code: string) {
|
function onLangChange(code: string) {
|
||||||
@@ -374,10 +380,9 @@
|
|||||||
|
|
||||||
// ----- Description formula editing -----
|
// ----- Description formula editing -----
|
||||||
function addDescSection() {
|
function addDescSection() {
|
||||||
descSections = [
|
// New sections start EMPTY — placeholder text guides the user; no
|
||||||
...descSections,
|
// English default instructions are ever inserted into stored content.
|
||||||
{ id: crypto.randomUUID(), type: "p", instructions: getDefaultInstructions("p") }
|
descSections = [...descSections, { id: crypto.randomUUID(), type: "p", instructions: "" }];
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDescSection(id: string) {
|
function removeDescSection(id: string) {
|
||||||
@@ -385,14 +390,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDescSection(id: string, updates: Partial<DescriptionSection>) {
|
function updateDescSection(id: string, updates: Partial<DescriptionSection>) {
|
||||||
descSections = descSections.map((section) => {
|
descSections = descSections.map((section) =>
|
||||||
if (section.id !== id) return section;
|
section.id === id ? { ...section, ...updates } : section
|
||||||
const next = { ...section, ...updates };
|
);
|
||||||
if (updates.type && updates.type !== section.type) {
|
|
||||||
next.instructions = getDefaultInstructions(updates.type);
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reorderDescSections(from: number, to: number) {
|
function reorderDescSections(from: number, to: number) {
|
||||||
@@ -403,11 +403,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildDescriptionTemplate() {
|
function buildDescriptionTemplate() {
|
||||||
if (descSections.length === 0) return null;
|
const keep = descSections.filter((s) => s.instructions.trim() || s.exportId?.trim());
|
||||||
|
const mt = metaTitle.trim();
|
||||||
|
const md = metaDescription.trim();
|
||||||
|
if (keep.length === 0 && !mt && !md) return null;
|
||||||
return {
|
return {
|
||||||
sections: descSections,
|
sections: keep,
|
||||||
metaTitle: metaTitle.trim() || undefined,
|
metaTitle: mt || undefined,
|
||||||
metaDescription: metaDescription.trim() || undefined
|
metaDescription: md || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,19 +823,9 @@
|
|||||||
rows="2"
|
rows="2"
|
||||||
placeholder={i18n.t("categories.seoTitlePlaceholder")}
|
placeholder={i18n.t("categories.seoTitlePlaceholder")}
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{i18n.t("categories.metaTitleHint")}
|
{i18n.t("categories.metaTitleHint")}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="text-xs"
|
|
||||||
onclick={() => (metaTitle = getDefaultMetaTitle())}
|
|
||||||
>
|
|
||||||
{i18n.t("categories.resetToDefault")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="metaDescription">{i18n.t("categories.metaDescriptionFormula")}</Label>
|
<Label for="metaDescription">{i18n.t("categories.metaDescriptionFormula")}</Label>
|
||||||
@@ -843,19 +836,9 @@
|
|||||||
rows="3"
|
rows="3"
|
||||||
placeholder={i18n.t("categories.seoDescriptionPlaceholder")}
|
placeholder={i18n.t("categories.seoDescriptionPlaceholder")}
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{i18n.t("categories.metaDescriptionHint")}
|
{i18n.t("categories.metaDescriptionHint")}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="text-xs"
|
|
||||||
onclick={() => (metaDescription = getDefaultMetaDescription())}
|
|
||||||
>
|
|
||||||
{i18n.t("categories.resetToDefault")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user