Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
@@ -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."
---
Expand Down
76 changes: 39 additions & 37 deletions modules/application-tracker/scripts/build_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<span title="'+esc(L.conv_deleted_title)+'" style="color:var(--color-text-tertiary); font-style:italic; white-space:nowrap;">'+esc(dateShort)+'</span>';
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 '<span title="'+esc(it.fullDate + " \u2014 " + L.conv_deleted_title)+'" style="color:var(--color-text-tertiary); font-style:italic;">\u2717</span>';
}
// 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 '<span title="'+esc(L.conv_desktop_title)+'" style="color:var(--color-text-secondary); user-select:all; -webkit-user-select:all; cursor:text;">'+esc(marker)+'</span>';
}
// Web: clickable link (new tab, same account).
return '<a href="'+esc(url)+'" target="_blank" rel="noopener" title="'+esc(rawLabel||url)+'" style="color:var(--color-text-info); white-space:nowrap; text-decoration:none;">'+esc(dateShort)+'</a>';
var tip = it.fullDate + ((idx === titleIdx && rt) ? " \u00b7 " + rt : "");
return '<a href="'+esc(it.url)+'" target="_blank" rel="noopener" title="'+esc(tip)+'" style="color:var(--color-text-info); text-decoration:none;">\u2197</a>';
}
// 3) Current conversation / not yet linked → grey, ◆ suffix
var title3 = here ? L.conv_current_title : L.conv_unlinked_title;
return '<span title="'+esc(title3)+'" style="color:var(--color-text-tertiary); white-space:nowrap;">'+esc(dateShort + (here ? "\u2009\u25C6" : ""))+'</span>';
// 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 '<span title="'+esc(it.fullDate + " \u2014 " + title3)+'" style="color:var(--color-text-tertiary);">'+glyph3+'</span>';
}).join(' <span style="color:var(--color-text-tertiary);">\u00b7</span> ');
}

Expand Down Expand Up @@ -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
? '<span style="color: var(--color-text-secondary);">\ud83d\udccb '+esc(L.legend_desktop_example)+'</span>\u2003'+esc(L.legend_desktop_text)
: '<span style="color: var(--color-text-info);">05-28</span>\u2003'+esc(L.legend_web_clickable);
: '<span style="color: var(--color-text-info);">\u2197</span>\u2003'+esc(L.legend_web_clickable);
document.getElementById("legCurrent").innerHTML =
'<span style="color: var(--color-text-tertiary);">05-29\u2009\u25C6</span>\u2003'+esc(L.legend_current);
document.getElementById("legLinked").innerHTML =
'<span style="color: var(--color-text-info);">05-29</span>\u2003'+esc(L.legend_linked);
'<span style="color: var(--color-text-tertiary);">\u25C6</span>\u2003'+esc(L.legend_current);
document.getElementById("legLinked").innerHTML = "";
document.getElementById("legDeleted").innerHTML =
'<span style="color: var(--color-text-tertiary); font-style:italic;">05-30\u2009\u2717</span>\u2003'+esc(L.legend_deleted);
'<span style="color: var(--color-text-tertiary); font-style:italic;">\u2717</span>\u2003'+esc(L.legend_deleted);
}

// ---- Filters: populating the selects ----
Expand Down
25 changes: 25 additions & 0 deletions tests/test_build_dashboard.py
Original file line number Diff line number Diff line change
@@ -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