Backfill (scripts/backfill-wireless-equip.js): mirror F wireless CPEs
(airos_ac/airosm/cambium) into OPS Service Equipment, resolving customer +
service_location via device→service→delivery_id→Service Location(legacy_delivery_id),
dedup by MAC-or-IP, serial=MAC when F sn is null, undo manifest. Park was already
~99% synced — created the 2 genuinely-missing (incl. James A. Bryson) + flagged
devices without service/MAC. Verified: Bryson EQP-0000100001 → live LiteBeam 5AC
signal -74/-72, airMAX 93/67.
Signal interval coloring (F parity): wifiSigColor (airOS: >=-60 green … <-78 red)
and fibreRxColor (Rx -8..-25 green, -25..-28 orange, else red) color the live
signal tiles (wireless + fibre). Assistant analysis (check_signal advice) kept.
Device + AP links in service details: CPE management GUI link (Interface →
https://ip) in the wireless panel; AP link when its mgmt IP resolves (ops_airos.php
resolves the AP by radio MAC in F's device table, best-effort → apIp/ap_ip), else
shows the AP MAC. Hub normalizeAiros passes apIp through.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Comms: sélecteur d'expéditeur « De: » (défaut groupe Support TARGO) via resolveSendFrom + alias vérifiés
- Notifs: prefs de feeds PAR utilisateur (/conversations/notif-prefs) + cloche à bascules ; boot tooltip-ux (clic prioritaire + anti-empilement)
- Courriel: invitation à évaluer = modèle Unlayer éditable (transactional-rating-invite-*) ; test-send via Gmail + expansion {{rating}} ; logo TARGO auto-hébergé sur le magasin d'actifs du hub
- Planif: bloc « sans déplacement » (damier, début de quart, alerte si pas de quart), quart éditable dans l'éditeur de jour, icônes de compétence en vue jour (TV pour télé), clic cellule → éditeur, clic gauche lane → liste / clic droit → menu quart, lien ↗ ticket par job
- Tickets: défaut « Non fermés » + correction du filtre « Mes tickets » (owner)
- Inbox: poll Gmail 1 min + rafraîchir à la demande (poll-now)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The native-block imageBlock factory was emitting img tags wrapped only
by a td with text-align:center. That doesn't actually center the image
because text-align only affects inline content, and the img has
display:block. The result: top header logo and dark-footer logo were
left-aligned despite the textAlign:"center" prop on the block.
Fix: wrap each img in an inner <table align="<textAlign>"> exactly the
way MJML/Litmus/Mailchimp do it. This is the canonical email-client
pattern that works in Outlook 2007-2019 (which ignores margin:0 auto
on inline tables but respects table align attributes).
Also: the AI converter dumped the entire dark footer band into a
SINGLE htmlBlock with malformed table markup (a stray </td> outside
its row). Split into proper image + text native blocks so:
1. The logo inherits the new centered nested-table pattern
2. The URL+copyright text is now individually editable in Unlayer
3. The {{year}} placeholder is in a text block where it belongs
And one AI hallucination correction: the converter assigned
textAlign:"left" to the top header logo (probably because the
surrounding column had align="left" in the MJML output). Original
design intent was centered — fixed in the spec.
Verified live: both logos (140px top, 120px footer) now render with
align="center" on their nested wrapper table.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Until now, every Unlayer-edited template stored as a single giant
"Custom HTML" block (~37 KB). The operator couldn't manipulate the
greeting, the CTA, or the expiry badge independently — they had to
edit raw HTML inside one block.
New scripts/build-native-template.js generates matched .json
(Unlayer design tree) + .html (compiled output) from a JS template
spec under scripts/templates-spec/. Each block becomes a separate
entry in the design tree with its own type:
- 9 text blocks : greeting, urgency, body, expiry, prorata,
Option 2 text, signature, contact, dark footer
- 2 image blocks : header logo, footer logo
- 1 button block : the CTA (🎁 {{amount}})
- 4 html blocks : view-in-browser, Option 1 chip, brand-logo
card, Option 2 chip (kept as raw HTML — too
custom for native equivalents)
gift-email-native-reminder-fr ships as the proof of concept:
- Compiled HTML: 30,867 bytes (vs 39,484 for the MJML-compiled
reminder-fr — saves 22%)
- JSON: 42,274 bytes (essentially same as before, but now broken into
16 individually-editable blocks instead of 1 monster Custom HTML)
What this unlocks in Unlayer:
- Click any text → font / color / size / padding / alignment in the
right panel
- Click the CTA → button-specific controls (corner radius, hover
color, padding)
- Drag-reorder blocks within the email
- Mobile preview reflects each block's responsive defaults
- Save a block to the personal library for reuse in other campaigns
Limitations on the 4 html blocks:
- Chips (Option 1 / Option 2) require raw HTML edit because the
rounded badge styling has no native equivalent
- Brand-logo strip needs precise inline img widths Unlayer can't set
Once the operator validates rendering across Gmail/Outlook/Apple
Mail, we'll port the rest: gift-email-fr/en + the existing reminder
templates can all migrate using the same build script.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Solve the "editor starts blank" problem by writing a one-time converter
that wraps each compiled .html template into a minimal Unlayer design
JSON (one Custom HTML block containing the entire body content). On
next editor load, Unlayer reads .json and renders the template in the
canvas — instant visual fidelity without manual reconstruction.
Strategy choice: Unlayer's "Import HTML" is a Pro-only feature. Building
a real HTML→Unlayer-blocks parser is several days of work. The minimal-
viable conversion (1 row + 1 Custom HTML block) gets the user 90% there
immediately:
• Canvas shows the template visually (Unlayer renders the HTML)
• Variables ({{firstname}}, {{gift_url}}, etc.) preserved as text
• User can edit the HTML directly via the block's side panel
• User can incrementally REPLACE the HTML block with native Unlayer
blocks (Text, Image, Button) for any section they want decomposed —
on their own schedule, not blocking the campaign send
New file: services/targo-hub/scripts/convert-html-to-unlayer.js
• CLI: node scripts/convert-html-to-unlayer.js <template-name>
• Reads templates/<name>.html, extracts <body> inner content, detects
preheader from a hidden <div style="display:none">, builds Unlayer
design JSON with brand-appropriate body.values (Targo Green link
color #00C853, Plus Jakarta Sans font, F5FAF7 page background).
• Backs up existing .json before overwriting.
Generated outputs (committed):
templates/gift-email-fr.json — 34 KB (30 KB inner HTML + Unlayer chrome)
templates/gift-email-en.json — 33 KB
Live verification: GET /campaigns/templates/gift-email-fr now returns
{ design: {...Unlayer JSON...} } alongside html. The editor's
onReady() callback in TemplateEditorPage detects data.design and calls
editor.loadDesign(design) → canvas populated immediately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>