From b56e74c4413ee778e8974f6c588e0db9e321dcdd Mon Sep 17 00:00:00 2001 From: David Bon-Salomon <62355986+David-BS@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:02:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20DRV-7=20(A)=20=E2=80=94=20gl?= =?UTF-8?q?yph=20conversation=20column,=20most-recent-first=20(0.19.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++- SKILL.md | 2 +- .../scripts/build_dashboard.py | 76 ++++++++++--------- tests/test_build_dashboard.py | 25 ++++++ 4 files changed, 77 insertions(+), 39 deletions(-) create mode 100644 tests/test_build_dashboard.py 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