diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2916980..f102410 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,18 @@ fix → patch (`0.3.x`); feature addition → minor (`0.x.0`).
> Ajouté → Added · Modifié → Changed · Corrigé → Fixed · Déprécié → Deprecated ·
> Supprimé → Removed · Sécurité → Security · Validé → Validated · Documentation → Documentation.
-## [0.18.0] — 2026-06-06
+## [0.19.0] — 2026-06-06
+
+### Changed
+- **DRV-7 (option A) — dashboard conversation column redesigned (web).** Each conversation now renders as a compact glyph instead of a date label: `↗` linked (clickable, opens the conversation), `◆` current conversation (non-clickable — no self-link possible), `✗` deleted (non-clickable, greyed). Each linked conversation is individually clickable (already the engine behaviour; only the visual token changed). Conversations are ordered **most-recent-first**. Web tooltip = the full date, plus the single captured title (`realTitle`) attached to the **most-recent linked** conversation only — there is no per-conversation title in the data model yet, so older conversations show date-only rather than a misattributed title. Legend updated to the three glyphs (existing label keys reused — no label-contract change). `isRowDeleted()` (whole-row greying) is unaffected: it keys off marker state, not rendering.
+- **Desktop rendering kept as-is** (full selectable marker to find/copy in the sidebar): a clickable glyph would open the browser, and the no-link constraint there is unchanged. The most-recent-first ordering applies to both surfaces.
+- **Glyphs are Unicode** (`↗ ◆ ✗`), recolourable and surface-safe — the dashboard has no icon webfont (it already uses `◆/✗/📋/↻`). Robustness floor: no dependency on an unguaranteed font.
+
+### Notes
+- Per-conversation title/deliverable mapping (so the tooltip could show *which* deliverable lives in *which* conversation) requires extending the data model (a title per conversation marker, captured at reconcile). Tracked as a separate item; out of scope here. Once it lands, these same glyphs gain real per-conversation titles for free.
+- Behavioural verification: renderConv exercised under Node (11 assertions — states, ordering, title attribution, non-clickable ◆/✗, desktop unchanged). CI guard (`tests/test_build_dashboard.py`) asserts the glyph/ordering wiring in the generated HTML, since the harness has no JS engine.
+
+
### Changed
- **Cover-letter template de-French-ified (minimal pass).** The place-and-date line was a hardcoded `{{SENDER_CITY}}, {{DATE_LETTER}}` composite — a continental/French tic (mandatory city + comma) baked into the layout. It is now a **single `{{DATE_LINE}}` slot the model composes in the target locale's convention** (e.g. `Paris, le 6 juin 2026`, `New York, June 6, 2026`, or a date-only line where the locale omits the city). The date was already model-supplied, so this only merges two values into one model-composed line — no script-owned formatting handed over. `sender_city` stays a slot for the sender address block. Body alignment (justify) left as-is: acceptable for the majority of the 11 default languages, and changing it would swap one bias for another. Regenerated `assets/Cover_letter_template.docx` accordingly.
diff --git a/SKILL.md b/SKILL.md
index 3dae366..2e4e8e8 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -1,6 +1,6 @@
---
name: candidate-suite
-version: 0.18.0
+version: 0.19.0
updated: 2026-06-06
description: "All-in-one suite for preparing job applications and interviews, bundled as a single module: cover letter, interview prep, application summary, strategic playbook, one-page reference card, and application tracking, plus candidate-profile configuration. Single entry point: it shows a selection widget, then generates each deliverable through its sub-module's script. Use whenever the user wants to apply for a role, prepare an application or an interview, get tools for a job posting, configure the skill or update their CV or profile, generate a cover letter, produce a summary, playbook, or reference card, track applications or view their application dashboard (e.g. asking where their applications stand), or makes any open-ended request for job-application help. These are intents, not required wordings: match the user's intent regardless of the language the request is written in."
---
diff --git a/modules/application-tracker/scripts/build_dashboard.py b/modules/application-tracker/scripts/build_dashboard.py
index 1dfde39..f7c69d8 100644
--- a/modules/application-tracker/scripts/build_dashboard.py
+++ b/modules/application-tracker/scripts/build_dashboard.py
@@ -334,43 +334,47 @@ def build_html(entries, statuses, readonly, surface, ui_lang, labels):
function renderConv(c, company, position, realTitle){
if(!c) return "";
var parts = String(c).split(";").map(function(x){ return x.trim(); }).filter(Boolean);
- return parts.map(function(p){
- var arrow = p.indexOf("\u2192"); // →
- var deleted = /\u2717/.test(p);
- var here = /\u25C6/.test(p);
- var rawLabel = (arrow > -1 ? p.slice(0, arrow) : p)
- .replace(/\u2717/g, "")
- .replace(/\u25C6/g, "")
- .trim();
+ var rt = (realTitle && realTitle.trim()) ? realTitle.trim() : "";
+ var items = parts.map(function(p){
+ var arrow = p.indexOf("\u2192");
var token = (arrow > -1) ? p.slice(arrow + 1).trim() : "";
- var url = convUrl(token) || (/^https?:\/\//i.test(p) ? p.trim() : "");
+ var rawLabel = (arrow > -1 ? p.slice(0, arrow) : p)
+ .replace(/\u2717/g, "").replace(/\u25C6/g, "").trim();
var dm = rawLabel.match(/(\d{4})-(\d{2})-(\d{2})/);
- var dateShort = dm ? (dm[2] + "-" + dm[3]) : (rawLabel || L.conv_link_fallback);
- var fullDate = dm ? dm[0] : rawLabel;
- // Marker to find/copy in the sidebar: real title if captured,
- // otherwise the fabricated marker `📋 date - company - position`.
- var fabricated = "\ud83d\udccb " + fullDate +
- (company ? " - " + company : "") +
- (position ? " - " + position : "");
- var marker = (realTitle && realTitle.trim()) ? realTitle.trim() : fabricated;
-
- // 1) Deleted conversation: URL invalidated → grey italic, not clickable
- if (deleted){
- return ''+esc(dateShort)+'';
+ return {
+ deleted: /\u2717/.test(p),
+ here: /\u25C6/.test(p),
+ url: convUrl(token) || (/^https?:\/\//i.test(p) ? p.trim() : ""),
+ dateShort: dm ? (dm[2] + "-" + dm[3]) : (rawLabel || L.conv_link_fallback),
+ fullDate: dm ? dm[0] : rawLabel,
+ sortKey: dm ? dm[0] : ""
+ };
+ });
+ // Most-recent first: the latest conversation leads (the order users scan).
+ items.sort(function(a, b){ return a.sortKey < b.sortKey ? 1 : (a.sortKey > b.sortKey ? -1 : 0); });
+ // The single captured title (realTitle) belongs to the most-recent LINKED
+ // conversation only — there is no per-conversation title in the data model yet.
+ var titleIdx = -1;
+ for (var i = 0; i < items.length; i++){ if (items[i].url){ titleIdx = i; break; } }
+ return items.map(function(it, idx){
+ // 1) Deleted -> crossed glyph, grey italic, not clickable.
+ if (it.deleted){
+ return '\u2717';
}
- // 2) Existing conversation (known link)
- if (url){
- // Desktop: a link would open the browser → we show the exact marker to
- // find/copy in the sidebar (selectable with one click).
+ // 2) Linked -> web: clickable open-glyph; desktop: selectable marker to find in the sidebar.
+ if (it.url){
if (IS_DESKTOP){
+ var fabricated = "\ud83d\udccb " + it.fullDate + (company ? " - " + company : "") + (position ? " - " + position : "");
+ var marker = rt ? rt : fabricated;
return ''+esc(marker)+'';
}
- // Web: clickable link (new tab, same account).
- return ''+esc(dateShort)+'';
+ var tip = it.fullDate + ((idx === titleIdx && rt) ? " \u00b7 " + rt : "");
+ return '\u2197';
}
- // 3) Current conversation / not yet linked → grey, ◆ suffix
- var title3 = here ? L.conv_current_title : L.conv_unlinked_title;
- return ''+esc(dateShort + (here ? "\u2009\u25C6" : ""))+'';
+ // 3) Current (diamond) / pending -> grey, not clickable.
+ var title3 = it.here ? L.conv_current_title : L.conv_unlinked_title;
+ var glyph3 = it.here ? "\u25C6" : esc(it.dateShort);
+ return ''+glyph3+'';
}).join(' \u00b7 ');
}
@@ -408,16 +412,14 @@ def build_html(entries, statuses, readonly, surface, ui_lang, labels):
// ---- Link-column legend (surface-aware) ----
function buildLegend(){
document.getElementById("legLinkColumn").textContent = L.legend_link_column;
- var legLink = document.getElementById("legLink");
- legLink.innerHTML = IS_DESKTOP
+ document.getElementById("legLink").innerHTML = IS_DESKTOP
? '\ud83d\udccb '+esc(L.legend_desktop_example)+'\u2003'+esc(L.legend_desktop_text)
- : '05-28\u2003'+esc(L.legend_web_clickable);
+ : '\u2197\u2003'+esc(L.legend_web_clickable);
document.getElementById("legCurrent").innerHTML =
- '05-29\u2009\u25C6\u2003'+esc(L.legend_current);
- document.getElementById("legLinked").innerHTML =
- '05-29\u2003'+esc(L.legend_linked);
+ '\u25C6\u2003'+esc(L.legend_current);
+ document.getElementById("legLinked").innerHTML = "";
document.getElementById("legDeleted").innerHTML =
- '05-30\u2009\u2717\u2003'+esc(L.legend_deleted);
+ '\u2717\u2003'+esc(L.legend_deleted);
}
// ---- Filters: populating the selects ----
diff --git a/tests/test_build_dashboard.py b/tests/test_build_dashboard.py
new file mode 100644
index 0000000..007ed77
--- /dev/null
+++ b/tests/test_build_dashboard.py
@@ -0,0 +1,25 @@
+"""Tier 1 — DRV-7 conversation-column rendering guard.
+
+renderConv is client-side JS, so CI (pytest, no JS engine) can only assert the
+wiring is present in the generated HTML. Behavioural correctness (states,
+most-recent-first ordering, realTitle attributed to the most-recent LINKED
+conversation only, non-clickable ◆/✗) is verified out-of-band with Node.
+"""
+
+from _helpers import run_cli
+
+DASH = "modules/application-tracker/scripts/build_dashboard.py"
+
+
+def test_drv7_web_column_uses_glyphs_ordering_and_drops_old_tooltip(tmp_path):
+ out = tmp_path / "dash.html"
+ proc = run_cli(DASH, "--output-path", str(out))
+ assert proc.returncode == 0, proc.stderr
+ html = out.read_text(encoding="utf-8")
+ # Compact open-glyph per linked conversation (web) — new in 0.17→DRV-7.
+ assert "\\u2197" in html
+ # Most-recent-first ordering wired.
+ assert "items.sort" in html
+ # The old date-text web tooltip (title = date) is retired in favour of the
+ # date (+ most-recent-linked title) tooltip.
+ assert "rawLabel||url" not in html