0% 5h quota used · resets in 2h live · 08/09/2026 12:16 CEST

tn — run journal

Append-only, one entry per run — newest first here, oldest first in the file. Attribution comes from the run id an entry cites.

12 of 38 entries · show more
eleventh seo-audit run (scheduled): TN-002 closed, and its root cause was found upstream seo-audit

Two things landed since 08-31, both verified live rather than trusting the merges. git log origin/develop --since=2026-08-31 on TN paths → one commit (6222c3da04f, PR #18264, the local faq-schema component); on the shared files behind TN-002 (app/Support/FaqAnswer.php + components/accordion) → one commit (a1b113ff29b, PR #18655). Full report: reports/2026-09-07-seo-audit.md.

TN-002 arc fully closed. /steg/numero-vert now emits a live 5-question FAQPage (cache-busted: FAQPage=1, Question=5, Answer=5) — PR #18264 works.

The bigger news: the shared FaqAnswer bug was root-caused and fixed upstream (PR #18655, merged 2026-09-01, by Aurian). The \v in the whitespace class meant vertical whitespace (incl. the 0x85 NEL byte) since PCRE 7.2, not the vertical tab that was meant — so every UTF-8 char carrying a 0x85 continuation byte (Arabic م, Greek υ, Nordic Å) got a space punched in, json_encode() returned false, and the whole FAQPage block vanished. 173 pages / 11 sites (3 on TN). Fix: \v\x0B + degrade per item instead of per page. This closes the fleet-wide defect TN-002 was a workaround for — the local duplication (PR #18264) is now technically redundant but harmless. Noted as a low-priority cleanup (retire faq-schema.blade.php, revert numero-vert to the shared path once the fix is confirmed there), to batch with the next mechanical fix — do not rush it.

All else healthy, 0 mechanical items. 8 core URLs 200 OK (homepage 4.09s cold, rest <1s). 13 URLs title/meta measured node+entity-decoded: max title 65, max meta 157 — zero breaches, annuaire + outage families still within guideline. Sitemap 673 (374 coupures), homepage 2 JSON-LD blocks, tarifs + coupures FAQPage=1 (shared accordion intact post-#18655). /calculateur-facture-steg (PR #19418) still a draft, 404 live as expected — score it after merge. No tn:seo-fixes trigger (genuine "nothing to fix" skip).

Lesson for future runs: the standing instruction "duplicate the faq to the country" (TN-002) was a workaround for an unknown root cause. Once the upstream fix landed, the local copy became redundant — so whenever a register entry authorises a local duplication to dodge a shared bug, a later run should re-check whether the shared bug got fixed before treating the local copy as permanent. Grep git log origin/develop -- app/Support resources/views/components for the defect's files, not just TN paths. Standing backlog unchanged: ogImage 0/673 (12th run), 6 /steg/* FAQ sections, TN-003 relocation sweep.

first `content` run: the STEG bill simulator, and a site with no easy gaps content

Task created 2026-08-21, scheduled the 5th — this is its first fire, so there was no previous page to grade. Ships as draft PR #19418 (https://github.com/Selectra-Dev/cms/pull/19418), branch countryos/tn/2026-09-05-content-calculateur-facture-steg, 6 files, 1 commit.

Also confirmed on the way in: PR #18264 (the TN-002 local faq-schema component) merged 2026-09-02. gh pr list --search "countryos/tn" was empty before this run. Next seo-audit can now spot-check that /steg/numero-vert emits a live 5-question FAQPage — the last open item from the 08-31 arc.

The finding that shaped the whole run: every big cluster is already served

I ran the skill's three sources honestly and they came back almost empty of a non-cannibalising candidate. This site is unusual: its demand is extremely concentrated, and the pages built before July already sit on all of it. Measured over 2026-08-06 → 09-02 and rejected, each for cannibalisation:

  • coupures électricité tunisie — 24 053 impr, pos 8,4, the biggest page-two query on the site. Served by /coupures/tunis, itself split with /coupures/tunis/tunis.
  • société tunisienne de l'électricité et du gaz (+ the unaccented spelling) — 23 561 impr, 5 clicks, pos 10,2/10,7. Served by /steg. Navigational: steg.com.tn and Wikipedia own the SERP, the ceiling is genuinely low.
  • facture / espace client / paiement — ~9 700 impr at pos 7-9, all on /steg/facture, which has a dedicated payment H2 and converts (76 clicks on paiement facture steg par carte bancaire alone).
  • palier steg family — 936 impr; /tarifs already takes 738 of them at pos 2,8-6,4.
  • délestage family — 557 impr, pos ~10, and I nearly picked it: /steg/coupures covers "que faire" not "pourquoi", and no page is about délestage. But /coupures/tunis already takes 416 of the 557 impressions. Rejected.

Two standing candidates die on the skill's own rule ("if you cannot point at a number, it is not a candidate"), and this is worth carrying forward:

  • Compteurs intelligents — the August business review ranked it #4 and the skill lists it first. Real demand over 28 days: 15 impressions. It is also already a section on /steg/facture. It is a good story, not a page. Stop proposing it until a number appears.
  • PROSOL / solaire — review's #2. 82 impressions, best position 7,2, most rows at pos 25-34. /steg/solaire exists. Same verdict.

The three top-ranked opportunities of the August review (#1 /steg/facture, #2 /steg/solaire, #3 /steg/coupures) are all rework, not new pages — and /improve is still blocked by the no-Python constraint (TN-005, rejected). That is the real structural problem here, and it is now two months old: this site's remaining upside is almost entirely in improving pages that already rank, and Country OS has no tool that can do it. Worth a decision entry if a third month passes.

What I shipped instead, and why it is not cannibalisation

/calculateur-facture-steg — the bill simulator. The intent is real and it is ours already: on /tarifs, the tool wording of the calculation intent (simulateur*, simulation*, calculer/calcul*, estimation*) draws 1 585 impressions and 21 clicks at positions 8,5-13,6simulateur steg alone is 334 impr for 0 clicks. Meanwhile /tarifs wins the barème intent (tarif steg tunisie 2026 pos 1,6, 167 clicks). None of its winning queries contains a tool word. So this is not a new page competing with a winning one; it is an intent an explanatory tariff table cannot serve. 14 clicks at risk, ~1 570 impressions earning nothing.

The blockers that had frozen this since August are all lifted, and I checked rather than assumed: proposals/calculateur-facture/SPEC.md §2 (the incoherent 250 kWh example / the 100 kWh switchover rule) was resolved on 08-18/19, and §3 (extract the barème) is done — departments/tunisia-energy/data/tariffs.php exists with verified_at. Reading the spec before writing anything saved the whole design; the one useful lesson of 08-31 ("grep for prior art before building what a decision authorises") generalises to read the proposal folder too.

No figure was written by hand. Page and JavaScript both read tariffs.php; I added max_kwh per tranche (restating the bound already in range in machine-readable form — the skill explicitly allows adding to that file). The arithmetic reproduces exactly the three anchors the repo records against the official STEG simulator: 250 kWh at 1 / 3 / 4 kVA → 63 448 / 65 114 / 65 947 millimes. Tested with node before committing.

Design choices worth keeping: the scale table is server-rendered (50 → 1 000 kWh), because the "combien coûtent N kWh" queries — including the Arabic ones — must be answered in the served HTML, not inside a widget Google will not run. The widget is the conversion, the table is the ranking.

One thing I deliberately did NOT build, and it is the honest half of the run: SPEC §4's billed-period multiplier. Whether the RTT and the redevance are charged once per bill or once per month is documented nowhere I could verify, and multiplying them would have been a guess dressed as a feature. The simulator computes one month, says so on the page, and tells the reader to divide a multi-month reading. Flagged in the PR.

One open factual question raised for the reviewer, not resolved: the tranche is selected on consumption alone, per economic_band_max_kwh => 100, which carries no power condition — but the barème's puissance column labels the 62/96 bands "1-2 kVA". What does a 3 kVA subscriber consuming 80 kWh pay? Both verified anchors are at 250 kWh and do not disambiguate it. If the answer is "the économique band is reserved to ≤ 2 kVA", the fix belongs in tariffs.php and the widget follows for free.

steg.com.tn: unreachable a second consecutive month

Retried first thing, as the 09-01 entry instructed: curl timed out after 25 s while google.com returned 200 in the same window. That is now two consecutive months, which the 09-01 entry itself set as the threshold for escalating from bad luck to a real dependency risk. It did not block this run — everything shipped derives from figures a previous run already verified into tariffs.php, which is precisely the value of having that file — but it will block content-freshness again on 2026-10-01. A future run should open a decision entry rather than skip-and-flag a third time.

Next run

  1. Grade this page at merge + 28 days. The criterion is not this page's own traffic: it is whether the family gained clicks. Baseline to beat: 1 585 impr / 21 clicks on /tarifs for the simulateur+calcul+estimation wordings, plus 429 impr / 14 clicks on كم سعر 1000 كيلو كهرباء في تونس. Watch /tarifs does not lose comment calculer facture steg tunisie 2026 (pos 4,9) or simulation facture steg 2026 (pos 7,2) — the 14 clicks knowingly put at risk.
  2. Next month's candidates, all weak, listed honestly — délestage (557 impr but /coupures/tunis takes 416), an /ar/ pilot on this very calculator (SPEC §6; the Arabic script section shows 945 queries / 10 819 impr at the same 2,9 % CTR as latin, so Arabic is proportionally served, not starved — this is a growth bet, not a gap), and nothing else above 500 impressions. If the next run also finds no clean gap, say so and write nothing — that is a more useful month than a page nobody searches for.
  3. Standing backlog unchanged: ogImage 0/673 (11 audit runs), 6 /steg/* FAQ sections, TN-003 relocation sweep still sitting above ## Resolved in decisions.md.
third content-freshness run (scheduled): blocked at the source, no PR opened content

steg.com.tn was completely unreachable this run — the first time since tracking began. Every fetch attempt failed, by two independent tools, over roughly 3 minutes of retries: 4× WebFetch (ECONNREFUSED 196.203.146.163:443, on the accented tariff URL, the réclamations/dépannage URL, the contact page, and the bare domain), then 3× raw curl ((28) Connection timed out), including one attempt straight against the resolved IP with a Host: header (bypassing DNS) and a background retry loop spaced 20s apart. General internet egress from this environment is fine in the same window — curl to google.com and facebook.com both returned 200 immediately. So this is not an environment-wide network block; it is specific to STEG's server refusing/timing out this environment's connections. WebSearch snippets show the site's own pages (tariffs, réclamations, districts) indexed and readable by Google's crawler, so the site itself is up — whatever is blocking is likely IP-based (geo-fencing to Tunisia, or a datacenter/cloud-IP block added since the 08-24 run, which fetched the same URLs successfully).

Consequence: step 1 of the skill (verify tariffs/phone numbers against the official source) could not be performed this run. Per hard rule 1 — "if a fact cannot be verified, flag it instead of guessing" — I did not touch tariffs.php, verified_at, or any page figure. Bumping verified_at without an actual fetch against steg.com.tn would have been a false claim of verification, exactly the kind of invented-confidence this rule exists to prevent. No PR opened — there was nothing verified to ship, and the skill says not to open an empty one.

What I could still check without the primary source, and found unchanged:

  • git log origin/develop --since=2026-08-25 on TN paths → empty, so no page has drifted since the last confirmed-fresh state (08-24 run + the 08-31 seo-fixes PR #18264, still open/ unmerged, touches only numero-vert.blade.php + new faq-schema.blade.php — no overlap with tariff/contact/agency files).
  • Phone numbers (80 100 444 numéro vert, 71 239 222 CNSD) and the tariff headline figures (62–414 mill/kWh, 700 mill/kVA redevance) corroborated via WebSearch snippets pulling from STEG's own indexed pages and independent secondary sources (sunrise.tn, hellowatt.fr on the unrelated French TRVE rise, which surfaced only because of a generic query — not a STEG source). Secondary corroboration is reassuring but does not meet hard rule 1's bar — logged as context, not treated as verification.
  • ELMED status (/steg page): current wording — "phase d'approvisionnement achevée juin 2026", Hitachi Energy ~770 M€, mise en service visée 2031 — re-checked against fresh French press (lapresse.tn April/July 2026, kapitalis.com) and still accurate; 2028 vs 2030/2031 remains a genuinely contested date across sources, same ambiguity as 08-24, left as-is.
  • steg_agencies.json: static snapshot (fetched_at: 2026-05-06), untouched since 08-25 per the git log above. Spot-checked 10 evenly-spaced entries plus the three previously-flagged anomalies by name (STEG District Le Bardo idx 13, STEG Skhira idx 129 — both still show Google-sourced "Ouvert 24h/24" hours on a page that lists mostly 08:00–17:00 entries; IAT Smairia idx 144 — name still doesn't match the STEG naming pattern; the Siliana district entry idx 70 still carries phone 42 105 169, a mobile-range prefix rather than the Siliana 78 landline area code). All four are unchanged repeats, not new findings — no correction made, per the same "flag, don't guess" rule (and the data file itself hasn't been re-fetched from OSM/Google since May, so there is nothing this run could newly verify there either).

Lesson for next run, and probably the most consequential one for this skill's design: the entire freshness check has depended on a single primary source (steg.com.tn) being reachable from this specific container, with no fallback and no explicit failure mode in the skill. That assumption held for 3 straight runs (08-04, 08-24, and implicitly every seo-audit link check) and then silently didn't. First thing next run: retry steg.com.tn before anything else. If it's reachable again, this was transient (a STEG-side outage or a temporary edge-network hiccup) — proceed normally. If it's unreachable a second consecutive month, that crosses from "bad luck" into "this skill has a real dependency risk worth a TN report/decision" — e.g. asking whether a mirror/cache (Wayback Machine — also untested this run, WebFetch refused it outright: "unable to fetch from web.archive.org") or a different fetch path is worth building, versus accepting a "skip and flag" month whenever it happens. Don't silently re-attempt-and-skip a third time without escalating.

Standing backlog, all unchanged and unrelated to this blocker: PR #18264 (TN-002 FAQ fix, still open/draft, review pending), ogImage (10+ audit runs flagged), 6 remaining /steg/* FAQ sections, TN-003 relocation sweep in decisions.md (still sitting above ## Resolved).

seo-fixes run (chained from the audit above): TN-002 shipped seo-fixes

Carried the TN-002 resolution ("duplicate the faq to the country") into an actual fix. Branch countryos/tn/2026-08-31-seo-fixes from origin/develop, 1 commit, pushed, draft PR opened: https://github.com/Selectra-Dev/cms/pull/18264. No open countryos/tn PR existed first (gh pr list checked).

What was done, and the discovery that made it low-risk instead of speculative. Before writing anything, grepped the fleet for faq-schema and found the directive had already been implemented elsewhere: callmepower.be, selectra.be and echosdunet.net each carry their own sites/{slug}/components/faq-schema.blade.php — a site-scoped component that emits FAQPage JSON-LD via plain strip_tags/html_entity_decode, bypassing the shared FaqAnswer::fromHtml() DOM flattener entirely. Two variants exist (an :items q/a shape for pages with no visible accordion, and a :faqs shape built specifically for "accordion :items shape" — title/content). The second matches /steg/numero-vert's $faq array exactly. Copied it verbatim into sites/tunisie-electricite.com/components/faq-schema.blade.php — not invented from scratch — then edited numero-vert.blade.php: dropped the broken :schema="true" from <x-accordion :items="$faq" ...> (visual rendering untouched) and added <x-faq-schema :faqs="$faq" /> right after, reusing $faq as-is.

Why this is safe, confirmed by reading the framework code, not assumed: app/Http/Middleware/ResolveSite.php:186-188 registers sites/{slug}/components/ as an anonymous-component path per request, scoped to that site, and site paths win over shared ones in the resolution order. So the new <x-faq-schema> only exists for tunisie-electricite.com requests, and doesn't touch or shadow anything fleet-wide. Just as important: it's a new component name, not an override of <x-accordion> itself — the other 4 places on this site using the shared <x-accordion :schema="true"> (/tarifs, /coupures, and the 24+350 /coupures/{gouvernorat}[/{municipalite}] templates) are untouched, because they already work correctly (only numero-vert's specific FAQ content trips the shared bug) — overriding the whole component site-wide would have risked 374 currently-healthy pages to fix 1.

Lesson for next run, and probably the most useful one in a while: before building anything a decision authorises, grep the fleet for prior art. The instruction said "duplicate the faq to the country" in the abstract; the concrete shape of that duplication (component name, prop shape, flattening strategy) had already been solved and battle-tested on 3 other sites. Reusing it instead of designing a new local flattener from first principles removed all the guesswork about why FaqAnswer::fromHtml() breaks on this page's content (still not root-caused — no PHP runtime in this container, per the standing TN-005 constraint) — the fix doesn't need to know why, only to not depend on the broken path at all.

No local Docker/Sail to render-test before merge (standing limitation); flagged in the PR body for the human reviewer to smoke-test /steg/numero-vert's rendered JSON-LD against /coupures/tunis's as a reference.

Next run: verify PR #18264 lands and /steg/numero-vert emits a 5-question FAQPage live (cache-busted). If merged, TN-002 in decisions.md needs no further register action (already resolved). Standing non-mechanical backlog unchanged: ogImage (10 runs), 6 remaining /steg/* FAQ sections (candidate for tn:content), TN-003 relocation sweep in decisions.md.

tenth seo-audit run (scheduled) seo-audit

No-change confirmation run on the site itself, but the first run since the TN-002 resolution where the FAQPage fix is in scope and authorised — and I acted on it.

git log origin/develop --since=2026-08-27 on TN paths → empty; on app/Support/FaqAnswer.php + components/accordion since 08-17 → empty (shared regression still unfixed in shared code, as expected — the resolution was explicitly not to wait for it). gh pr list --state open --search "countryos/tn" → empty. All 8 core URLs 200 OK (0.09–0.33s). Re-measured the standing checks cache-busted: delegation meta 145 / title 65, governorate meta 151 (both #17032 fixes holding), /coupures/tunis healthy (FAQPage+BreadcrumbList), homepage 2 JSON-LD blocks, homepage og:image still absent. Full report: reports/2026-08-31-seo-audit.md.

The one live defect, now actionable: /steg/numero-vert still emits 0 FAQPage blocks (BreadcrumbList only). TN-002 was resolved 2026-08-27 with the directive "duplicate the faq to the country" — TN gets its own FAQ/accordion rendering, do not touch the shared component — and the register explicitly records it as "runnable, not run: tn:seo-fixes carries the local FAQ rendering". It had sat unexecuted for 4 days because no scheduled TN task fires between the 27th and the next Monday seo-audit.

Decision, and the reasoning to keep: I chained tn:seo-fixes despite finding only 1 mechanical item, against the literal "≥2 to chain" rule. The ≥2 threshold is a batching default for mechanical items the audit discovers on its own; it should not gate a fix a human has already decided and recorded as authorised. Leaving a recorded, authorised fix unexecuted another week is the exact "machinery stalling past its own decision" failure the 08-27 arc closed (and that the register exists to prevent). Chained as task-1788149101166-51c46f with a precise instruction (repro, do-not-touch-shared, and /coupures/tunis as the JSON-LD reference pattern). Future run: this was a deliberate, documented deviation — if it turns out wrong (e.g. seo-fixes can't carry a component-level duplication), the fallback is a dedicated task, not a re-chain from seo-audit.

Standing backlog unchanged and all non-mechanical: ogImage 0/673 (10th run), 6 /steg/* FAQ sections (candidate for tn:content, one page/month), and the TN-003 relocation sweep (confirmed still unresolved — TN-003 is resolved status but sits above ## Resolved in decisions.md; trivial markdown move, zero site impact, still nobody's task).

Next run: verify the seo-fixes PR for the TN-002 fix lands correctly (spot-check /steg/numero-vert emits 5-question FAQPage live), and confirm whether the shared FaqAnswer fix ever landed upstream so the local duplication can eventually be retired.

decision ids are now `<CC>-nnn`, and the `D-` prefix is gone unattributed

Fred's call, and it removes the ambiguity at the source instead of resolving it downstream: D-002TN-002, and _global renumbered to OS-001/OS-002. Numbers still restart per register — the prefix is the only thing making an id unique across the fleet, which matters because one Google Chat space is the inbox for every country at once. No backwards compatibility was kept for old Chat messages, PRs or issues; there are few enough of them that carrying a legacy spelling forever cost more than losing it. Every D-nnn in the journal entries above refers to what is now TN-nnn — those entries, and the verbatim quotes inside the register, keep the spelling that was true when they were written.

The order mattered: both parsers hardcoded [A-Z]-\d+ (tools/bin/decisions:142, ui/server/data/decisions.js:40), so TN-002 did not match — and a heading that does not match is not an error, it is an entry that silently stops existing, which is the exact failure this register was built to end. Regexes widened to [A-Z]{1,6} FIRST, headings renamed second. They still accept a one-letter D-nnn on purpose: an ambiguous entry costs a clarify, an invisible one costs everything.

tn:decisions-escalate is unaffected (it never hardcoded an id shape) and the dispatcher route still decides the workspace by which register carries the entry, not by reading the prefix — the prefix is a convention in a markdown file, and trusting it would mean trusting the spelling of the very thing being disambiguated.

Still open after this: _global has no decisions-answer task, so OS-001 and OS-002 (both still never announced) have no path back into the register.

D-002 answered six days late, and why the answer was lost unattributed

D-002 is resolved, and the register now has 0 open entries. Fred's answer (2026-08-21, Chat: "D-002 rework the component directly on the tunisia website for now", restated today as "duplicate the faq to the country") is recorded verbatim. The ruling: the TN site gets its own copy of the FAQ/accordion rendering — do not wait for the shared component, and do not edit it either, so hard rule 5 stands. tn:seo-fixes is now authorised to carry that (draft PR, as always) and has NOT been run. cms#16773 is closed with no fix, so the fleet-wide FaqAnswer bug is unowned — but it is no longer TN's to chase.

Why the answer never landed, and this is the instructive part. The register's whole premise is "answer in whatever words you like — the reply is read, not parsed". But between Chat and decisions-answer sits the dispatcher, which must pick one task from free text. An answer that names an action ("rework the component directly on the tunisia website") is indistinguishable from a request to go do it — so the dispatcher did the correct thing and asked which, in-thread, and that question was never answered. The queue entry (data/queue/chat-784d4bb68abb.json, status unclear) has been sitting there since. Meanwhile decisions-escalate re-raised D-002 on 08-20 and 08-27, asking for an answer that already existed — the machinery was loudly nagging past its own inbox.

Fixed at the routing layer, not the prompt layer. orchestrator/src/dispatch.js now routes any reply that opens with a register id (D-002 …) straight to that workspace's decisions-answer, deterministically, with no model call — which is the exact shape the announcement message asks for (@countryos D-002 routed to X). Ids collide across registers (tn and _global both have a D-002), so the route is decided by which workspaces both carry the entry and have a task that can record it; two candidates produce a precise clarify instead of a guess. False positives are harmless by construction: decisions-answer only transcribes, and is forbidden from running the work an answer authorises.

Lesson. A promise of "any wording works" is only as good as the most deterministic route behind it. Wherever a documented message shape exists, match it in code before handing the text to a model — the model's job is the messages that don't have a shape. And an escalation that cannot see its own inbox will always eventually nag someone for something they already gave you.

Known gap, not fixed: _global has decisions-escalate but no decisions-answer task, so an answer to _global D-001/D-002 has nowhere to land — the same class of failure as this one, one step earlier. The new route will correctly refuse to invent a destination for it.

Addendum, same day — Fred asked what happens when two countries have the same id open, and the first version of the route got it wrong. It looked for the collision only among workspaces that have a decisions-answer task, which is not the same question. With tn's D-002 resolved, _global's D-002 open, and only tn able to record anything, tn became the sole candidate: an answer about a MySQL replica pool would have been transcribed into Tunisia's FAQPage entry. Caught by testing the live fleet state rather than the intended one — the two disagreed. The route now reads every register first, then asks who can record: two live entries produce a clarify naming both with exact syntax; a live entry in a workspace that cannot record one says so and offers the same id where it is answerable. A misfiled answer is worse than a missing one, because it reads as settled.

The remaining structural weakness: ids are per-register but the Chat space is one shared inbox, so @countryos D-002 yes is ambiguous by construction as soon as two countries run this. The clean fix is thread identity, not ids — decisions announce already posts one message per entry, and chat.js already sees msg.thread; if announce recorded the thread name on the entry, any reply in that thread would route with no id at all, and no ambiguity to resolve. Not built.

ninth seo-audit run (scheduled) seo-audit

No-change confirmation run. git log origin/develop --since=2026-08-25 on TN paths → empty; on app/Support/FaqAnswer.php + components/accordion since 08-17 → empty (D-002 still unfixed). gh pr list --state open --search "countryos/tn" → empty. All 8 core URLs 200 OK (0.10–0.26s), 3 dynamic spot-checks 200. Re-measured the two pages fixed by #17032 (delegation meta 145, governorate meta 151 — holding), confirmed /steg/numero-vert still emits 0 FAQPage (D-002 live), /coupures/tunis healthy, homepage still no og:image. Full report: reports/2026-08-27-seo-audit.md.

0 mechanical items → no tn:seo-fixes trigger (9th consecutive non-mechanical-only run). Standing backlog unchanged: D-002 (re-raised today by tn:decisions-escalate, its Notified now reads …2026-08-27), ogImage 0/673 (9 runs), 6 /steg/* FAQ sections, D-003 relocation sweep still not done.

Lesson worth carrying: when an audit run finds "nothing changed", the one thing still worth doing is confirming the escalation machinery actually fired on the standing out-of-scope defect, rather than re-filing it. D-002 was re-raised today by the daily tn:decisions-escalate task — seo-audit should note that, not duplicate it. The open risk to watch next run: cms#16773 (the GitHub outbox issue) is CLOSED while D-002 stays open — if the escalation tool files/updates issues keyed on "open", a closed outbox issue could silence the re-raise; verify the next re-raise still reaches a developer.

eighth seo-audit run (scheduled) seo-audit

Both countryos/tn PRs open as of the 08-24 audit merged to develop the same day: #17032 (seo-fixes, annuaire meta/title trims) and #17084 (content freshness). First run since 08-10 with zero open TN PRs (gh pr list --search "countryos/tn" → empty). Full report: reports/2026-08-25-seo-audit.md.

Verified both PRs live, cache-busted, rather than trusting the merge — and the cache-busting mattered: the first non-busted fetch of /monastir/sayada-lamta-bou-hajar still served the pre-fix 220-char meta (stale CDN), which would have read as "fix didn't land". The busted fetch confirms it: delegation meta 145 (was 230 live pre-merge), governorate meta 151 (was 244, /annuaire/sidi-bouzid), index title 54 (was 67), /tarifs "barème vérifié le 24 août 2026" ✓, /steg ELMED "mise en service visée 2031 (Hitachi)" ✓. All 9 re-checked pages within title/meta guideline; sitemap still 673 URLs with a consistent shape breakdown (7/316/350); /coupures/tunis + /medenine/djerba-midoun spot-checked healthy (2 JSON-LD each). This is the first run where the annuaire and outage page families are simultaneously within length guideline — the 285-page template bug from 08-24 is fully closed.

0 mechanical items found — no tn:seo-fixes trigger (genuine "nothing to fix" skip, like 08-10, not PR-blocked). Standing backlog unchanged and all non-mechanical: D-002 (/steg/numero-vert FAQPage regression, register open, cms#16773 open, zero commits to FaqAnswer.php/accordion since 08-17 — re-checked), ogImage (8 runs flagged), 6 /steg/* FAQ sections, and the D-003/D-005-under-## Resolved relocation sweep still not done.

Lessons carried: (1) always cache-bust (?v=) before concluding a merged fix didn't land or a page regressed — stale CDN reads cost real time this run and would have produced a false "broken" finding; the 08-17/08-24 cf-cache lessons apply to verifying merges, not just diagnosing anomalies. (2) D-002 re-raise due ~08-27 per the escalation cadence — if cms#16773 is still untouched by then, that's the moment to escalate harder.

Next run: diff against this report; re-verify only pages that changed (expect none, TN path has been quiet) or scored <80. If D-002 still open, escalate; if cms#16773 shows dev action, re-test /steg/numero-vert FAQPage. ogImage + FAQ sections remain the standing non-mechanical backlog worth a dedicated task.

content-freshness run (scheduled) content

Second /content-freshness run (first since 08-04). Branch countryos/tn/2026-08-24-freshness from origin/develop, 2 commits, pushed, draft PR opened: https://github.com/Selectra-Dev/cms/pull/17084. No open countryos/tn PR on the same files (seo-fixes #17032 touches annuaire templates only).

Re-verified everything against steg.com.tn — zero tariff changes. Electricity BT (62/96/176/218/341/414, redevance 700/kVA, CL 5, FTE 5, TVA 7/13) and gas (BP1 231-824, BP2 762, FTE 11.875, TVA 19%) all match tranche-for-tranche. Phone numbers 80 100 444 (numéro vert) and 71 239 222 (CNSD) confirmed on the réclamations page. The 08-04-flagged BP2 762 ambiguity is resolved: a clean fetch of the STEG gaz page confirms 80,2 mill/th = 762 mill/m³. The "500 000 compteurs intelligents fin 2026" claim is still the official target (STEG CEO, ~80k installed at 31/03/2026).

What was fixed (4 files, +12/−12):

  1. tariffs.php: verified_at 08-18 → 08-24 (per the 08-19 instruction to maintain this field), and source pointed at the non-accented URL which 404s — now the accented one that returns 200 (the /tarifs page already linked the accented version; only the data file was dead).
  2. /tarifs FTE (G) card: duplicated "Au-delà de 30 m³/mois." sentence removed.
  3. /tarifs-gaz: conversion was inverted — "1 thermie ≈ 9,5 m³" → "1 m³ ≈ 9,5 thermies" (matches the STEG grid: 24,3 mill/th = 231 mill/m³, so ~9,5 th/m³).
  4. /steg: chiffres clés refreshed to official STEG homepage figures — electricity clients 4.58 M (4,582,694), gas clients 1.07 M (1,070,360), employees ~13,200 (13,223) — the latter finally closes the "2020 employee count" flag open since 08-04. ELMED: "construction lancée en 2026 / mise en service 2028-2030" → phase d'approvisionnement achevée juin 2026 (contrat Hitachi ~770 M€), début des travaux attendu ~2027, mise en service visée 2031 (selon Hitachi Energy).

Flagged in the PR body (not changed — ambiguous sources): ELMED commissioning date is contested (Hitachi/Reuters say 2031, African Manager still 2028 — kept 2031 with attribution); /steg ~5 982 MW installée (2023) vs STEG homepage "4 630 MW Puissance Développable" (likely different metrics, left as-is); the same 3 agency data-quality flags from 08-04 persist (Le Bardo + Skhira "Ouvert 24h/24", IAT Smairia odd name) plus a new one — Silyana district phone "42 105 169" doesn't match the Tunisian numbering plan.

Lessons for next run: (1) grep URL liveness, not just figures — the only dead link found this run was in tariffs.php's source field (non-accented), not on any page; both suspected-404 links on /steg/solaire (avenant + Manuel PROSOL PDF) were false alarms from regex truncation, re-verified 200 with full URLs. (2) STEG homepage Chiffres clés is a trustworthy, current source for customer/employee counts — the 2020 employee flag could have been closed any run since it landed there. (3) Gas tariffs are still hardcoded in /tarifs-gaz (unlike electricity in tariffs.php) — if a gaz calculator ships, that grid needs the same extraction (#16291-style) first.

Next run: verify PR #17084 lands (spot-check /tarifs shows "barème vérifié le 24 août 2026", /steg ELMED wording). Standing backlog unchanged: #17032 seo-fixes review, D-002, ogImage (9 runs), 6 FAQ sections.

seo-audit second fire (duplicate of today's earlier run) seo-audit

The scheduled seo-audit task fired a second time today (10:03), ~20 min after the first fire (09:45) ran the audit and chained seo-fixes (PR #17032, opened 09:48). Recognised as a duplicate rather than re-running the audit — the useful pattern when a scheduled task double-fires: confirm nothing changed since the first fire, verify the hand-off landed correctly, record. Appended an addendum to reports/2026-08-24-seo-audit.md.

Verified: all 8 core URLs still 200 OK (0.08–0.13s); git log origin/develop --since=2026-08-17 on TN paths unchanged (the 5 known commits); zero commits touching FaqAnswer.php/components/accordion since 08-17 (the shared-component diff from the 08-17 lesson). Independently re-derived the seo-fixes branch's template lengths for every real (governorat) and (delegation, gouvernorat) pair from governorates.json + steg_agencies.json: governorate meta max 151 (Sidi Bouzid, 2 agences/12 délégations), delegation meta max 145 (Bou Mhel el Bassatine / Ben Arous — not the audit's named worst case; another confirmation that with >1 interpolated variable the real max comes from a different pair), delegation title max 65 (Sayada Lamta Bou Hajar), index title 54. All within guideline, matching the seo-fixes run's numbers. PR #17032 is correctly formed; nothing to add. Per the chain rule, did not trigger a new seo-fixes (an open PR already covers all 3 mechanical items).

Live baseline measured (what the PR will change): production metas are 244 chars on /annuaire/sidi-bouzid and 230 on /monastir/sayada-lamta-bou-hajar — the latter 10 chars above this morning's audit's 220 worst-case estimate. Even a worst-case enumeration reconstructed from the template string understated the rendered length; another instance of "measure the rendered page, not the reconstructed string". The branch fix brings both to ≤160, unaffected.

cms#16773 is CLOSED but D-002 is not fixed. Register entry still open, live page still emits 0 FAQPage blocks, no commit touched the shared files. The GitHub issue is the regenerated aggregator ("[tn] Country OS reports — 1 waiting", updated 08-24 08:07Z, generated 08-20) — closed state is issue lifecycle, not resolution. Worth a glance from whoever runs tn:decisions-escalate/owns the announce tool: an aggregator issue flipping to CLOSED while an entry is open could silently mute the outbox. Next re-raise due ~08-27.

Housekeeping flagged, not done (out of scope): D-003 sits resolved but outside the ## Resolved section of decisions.md — the sweep requested in the 08-19 decisions-answer entry still hasn't happened. tn:decisions-answer or a dedicated sweep should relocate it.

Next run: PR #17032 review/merge is the only open TN item — once it lands, spot-check /annuaire/sidi-bouzid and /monastir/sayada-lamta-bou-hajar render ≤160 meta live. D-002 + ogImage (8 runs flagged) + 6 /steg/* FAQ sections unchanged standing backlog.

seo-fixes run (triggered by the seo-audit above) seo-audit

Implemented the 3 mechanical items from reports/2026-08-24-seo-audit.md. Branch countryos/tn/2026-08-24-seo-fixes from origin/develop, 2 commits, pushed, draft PR opened: https://github.com/Selectra-Dev/cms/pull/17032. No open countryos/tn PR existed first (gh pr list checked).

What was done: shortened the annuaire/_gouvernorat.blade.php meta (24 pages, was 232–244 chars) and annuaire/_delegation.blade.php meta (261 pages, was 199–220 chars), kept the meaning (counts, dépannage number, électricité et gaz, the governorate page's Arabic clause); trimmed the annuaire/_index.blade.php title from 67 to 54 chars by reusing the page's own H1 wording. Titles on the governorate/delegation templates were already within guideline per the audit — untouched.

Verification went beyond the audit's two named worst-case examples, and that mattered. The audit's worst case for the delegation template was "Sayada Lamta Bou Hajar" (Monastir, longest delegation name) — but the new template's max length across all 261 real (delegation, governorate) pairs turned out to be a different pair entirely ("Bou Mhel el Bassatine" / "Ben Arous", 145 chars) once the governorate name is also embedded in the string, since delegation-name length and governorate-name length don't correlate and delegations don't pair with an arbitrary governorate — each belongs to exactly one real one. Same logic for the governorate template: worst case by name (Sidi Bouzid) isn't necessarily worst case once agency count and delegation count are embedded too (Sfax has 11 agences, the site max, but a short name) — had to cross governorates.json with steg_agencies.json's per-governorate agence_commerciale counts to confirm Sidi Bouzid (2 agences, 12 délégations) still wins at 151 chars. Lesson for any future template worst-case check with >1 interpolated variable: compute the real max over every actual row of the data, never assume the single worst-case name from the audit report is still the worst case once other embedded values change.

Environment unchanged: node only, no python3/php — fine, used a Node script against the JSON data files directly, same pattern as the 07-27 outage-template fix.

Next run: verify PR #17032 lands (spot-check /annuaire/sidi-bouzid and /monastir/sayada-lamta-bou-hajar render correctly — no local Docker/Sail available in this container to smoke-test before merge). Standing non-mechanical backlog unchanged: D-002 (/steg/numero-vert FAQPage regression, cms#16773 still open), ogImage (7 runs flagged), 6 /steg/* FAQ sections.

12 of 38 entries · show more