@nightkatana/kronosys-app 1.0.0-beta.21 → 1.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/app/changelog/page.tsx +87 -19
  3. package/app/globals.css +10 -8
  4. package/app/guide/page.tsx +71 -34
  5. package/app/implementation/page.tsx +70 -60
  6. package/app/licenses/page.tsx +79 -47
  7. package/app/logs/page.tsx +103 -47
  8. package/app/page.tsx +104 -169
  9. package/app/reporting/page.tsx +1918 -1436
  10. package/app/settings/page.tsx +66 -44
  11. package/components/KronosysPayloadProvider.tsx +19 -5
  12. package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
  13. package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
  14. package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
  15. package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
  16. package/components/dashboard/AppShellRouteNav.tsx +336 -209
  17. package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
  18. package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
  19. package/components/dashboard/DashboardCommandCenter.tsx +119 -30
  20. package/components/dashboard/KronoFocusPanel.tsx +287 -260
  21. package/components/dashboard/LanguageMenu.tsx +23 -7
  22. package/components/dashboard/PageRefreshButton.tsx +42 -16
  23. package/components/dashboard/ReportingTour.tsx +20 -2
  24. package/components/dashboard/SessionListPanel.tsx +4 -4
  25. package/components/dashboard/ThemeToggle.tsx +4 -3
  26. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
  27. package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
  28. package/lib/appShellHeaderClasses.ts +22 -3
  29. package/lib/appShellToolbarChrome.ts +112 -0
  30. package/lib/appShellToolbarDeferredIntents.ts +112 -0
  31. package/lib/appShellToolbarSessionSlices.ts +67 -0
  32. package/lib/dashboardCopy.ts +78 -29
  33. package/lib/dashboardQuickSearch.ts +37 -6
  34. package/lib/dashboardUrlSession.ts +36 -0
  35. package/lib/generatedUserChangelog.ts +14 -0
  36. package/lib/implementationNotes.ts +18 -14
  37. package/lib/reportingAggregate.ts +68 -9
  38. package/lib/reportingMetricHelp.ts +8 -8
  39. package/lib/reportingStrings.ts +118 -9
  40. package/lib/reportingTagWeekBreakdown.ts +55 -13
  41. package/lib/settingsCopy.ts +6 -7
  42. package/lib/userGuideCopy.ts +29 -26
  43. package/package.json +7 -5
  44. package/server/db.ts +6 -4
  45. package/server/dbSchema.ts +2 -2
  46. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
@@ -9,13 +9,8 @@ export type DashboardStrings = {
9
9
  kronoFocusStart: string;
10
10
  kronoFocusPause: string;
11
11
  kronoFocusReset: string;
12
- /** Paragraphe du popover d’aide du panneau KronoFocus ; vide = pas d’aide (le bouton ? est masqué). */
13
- kronoFocusStandaloneSubtitle: string;
14
- /** Deuxième paragraphe du même popover ; vide = ignoré. */
15
- kronoFocusAutoRefreshNote: string;
16
- /** aria-label du bouton d’aide à côté du titre KronoFocus. */
17
- kronoFocusPanelHelpAriaLabel: string;
18
- kronoFocusLiveWhileViewingArchive: string;
12
+ /** Infobulle des boutons KronoFocus désactivés pendant la consultation d’une archive. */
13
+ kronoFocusControlsReadOnlyTooltip: string;
19
14
  /** Ligne sous KronoFocus quand il a été lancé depuis une tâche (avant le nom cliquable) */
20
15
  kronoFocusLinkedTaskIntro: string;
21
16
  kronoFocusEditDurationTitle: string;
@@ -712,8 +707,10 @@ export type DashboardStrings = {
712
707
  reportingTourStep3Body: string;
713
708
  reportingTourStep4Title: string;
714
709
  reportingTourStep4Body: string;
715
- reportingTourStep5Title: string;
716
- reportingTourStep5Body: string;
710
+ reportingTourStep5aTitle: string;
711
+ reportingTourStep5aBody: string;
712
+ reportingTourStep5bTitle: string;
713
+ reportingTourStep5bBody: string;
717
714
  reportingTourStep6Title: string;
718
715
  reportingTourStep6Body: string;
719
716
  /** Modale « nouvelle session » — titre. */
@@ -766,6 +763,28 @@ export type DashboardStrings = {
766
763
  appShellRouteNavGlobalResume: string;
767
764
  /** Infobulle lorsque la pause globale est inactive (rien ne tourne). */
768
765
  appShellRouteNavGlobalPauseDisabledTooltip: string;
766
+ /** Infobulle : Gantt du jour uniquement depuis le tableau de bord. */
767
+ appShellRouteNavGanttDashboardOnlyTooltip: string;
768
+ /** Infobulle : pause globale uniquement depuis le tableau de bord (session live). */
769
+ appShellRouteNavGlobalPauseDashboardOnlyTooltip: string;
770
+ /** Infobulle : pause globale indisponible sur le tableau de bord (archive consultée ou chargement). */
771
+ appShellRouteNavGlobalPauseNoSessionContextTooltip: string;
772
+ /** Infobulle : entrée changelog masquée sur cette route (libellé vide). */
773
+ appShellRouteNavChangelogUnavailableTooltip: string;
774
+ /** Infobulle : entrée implémentation masquée sur cette route (libellé vide). */
775
+ appShellRouteNavImplementationUnavailableTooltip: string;
776
+ /** `aria-label` du bandeau KronoFocus pendant l’absence de charge utile session. */
777
+ appShellHeaderKronoFocusLoadingAria: string;
778
+ /** `aria-label` du groupe de navigation : tableau de bord, rapports, paramètres, journaux. */
779
+ appShellRouteNavGroupAppAria: string;
780
+ /** `aria-label` du groupe : guide, détails d’implémentation. */
781
+ appShellRouteNavGroupDocsAria: string;
782
+ /** `aria-label` du groupe : Gantt du jour, pause globale (ou emplacement réservé). */
783
+ appShellRouteNavGroupToolsAria: string;
784
+ /** `aria-label` du groupe : page licences (lecture seule). */
785
+ appShellRouteNavGroupLegalAria: string;
786
+ /** `aria-label` du ruban : thème, rafraîchissement, langue. */
787
+ appShellUtilityToolbarGroupAria: string;
769
788
  /** Modale — titre avant d’activer la pause globale. */
770
789
  globalPauseConfirmTitle: string;
771
790
  /** Modale — texte d’introduction (comportement + liste à suivre). */
@@ -863,12 +882,8 @@ const en: DashboardStrings = {
863
882
  kronoFocusStart: "Start",
864
883
  kronoFocusPause: "Pause",
865
884
  kronoFocusReset: "Reset",
866
- kronoFocusStandaloneSubtitle:
867
- "KronoFocus is Kronosys’s attention rhythm: one work segment at a time (duration you set), short pauses between segments, and a longer pause after several cycles. The counters and phases belong to Kronosysthey are not a Pomodoro timer clone.",
868
- kronoFocusAutoRefreshNote: "",
869
- kronoFocusPanelHelpAriaLabel: "KronoFocus panel help",
870
- kronoFocusLiveWhileViewingArchive:
871
- "While you browse an archived session, this still shows KronoFocus for the active live session (if any).",
885
+ kronoFocusControlsReadOnlyTooltip:
886
+ "Read-only while you inspect an archivethese controls target the live session.",
872
887
  kronoFocusLinkedTaskIntro: "Started from:",
873
888
  kronoFocusEditDurationTitle: "Configure KronoFocus rhythm (timer not started)",
874
889
  kronoFocusDurationPickerLabel: "Duration (hours : minutes : seconds)",
@@ -1384,7 +1399,7 @@ const en: DashboardStrings = {
1384
1399
  "This area shows your Git author line and forge account when they are configured in Settings → Git identity, the workspace roots detected for metrics, and a small badge for how session data is stored here: SQLite (default), JSON driver, or MongoDB mirror when enabled.\n\nSQLite keeps the dashboard payload on this machine under your Kronosys data directory; this page reads and writes it through the local API.",
1385
1400
  tourStep8Title: "What is KronoFocus?",
1386
1401
  tourStep8Body:
1387
- "**KronoFocus** paces your session with a configurable work segment, short breaks, then a long break after several cycles. It is built into Kronosys (counts, phases, how it ties to the session) and is not limited to a fixed Pomodoro-style recipe.\n\nShowing it here or next to task actions is configured under Settings → Dashboard & API (KronoFocus).",
1402
+ "**KronoFocus** paces your session with a configurable work segment, short breaks, then a long break after several cycles. It is built into Kronosys (counts, phases, how it ties to the session) and is not limited to a fixed Pomodoro-style recipe.\n\nThe header toolbar always includes KronoFocus so navigation stays aligned on every page. Whether you also show it next to task actions is configured under Settings → Dashboard & API (KronoFocus).",
1388
1403
  tourStep8LearnMoreUrl: "https://en.wikipedia.org/wiki/Time_management",
1389
1404
  tourStep8LearnMoreLabel: "Further reading — time management (new tab)",
1390
1405
  tourStep8LearnMoreAriaLabel: "Opens the English Wikipedia article on time management in a new tab",
@@ -1432,9 +1447,12 @@ const en: DashboardStrings = {
1432
1447
  reportingTourStep4Title: "Daily charts",
1433
1448
  reportingTourStep4Body:
1434
1449
  "Bar views show sessions, tasks by status, recorded task time, and session wall-clock by calendar day. When several weeks exist, use the week strip to align charts and tag calendars.",
1435
- reportingTourStep5Title: "Time by tag and by project",
1436
- reportingTourStep5Body:
1437
- "Review recorded task minutes grouped by #tags (including the untagged bucket when it applies) and by @project, with weekly grids when data spans multiple days.",
1450
+ reportingTourStep5aTitle: "Time by tag",
1451
+ reportingTourStep5aBody:
1452
+ "Review recorded task minutes grouped by #tags (including the untagged bucket when it applies), with weekly grids when data spans multiple days.",
1453
+ reportingTourStep5bTitle: "Time by project (Work vs Personal)",
1454
+ reportingTourStep5bBody:
1455
+ "The weekly-by-project grid has a Work (@) / Personal (!) toggle under the section title: Work (default) excludes personal-ledger minutes; Personal shows only that ledger (including tasks inferred from a ! token in the title when the saved flag is missing). Tag totals above still mix both ledgers unless you filter by tags.",
1438
1456
  reportingTourStep6Title: "Outline and two-column layout",
1439
1457
  reportingTourStep6Body:
1440
1458
  "This block is the main Reporting layout: your content on the left and, on wide screens, a sticky jump list on the right. On phones, the same anchors appear as chips above the filters.",
@@ -1483,6 +1501,22 @@ const en: DashboardStrings = {
1483
1501
  appShellRouteNavGlobalResume: "Resume everything paused by global pause in this context",
1484
1502
  appShellRouteNavGlobalPauseDisabledTooltip:
1485
1503
  "Nothing is active to pause: the session wall clock is already paused and no task or subtask timer is running.",
1504
+ appShellRouteNavGanttDashboardOnlyTooltip:
1505
+ "Today’s Gantt is available from the dashboard only.",
1506
+ appShellRouteNavGlobalPauseDashboardOnlyTooltip:
1507
+ "Global pause is available from the dashboard when a live session is loaded.",
1508
+ appShellRouteNavGlobalPauseNoSessionContextTooltip:
1509
+ "Global pause is not available while you inspect an archived session or before the live session is loaded.",
1510
+ appShellRouteNavChangelogUnavailableTooltip:
1511
+ "Changelog shortcut is hidden on this screen (no label).",
1512
+ appShellRouteNavImplementationUnavailableTooltip:
1513
+ "Implementation shortcut is hidden on this screen (no label).",
1514
+ appShellHeaderKronoFocusLoadingAria: "KronoFocus — waiting for session data",
1515
+ appShellRouteNavGroupAppAria: "Main app areas",
1516
+ appShellRouteNavGroupDocsAria: "Help and reference",
1517
+ appShellRouteNavGroupToolsAria: "In-session tools",
1518
+ appShellRouteNavGroupLegalAria: "Legal information",
1519
+ appShellUtilityToolbarGroupAria: "Display and language",
1486
1520
  globalPauseConfirmTitle: "Global pause",
1487
1521
  globalPauseConfirmIntro:
1488
1522
  "Global pause freezes the session wall clock when it is running, pauses each task timer that is not already paused, and stops any active subtask timer (recording elapsed time). The next click on this control resumes only what was paused in this operation.\n\nReview what will be affected:",
@@ -1540,12 +1574,8 @@ const fr: DashboardStrings = {
1540
1574
  kronoFocusStart: "Démarrer",
1541
1575
  kronoFocusPause: "Pause",
1542
1576
  kronoFocusReset: "Réinitialiser",
1543
- kronoFocusStandaloneSubtitle:
1544
- "KronoFocus est le rythme d’attention intégré à Kronosys : un segment de travail à la fois (durée au choix), des pauses courtes entre segments, puis une pause longue après plusieurs cycles. Les compteurs et phases sont propres à Kronosys — ce n’est pas un clone de minuteur Pomodoro.",
1545
- kronoFocusAutoRefreshNote: "",
1546
- kronoFocusPanelHelpAriaLabel: "Aide sur le panneau KronoFocus",
1547
- kronoFocusLiveWhileViewingArchive:
1548
- "Pendant la consultation d’une session archivée, ce panneau affiche encore KronoFocus pour la session en cours (s’il y en a une).",
1577
+ kronoFocusControlsReadOnlyTooltip:
1578
+ "Lecture seule pendant l’inspection d’une archive ces commandes s’appliquent à la session en cours.",
1549
1579
  kronoFocusLinkedTaskIntro: "Lancé depuis :",
1550
1580
  kronoFocusEditDurationTitle: "Configurer le rythme KronoFocus (minuteur non démarré)",
1551
1581
  kronoFocusDurationPickerLabel: "Durée (heures : minutes : secondes)",
@@ -2077,7 +2107,7 @@ const fr: DashboardStrings = {
2077
2107
  "Ici s’affichent la ligne d’auteur Git et le compte forge lorsqu’ils sont renseignés dans Paramètres → Identité Git, les racines de workspace détectées pour les métriques, et une pastille indiquant comment les sessions sont enregistrées ici : SQLite (par défaut), pilote JSON ou miroir MongoDB si activé.\n\nSQLite conserve la charge utile du tableau de bord sur cette machine dans le répertoire de données Kronosys ; cette page lit et écrit via l’API locale.",
2078
2108
  tourStep8Title: "Qu’est-ce que KronoFocus ?",
2079
2109
  tourStep8Body:
2080
- "**KronoFocus** cadence votre session : un segment de travail dont vous choisissez la durée, des pauses courtes, puis une pause longue après plusieurs cycles. Ce dispositif est intégré à Kronosys (compteurs, phases, lien à la session) ; il ne se résume pas à une méthode Pomodoro figée.\n\nLaffichage dans len-tête ou à côté des actions de tâche se règle dans Paramètres → Tableau de bord et API (section KronoFocus).",
2110
+ "**KronoFocus** cadence votre session : un segment de travail dont vous choisissez la durée, des pauses courtes, puis une pause longue après plusieurs cycles. Ce dispositif est intégré à Kronosys (compteurs, phases, lien à la session) ; il ne se résume pas à une méthode Pomodoro figée.\n\nLa barre doutils affiche toujours KronoFocus pour garder la navigation alignée sur toutes les pages. Laffichage à côté des actions de tâche se règle toujours dans Paramètres → Tableau de bord et API (section KronoFocus).",
2081
2111
  tourStep8LearnMoreUrl: "https://fr.wikipedia.org/wiki/Gestion_du_temps",
2082
2112
  tourStep8LearnMoreLabel: "Pour aller plus loin — gestion du temps (nouvel onglet)",
2083
2113
  tourStep8LearnMoreAriaLabel: "Ouvre l’article Wikipédia sur la gestion du temps dans un nouvel onglet",
@@ -2125,9 +2155,12 @@ const fr: DashboardStrings = {
2125
2155
  reportingTourStep4Title: "Graphiques par jour",
2126
2156
  reportingTourStep4Body:
2127
2157
  "Les barres montrent les sessions, les tâches par état, le temps enregistré sur les tâches et la durée murale des sessions par jour calendaire. Quand plusieurs semaines existent, la bande de navigation aligne graphiques et calendriers par étiquette.",
2128
- reportingTourStep5Title: "Temps par étiquette et par projet",
2129
- reportingTourStep5Body:
2130
- "Consultez les minutes enregistrées sur les tâches regroupées par #étiquettes (y compris le compartiment sans étiquette explicite le cas échéant) et par @projet, avec des grilles hebdomadaires lorsque les données couvrent plusieurs jours.",
2158
+ reportingTourStep5aTitle: "Temps par étiquette",
2159
+ reportingTourStep5aBody:
2160
+ "Consultez les minutes enregistrées sur les tâches regroupées par #étiquettes (y compris le compartiment sans étiquette explicite le cas échéant), avec des grilles hebdomadaires lorsque les données couvrent plusieurs jours.",
2161
+ reportingTourStep5bTitle: "Temps par projet (Travail vs Personnel)",
2162
+ reportingTourStep5bBody:
2163
+ "La grille hebdomadaire par projet propose un sélecteur Travail (@) / Personnel (!) sous le titre de la section : Travail (défaut) exclut le registre personnel ; Personnel n’affiche que celui-ci (y compris les tâches déduites d’un jeton « ! » dans le titre si le drapeau enregistré manque). Les totaux par étiquette au-dessus combinent toujours les deux registres, sauf filtre par étiquettes.",
2131
2164
  reportingTourStep6Title: "Sommaire et mise en page à deux colonnes",
2132
2165
  reportingTourStep6Body:
2133
2166
  "Ce bloc correspond à la disposition principale de Rapports : le contenu à gauche et, sur grand écran, le sommaire fixe à droite. Sur téléphone, les mêmes ancres sont proposées en pastilles au-dessus des filtres.",
@@ -2178,6 +2211,22 @@ const fr: DashboardStrings = {
2178
2211
  "Reprendre uniquement ce qui a été mis en pause globalement dans ce contexte",
2179
2212
  appShellRouteNavGlobalPauseDisabledTooltip:
2180
2213
  "Rien n’est actif à mettre en pause : l’horloge murale est déjà figée et aucun minuteur de tâche ou de sous-tâche ne tourne.",
2214
+ appShellRouteNavGanttDashboardOnlyTooltip:
2215
+ "Le Gantt du jour n’est disponible que depuis le tableau de bord.",
2216
+ appShellRouteNavGlobalPauseDashboardOnlyTooltip:
2217
+ "La pause globale n’est disponible que depuis le tableau de bord lorsqu’une session live est chargée.",
2218
+ appShellRouteNavGlobalPauseNoSessionContextTooltip:
2219
+ "La pause globale n’est pas disponible pendant la consultation d’une archive ou avant le chargement de la session live.",
2220
+ appShellRouteNavChangelogUnavailableTooltip:
2221
+ "Raccourci changelog masqué sur cet écran (libellé vide).",
2222
+ appShellRouteNavImplementationUnavailableTooltip:
2223
+ "Raccourci implémentation masqué sur cet écran (libellé vide).",
2224
+ appShellHeaderKronoFocusLoadingAria: "KronoFocus — attente des données de session",
2225
+ appShellRouteNavGroupAppAria: "Zones principales de l’application",
2226
+ appShellRouteNavGroupDocsAria: "Aide et documentation",
2227
+ appShellRouteNavGroupToolsAria: "Outils de session",
2228
+ appShellRouteNavGroupLegalAria: "Informations légales",
2229
+ appShellUtilityToolbarGroupAria: "Affichage et langue",
2181
2230
  globalPauseConfirmTitle: "Pause globale",
2182
2231
  globalPauseConfirmIntro:
2183
2232
  "La pause globale fige l’horloge murale de la session si elle tourne encore, met en pause chaque minuteur de tâche qui ne l’est pas déjà et arrête tout minuteur de sous-tâche actif (en consolidant le temps écoulé). Le prochain clic sur ce contrôle ne reprend que ce qui a été figé dans cette opération.\n\nVoici ce qui sera concerné :",
@@ -12,6 +12,7 @@ import {
12
12
  normalizeTagKey,
13
13
  parseProjectScopedTag,
14
14
  parseTaskWithAutoTags,
15
+ resolvePersonalProjectForTaskUpdate,
15
16
  taskTitleForDisplay,
16
17
  } from "@/lib/taskParsing";
17
18
 
@@ -48,6 +49,7 @@ type TaskLike = {
48
49
  name?: string;
49
50
  tags?: unknown;
50
51
  project?: unknown;
52
+ personalProject?: boolean;
51
53
  isDone?: boolean;
52
54
  manualTaskTimerPaused?: boolean;
53
55
  /** ISO 8601 côté modèle tâche (début d’enregistrement / entrée passée). */
@@ -100,7 +102,8 @@ function dateHayFromFields(...fields: (string | null | undefined)[]): string {
100
102
  function buildTaskSearchIndex(
101
103
  rawName: string,
102
104
  storedTags: unknown,
103
- storedProject: unknown
105
+ storedProject: unknown,
106
+ personalProject?: boolean,
104
107
  ): { haystackFragment: string; metaLine: string } {
105
108
  const storedTagArr = Array.isArray(storedTags)
106
109
  ? storedTags.filter((x): x is string => typeof x === "string")
@@ -111,9 +114,22 @@ function buildTaskSearchIndex(
111
114
  (typeof storedProject === "string" && storedProject.trim() ? storedProject.trim() : undefined) ??
112
115
  parsed.project;
113
116
 
117
+ const personalLedger =
118
+ personalProject === true || resolvePersonalProjectForTaskUpdate(rawName) === true;
119
+
114
120
  const hayPieces: string[] = [];
115
121
  if (projRaw) {
116
- hayPieces.push(projRaw, normalizeProjectKey(projRaw), formatProjectDisplay(projRaw), `@${normalizeProjectKey(projRaw)}`);
122
+ const pkey = normalizeProjectKey(projRaw);
123
+ hayPieces.push(
124
+ projRaw,
125
+ pkey,
126
+ formatProjectDisplay(projRaw, personalLedger ? { personal: true } : undefined),
127
+ );
128
+ if (personalLedger) {
129
+ hayPieces.push(`!${pkey}`, "personnel", "personal", "registre personnel", "personal ledger");
130
+ } else {
131
+ hayPieces.push(`@${pkey}`);
132
+ }
117
133
  }
118
134
  for (const tag of tagList) {
119
135
  hayPieces.push(tag, normalizeTagKey(tag), formatTagDisplay(tag));
@@ -130,7 +146,7 @@ function buildTaskSearchIndex(
130
146
 
131
147
  const metaParts: string[] = [];
132
148
  if (projRaw) {
133
- metaParts.push(formatProjectDisplay(projRaw));
149
+ metaParts.push(formatProjectDisplay(projRaw, personalLedger ? { personal: true } : undefined));
134
150
  }
135
151
  for (const tag of tagList) {
136
152
  metaParts.push(formatTagDisplay(tag));
@@ -353,7 +369,12 @@ export function buildDashboardQuickSearchItems(opts: {
353
369
  const seenMeta = new Set<string>();
354
370
  for (const t of tasks) {
355
371
  const rawName = typeof t.name === "string" ? t.name : "";
356
- const idx = buildTaskSearchIndex(rawName, t.tags, t.project);
372
+ const idx = buildTaskSearchIndex(
373
+ rawName,
374
+ t.tags,
375
+ t.project,
376
+ t.personalProject === true,
377
+ );
357
378
  hayFrags.push(idx.haystackFragment);
358
379
  if (idx.metaLine) {
359
380
  for (const part of idx.metaLine.split(" · ")) {
@@ -438,7 +459,12 @@ export function buildDashboardQuickSearchItems(opts: {
438
459
  }
439
460
  const rawName = typeof t.name === "string" ? t.name : "";
440
461
  const display = taskTitleForDisplay(rawName);
441
- const idx = buildTaskSearchIndex(rawName, t.tags, t.project);
462
+ const idx = buildTaskSearchIndex(
463
+ rawName,
464
+ t.tags,
465
+ t.project,
466
+ t.personalProject === true,
467
+ );
442
468
  const titleKey = (display || id.slice(0, 8)).trim() || id.slice(0, 8);
443
469
  const taskTimeHay = dateHayFromFields(
444
470
  readOptionalIsoString(t.startTime),
@@ -502,7 +528,12 @@ export function buildDashboardQuickSearchItems(opts: {
502
528
  continue;
503
529
  }
504
530
  const rawName = typeof tpl.name === "string" ? tpl.name : "";
505
- const idx = buildTaskSearchIndex(rawName, tpl.tags, tpl.project);
531
+ const idx = buildTaskSearchIndex(
532
+ rawName,
533
+ tpl.tags,
534
+ tpl.project,
535
+ tpl.personalProject === true,
536
+ );
506
537
  const displayTitle =
507
538
  taskTitleForDisplay(rawName).trim() || tid.slice(0, 8);
508
539
  const hay = `${rawName} ${displayTitle} ${tid} ${draft} ${idx.haystackFragment} ${tmplHayExtra} ${dt.dataSearchKindTaskTemplate}`
@@ -0,0 +1,36 @@
1
+ import type { SessionListEntry } from "@/components/dashboard/SessionListPanel";
2
+ import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
3
+
4
+ /** Minimal `payload.current` pour la résolution `?session=`. */
5
+ export type UrlSessionLiveShape = { sessionId?: string };
6
+
7
+ export type UrlSessionResolution =
8
+ | { mode: "none" }
9
+ | { mode: "loading" }
10
+ | { mode: "ok"; id: string }
11
+ | { mode: "invalid" };
12
+
13
+ export function resolveUrlSession(
14
+ urlParam: string | null,
15
+ payload: KronosysUpdatePayload | null,
16
+ live: UrlSessionLiveShape | undefined,
17
+ history: SessionListEntry[],
18
+ historyArchived: SessionListEntry[],
19
+ ): UrlSessionResolution {
20
+ if (!urlParam) {
21
+ return { mode: "none" };
22
+ }
23
+ if (!payload) {
24
+ return { mode: "loading" };
25
+ }
26
+ const liveSid =
27
+ typeof live?.sessionId === "string" ? live.sessionId.trim() : "";
28
+ if (
29
+ (liveSid !== "" && urlParam === liveSid) ||
30
+ history.some((s) => s.sessionId === urlParam) ||
31
+ historyArchived.some((s) => s.sessionId === urlParam)
32
+ ) {
33
+ return { mode: "ok", id: urlParam };
34
+ }
35
+ return { mode: "invalid" };
36
+ }
@@ -5,6 +5,20 @@ export type UserChangelogEntry = {
5
5
  };
6
6
 
7
7
  export const USER_CHANGELOG_ENTRIES: UserChangelogEntry[] = [
8
+ {
9
+ "version": "1.0.0-beta.22",
10
+ "items": [
11
+ "**Plus de barre de défilement horizontale** sur la barre d’outils d’en-tête (`lib/appShellHeaderClasses.ts`) : les segments **horloge / KronoFocus / palette de commandes / navigation de routes / utilitaires (thème, actualiser, langue)** passent désormais **à la ligne** (`flex-wrap`) lorsqu’ils ne tiennent plus, en gardant chaque rangée **centrée**, au lieu de produire un ascenseur horizontal sur le conteneur. Le comportement est identique sur **toutes** les routes principales (tableau de bord, rapports, paramètres, journaux, guide, implémentation, changelog, licences).",
12
+ "**Bandeau « Données actualisées » du bouton actualiser** (`components/dashboard/PageRefreshButton.tsx`) : rendu via **portail** vers `document.body` avec **`position: fixed`** au lieu d’un positionnement absolu sous le bouton. Corrige l’apparition intermittente d’une **barre de défilement verticale** sur la page à chaque clic — l’élément absolu participait au débordement du `<body>` et faisait osciller le `scrollHeight` pendant les ~2,8 s d’affichage du libellé.",
13
+ "**Popover d’édition de durée KronoFocus** (`components/dashboard/KronoFocusPanel.tsx`) : ouvrir le temps depuis l’en-tête (`variant=\"headerBar\"`) ne déclenche plus l’apparition de **scrollbars sur les deux axes** dans l’alvéole du ruban. La cause était la combinaison `overflow-x-auto` sur l’alvéole + popover en `position: absolute` : selon la spec CSS, dès qu’un axe d’overflow n’est pas `visible`, l’autre est promu en `auto`, donc deux ascenseurs apparaissaient sitôt que le panneau dépassait. Le popover est désormais rendu en **portail** avec `position: fixed` centré sous le bouton du temps. Détection click-outside révisée pour vérifier le **déclencheur** et le **panneau** par leurs `ref` distinctes (ils ne partagent plus de sous-arbre DOM).",
14
+ "**Surcouche carte « complète »** (variante `default` de `KronoFocusPanel`) alignée sur le même pattern portail pour rester cohérente.",
15
+ "**`useAnchoredFloatingPortalStyle`** (`components/dashboard/useAnchoredFloatingPortalStyle.ts`) gagne l’alignement **`\"center\"`** en plus de `start` / `end` : le panneau portail peut maintenant être centré horizontalement sous son déclencheur. Les autres consommateurs (`PageRefreshButton`, `PlainHelpPopover`, `TagsHelpTrigger`, `InlineMetricHelpTrigger`) conservent leurs alignements existants ; le hook continue de gérer la contrainte au viewport (marge 10 px) et le **basculement au-dessus** du déclencheur s’il manque de place en bas.",
16
+ "**Guide utilisateur** : `lib/userGuideCopy.ts` et `docs/USER_GUIDE.md` (FR/EN) — note explicite sur le repli en plusieurs rangées de la barre d’outils sur écran étroit ; en-tête de version pointe **`1.0.0-beta.22`**.",
17
+ "**Implémentation** : `lib/implementationNotes.ts` (FR + EN, `lastUpdated`) ; `docs/IMPLEMENTATION_DETAILS.md` — date d’en-tête et note sur le pattern « portail + `position: fixed` » pour les surcouches ancrées à des éléments d’en-tête (évite la propagation au `scrollableOverflow` du `<body>`).",
18
+ "**Dépôt** : `README.md` (référence de version npm).",
19
+ "Bump applicatif : **`1.0.0-beta.22`** (`package.json`, `package-lock.json`) ; **CHANGELOG** usager régénéré (`npm run changelog:build`)."
20
+ ]
21
+ },
8
22
  {
9
23
  "version": "1.0.0-beta.21",
10
24
  "items": [
@@ -55,7 +55,7 @@ const frBundle: ImplementationNotesBundle = {
55
55
  "Liste exhaustive des intentions utilisateur·rice·s : chaque ligne indique si la capacité est livrée (✓) ou absente / hors périmètre (✗). Les cases à droite servent à cocher une ligne « revue » sur cet appareil — le détail technique long reste dans le dépôt.",
56
56
  statusKeyLine:
57
57
  "✓ vert = implémenté dans le produit · ✗ rouge = non livré ou volontairement exclu.",
58
- lastUpdated: "Dernière mise à jour : 2026-05-12 (v1.0.0-beta.21)",
58
+ lastUpdated: "Dernière mise à jour : 2026-05-13 (v1.0.0-beta.22)",
59
59
  repositoryDocLabel: "Document dépôt : docs/IMPLEMENTATION_DETAILS.md",
60
60
  storyGroupsHeading: "Inventaire des user stories",
61
61
  implementationColumnLabel: "Implémentation",
@@ -92,7 +92,7 @@ const frBundle: ImplementationNotesBundle = {
92
92
  },
93
93
  {
94
94
  implemented: true,
95
- text: "Pour une livraison donnée (p. ex. 1.0.0-beta.21), valider le comportement en croisant la section correspondante du **CHANGELOG**, les récits marqués livrés sur cette page et `docs/IMPLEMENTATION_DETAILS.md`, puis cocher au besoin **intégration** / **E2E**.",
95
+ text: "Pour une livraison donnée (p. ex. 1.0.0-beta.22), valider le comportement en croisant la section correspondante du **CHANGELOG**, les récits marqués livrés sur cette page et `docs/IMPLEMENTATION_DETAILS.md`, puis cocher au besoin **intégration** / **E2E**.",
96
96
  },
97
97
  {
98
98
  implemented: true,
@@ -499,15 +499,17 @@ const frBundle: ImplementationNotesBundle = {
499
499
  implemented: true,
500
500
  integrationChecked: true,
501
501
  e2eChecked: false,
502
- text: "Sur la page Rapports, consulter les grilles par projet basées sur le temps productif (`@`) sans y mélanger le temps des projets personnels (`!`).",
502
+ text: "Sur la page Rapports, basculer la grille hebdomadaire par projet entre le registre travail (@) et le registre personnel (!) avec les mêmes filtres de plage et d’étiquettes.",
503
503
  detail:
504
- "`app/reporting/page.tsx` appelle `aggregateProjectTaskMinutesByDay` avec le scope par défaut `work` (`lib/reportingAggregate.ts`, `taskMatchesReportingProjectScope`) : les minutes des tâches `personalProject: true` sont exclues de ces grilles.",
504
+ "`app/reporting/page.tsx` appelle `aggregateProjectTaskMinutesByDay` avec le scope `work` ou `personal` selon le sélecteur (`lib/reportingAggregate.ts`, `taskMatchesReportingProjectScope`, `taskIsReportingPersonalLedger`). La vue **Travail** (défaut) exclut le registre personnel ; la vue **Personnel** ne l’inclut qu’elles, avec libellés `!` via `formatProjectDisplay`. Une tâche est **personnelle** pour l’agrégat si `personalProject` est vrai **ou** si le **titre** contient un jeton `!` reconnu (repli lorsque le drapeau manque sur d’anciennes données).",
505
505
  },
506
506
  {
507
- implemented: false,
508
- text: "Afficher sur la page Rapports un bloc ou calendrier dédié au temps personnel (`!`), parallèle aux vues projets `@`, avec les mêmes filtres de plage et d’étiquettes.",
507
+ implemented: true,
508
+ integrationChecked: true,
509
+ e2eChecked: false,
510
+ text: "Consulter par défaut les grilles par projet en temps productif (@) sans y mélanger le temps des projets personnels (!).",
509
511
  detail:
510
- "L’agrégat côté bibliothèque accepte `taskProjectScope: \"personal\"` (`aggregateProjectTaskMinutesByDay`, `aggregateReportingByProject`, `aggregateTagTaskMinutesByDayAndWeek`) ; aucune seconde grille « personnel » nest branchée dans l’UI au 2026-05-12.",
512
+ "Au chargement, le sélecteur est sur **Travail (@)** : même comportement quauparavant (scope `work` seul).",
511
513
  },
512
514
  {
513
515
  implemented: true,
@@ -622,7 +624,7 @@ const enBundle: ImplementationNotesBundle = {
622
624
  "An exhaustive list of user intentions: each row shows whether the capability is shipped (✓) or missing / out of scope (✗). Checkboxes on the right mark a row as reviewed on this device — long technical detail stays in the repo doc.",
623
625
  statusKeyLine:
624
626
  "Green ✓ = implemented in the product · Red ✗ = not shipped or intentionally excluded.",
625
- lastUpdated: "Last updated: 2026-05-12 (v1.0.0-beta.21)",
627
+ lastUpdated: "Last updated: 2026-05-13 (v1.0.0-beta.22)",
626
628
  repositoryDocLabel: "Repository document: docs/IMPLEMENTATION_DETAILS.md",
627
629
  storyGroupsHeading: "User story inventory",
628
630
  implementationColumnLabel: "Implementation",
@@ -659,7 +661,7 @@ const enBundle: ImplementationNotesBundle = {
659
661
  },
660
662
  {
661
663
  implemented: true,
662
- text: "For a given release (e.g. 1.0.0-beta.21), cross-check the matching **CHANGELOG** section, the shipped stories on this page and `docs/IMPLEMENTATION_DETAILS.md`, then tick **Integration** / **E2E** as appropriate.",
664
+ text: "For a given release (e.g. 1.0.0-beta.22), cross-check the matching **CHANGELOG** section, the shipped stories on this page and `docs/IMPLEMENTATION_DETAILS.md`, then tick **Integration** / **E2E** as appropriate.",
663
665
  },
664
666
  {
665
667
  implemented: true,
@@ -1066,15 +1068,17 @@ const enBundle: ImplementationNotesBundle = {
1066
1068
  implemented: true,
1067
1069
  integrationChecked: true,
1068
1070
  e2eChecked: false,
1069
- text: "On the Reporting page, read per-project grids that reflect productive (`@`) time without mixing in personal (`!`) project minutes.",
1071
+ text: "On the Reporting page, switch the weekly-by-project grid between the work (@) ledger and the personal (!) ledger with the same date-range and tag filters.",
1070
1072
  detail:
1071
- "`app/reporting/page.tsx` calls `aggregateProjectTaskMinutesByDay` with the default `work` scope (`lib/reportingAggregate.ts`, `taskMatchesReportingProjectScope`): tasks with `personalProject: true` are excluded from those grids.",
1073
+ "`app/reporting/page.tsx` calls `aggregateProjectTaskMinutesByDay` with scope `work` or `personal` from the toggle (`lib/reportingAggregate.ts`, `taskMatchesReportingProjectScope`, `taskIsReportingPersonalLedger`). **Work** (default) excludes the personal ledger; **Personal** includes only it, with `!` labels via `formatProjectDisplay`. A task is **personal** for aggregates when `personalProject` is true **or** when the **title** contains a recognized `!` token (fallback when the flag is missing on older rows).",
1072
1074
  },
1073
1075
  {
1074
- implemented: false,
1075
- text: "Show a dedicated personal-time (`!`) block or calendar on Reporting, parallel to `@` project views, with the same date-range and tag filters.",
1076
+ implemented: true,
1077
+ integrationChecked: true,
1078
+ e2eChecked: false,
1079
+ text: "By default, per-project grids show productive (@) time without mixing in personal (!) minutes.",
1076
1080
  detail:
1077
- "Library aggregates accept `taskProjectScope: \"personal\"` (`aggregateProjectTaskMinutesByDay`, `aggregateReportingByProject`, `aggregateTagTaskMinutesByDayAndWeek`); no second “personal” grid is wired in the UI as of 2026-05-12.",
1081
+ "On first load the toggle is **Work (@)** same behaviour as before (work scope only).",
1078
1082
  },
1079
1083
  {
1080
1084
  implemented: true,
@@ -2,9 +2,12 @@ import {
2
2
  DEFAULT_FALLBACK_TASK_TAG,
3
3
  formatProjectDisplay,
4
4
  formatTagDisplay,
5
+ formatTagDisplayForTask,
5
6
  isFallbackTaskTagKey,
6
7
  normalizeProjectKey,
7
8
  normalizeTagKey,
9
+ parseProjectScopedTag,
10
+ resolvePersonalProjectForTaskUpdate,
8
11
  } from "./taskParsing";
9
12
  import type { KronosysUpdatePayload } from "./kronosysApi";
10
13
  import { LEGACY_TASK_CYCLES_KEY, LEGACY_TASK_USED_FLAG_KEY } from "./legacyKronoFocusStorageKeys";
@@ -21,6 +24,8 @@ import { normalizeSessionEndReasonKind } from "./sessionEndReason";
21
24
 
22
25
  export type LooseTask = {
23
26
  id?: string;
27
+ /** Titre brut ; utilisé pour inférer `!` si `personalProject` est absent ou obsolète. */
28
+ name?: string;
24
29
  startTime?: string;
25
30
  endTime?: string;
26
31
  durationMs?: number;
@@ -38,11 +43,26 @@ export type LooseTask = {
38
43
  /** Filtre les tâches pour les agrégats « @ projet » vs « ! projet personnel ». */
39
44
  export type ReportingTaskProjectScope = "all" | "work" | "personal";
40
45
 
46
+ /**
47
+ * Registre personnel pour le reporting : drapeau persisté **ou** jeton `!` déduit du titre
48
+ * (rattrapage si `personalProject` n’a pas été enregistré sur d’anciennes tâches).
49
+ */
50
+ export function taskIsReportingPersonalLedger(task: LooseTask): boolean {
51
+ if (task.personalProject === true) {
52
+ return true;
53
+ }
54
+ const rawName = typeof task.name === "string" ? task.name.trim() : "";
55
+ if (!rawName) {
56
+ return false;
57
+ }
58
+ return resolvePersonalProjectForTaskUpdate(rawName) === true;
59
+ }
60
+
41
61
  export function taskMatchesReportingProjectScope(
42
62
  task: LooseTask,
43
63
  scope: ReportingTaskProjectScope,
44
64
  ): boolean {
45
- const isP = task.personalProject === true;
65
+ const isP = taskIsReportingPersonalLedger(task);
46
66
  if (scope === "all") {
47
67
  return true;
48
68
  }
@@ -424,6 +444,48 @@ function mergeIntervalsToMinutes(intervals: Array<[number, number]>): number {
424
444
  return totalMs / 60000;
425
445
  }
426
446
 
447
+ /** Entrée d’étiquette pour agrégats : clé canonique (sépare @ / ! sur `projet#suffixe`) + libellé. */
448
+ function taskTagReportingEntry(
449
+ raw: string,
450
+ task: LooseTask,
451
+ fallbackTagDisplay: string,
452
+ ): { key: string; display: string } | null {
453
+ const token = normalizeTagKey(String(raw)).trim();
454
+ if (!token) {
455
+ return null;
456
+ }
457
+ const lower = token.toLowerCase();
458
+ if (lower === DEFAULT_FALLBACK_TASK_TAG) {
459
+ return { key: lower, display: fallbackTagDisplay };
460
+ }
461
+
462
+ const scoped = parseProjectScopedTag(token);
463
+ const taskPersonal = taskIsReportingPersonalLedger(task);
464
+ const tpRaw = typeof task.project === "string" ? task.project.trim() : "";
465
+ const tp = tpRaw ? normalizeProjectKey(tpRaw) : "";
466
+
467
+ if (!scoped) {
468
+ return {
469
+ key: lower,
470
+ display: formatTagDisplayForTask(token, undefined, {
471
+ personalProject: taskPersonal,
472
+ taskProject: tp || null,
473
+ }),
474
+ };
475
+ }
476
+
477
+ const pk = normalizeProjectKey(scoped.projectKey);
478
+ const loc = scoped.localTag;
479
+ const sameProject = tp.length > 0 && pk.toLowerCase() === tp.toLowerCase();
480
+ const sigil: "@" | "!" = sameProject ? (taskPersonal ? "!" : "@") : "@";
481
+ const key = `${sigil}${pk}#${loc}`.toLowerCase();
482
+ const display = formatTagDisplayForTask(token, undefined, {
483
+ personalProject: taskPersonal,
484
+ taskProject: tp || null,
485
+ });
486
+ return { key, display };
487
+ }
488
+
427
489
  function uniqueTaskTagEntries(
428
490
  task: LooseTask,
429
491
  fallbackTagDisplay: string
@@ -432,18 +494,15 @@ function uniqueTaskTagEntries(
432
494
  const seen = new Set<string>();
433
495
  const out: Array<{ key: string; display: string }> = [];
434
496
  for (const raw of tags) {
435
- const token = normalizeTagKey(String(raw)).trim();
436
- if (!token) {
497
+ const one = taskTagReportingEntry(raw, task, fallbackTagDisplay);
498
+ if (!one) {
437
499
  continue;
438
500
  }
439
- const key = token.toLowerCase();
440
- if (seen.has(key)) {
501
+ if (seen.has(one.key)) {
441
502
  continue;
442
503
  }
443
- seen.add(key);
444
- const display =
445
- key === DEFAULT_FALLBACK_TASK_TAG ? fallbackTagDisplay : formatTagDisplay(token);
446
- out.push({ key, display });
504
+ seen.add(one.key);
505
+ out.push({ key: one.key, display: one.display });
447
506
  }
448
507
  return out;
449
508
  }
@@ -220,17 +220,17 @@ export const reportingMetricHelpEn: ReportingMetricHelpBlock = {
220
220
  "Sum of attributed task minutes for that tag and period. Minutes are not split across tags: a multi-tag task contributes its full duration to each tag row.",
221
221
  metricHelpProjectTitleAria: "Help: recorded time by project",
222
222
  metricHelpProjectTitleBody:
223
- "Recorded task duration (durationMs) split by each task’s @project, shown as a weekly calendar: one period line per week, then one table with a row per project and seven day columns plus a week total. Same date range, tag filter, archived-session rules, and week navigation as the tag calendar.",
223
+ "Recorded task duration (durationMs) split by each task’s project field, shown as a weekly calendar: one period line per week, then one table with a row per project and seven day columns plus a week total. Use the Work (@) / Personal (!) control to choose the ledger: Work (default) counts only non-personal tasks; Personal counts tasks marked personal (`personalProject`) and tasks whose title contains a recognized `!` project token (fallback when the flag was not saved), with `!` labels. Same date range, tag filter, archived-session rules, and week navigation as the tag calendar.",
224
224
  metricHelpProjectCalendarAria: "Help: project weekly calendar",
225
225
  metricHelpProjectCalendarBody:
226
- "Same layout as the tag calendar: for each week, the date span appears once; each row is one @project. Column headers show weekday and calendar date; day cells use task day (end or start); hover a header or cell for YYYY-MM-DD.",
226
+ "Same layout as the tag calendar: for each week, the date span appears once; each row is one project for the selected ledger (Work or Personal). Column headers show weekday and calendar date; day cells use task day (end or start); hover a header or cell for YYYY-MM-DD.",
227
227
  metricHelpProjectColProjAria: "Help: project column",
228
- metricHelpProjectColProjBody: "Project name from the task, or “no project” when none is set.",
228
+ metricHelpProjectColProjBody: "Project name from the task (with @ or ! depending on the ledger), or “no project” when none is set.",
229
229
  metricHelpProjectColTasksAria: "Help: tasks column",
230
230
  metricHelpProjectColTasksBody: "Number of task rows summed into that project row.",
231
231
  metricHelpProjectColTimeAria: "Help: recorded time column",
232
232
  metricHelpProjectColTimeBody:
233
- "Total recorded task minutes for that @project over the seven days of the row (from durationMs).",
233
+ "Total recorded task minutes for that project over the seven days of the row (from durationMs), for the selected Work or Personal ledger.",
234
234
  metricHelpWorkspaceTitleAria: "Help: open folder snapshot",
235
235
  metricHelpWorkspaceTitleBody:
236
236
  "Snapshot of the first workspace folder: line counts by language from Git-tracked files or a folder walk. Not tied to Kronosys sessions; cached ~5 minutes. See the paragraph below for details.",
@@ -378,17 +378,17 @@ export const reportingMetricHelpFr: ReportingMetricHelpBlock = {
378
378
  "Somme des minutes de tâche attribuées à cette étiquette sur la période de la ligne. Les minutes ne sont pas réparties entre les étiquettes : une tâche multi-étiquettes compte sa durée entière sur chaque ligne d’étiquette.",
379
379
  metricHelpProjectTitleAria: "Aide : temps enregistré par projet",
380
380
  metricHelpProjectTitleBody:
381
- "Durée enregistrée sur les tâches (durationMs) ventilée par @projet, sous forme de calendrier hebdomadaire : une ligne de période par semaine, puis un tableau avec une ligne par projet et sept colonnes jour plus le total de la semaine. Mêmes plage de dates, filtre d’étiquettes, règles d’archivage et navigation par semaine que le calendrier par étiquette.",
381
+ "Durée enregistrée sur les tâches (durationMs) ventilée par le champ projet de chaque tâche, sous forme de calendrier hebdomadaire : une ligne de période par semaine, puis un tableau avec une ligne par projet et sept colonnes jour plus le total de la semaine. Utilisez le sélecteur Travail (@) / Personnel (!) pour choisir le registre : Travail (défaut) ne compte que les tâches non personnelles ; Personnel compte les tâches marquées personnelles (`personalProject`) et celles dont le titre contient un jeton `!` reconnu (repli si le drapeau n’a pas été enregistré), avec libellés « ! ». Mêmes plage de dates, filtre d’étiquettes, règles d’archivage et navigation par semaine que le calendrier par étiquette.",
382
382
  metricHelpProjectCalendarAria: "Aide : calendrier hebdomadaire par projet",
383
383
  metricHelpProjectCalendarBody:
384
- "Même présentation que le calendrier par étiquette : pour chaque semaine, la plage de dates s’affiche une fois ; chaque ligne correspond à un @projet. Les en-têtes de colonnes indiquent le jour de la semaine et la date calendaire ; les cases jour utilisent le jour de la tâche (fin ou début) ; survol d’un en-tête ou d’une case pour AAAA-MM-JJ.",
384
+ "Même présentation que le calendrier par étiquette : pour chaque semaine, la plage de dates s’affiche une fois ; chaque ligne correspond à un projet pour le registre choisi (Travail ou Personnel). Les en-têtes de colonnes indiquent le jour de la semaine et la date calendaire ; les cases jour utilisent le jour de la tâche (fin ou début) ; survol d’un en-tête ou d’une case pour AAAA-MM-JJ.",
385
385
  metricHelpProjectColProjAria: "Aide : colonne projet",
386
- metricHelpProjectColProjBody: "Nom du projet sur la tâche, ou « sans projet » si absent.",
386
+ metricHelpProjectColProjBody: "Nom du projet sur la tâche (avec @ ou ! selon le registre), ou « sans projet » si absent.",
387
387
  metricHelpProjectColTasksAria: "Aide : colonne tâches",
388
388
  metricHelpProjectColTasksBody: "Nombre de lignes de tâches agrégées pour ce projet.",
389
389
  metricHelpProjectColTimeAria: "Aide : colonne temps enregistré",
390
390
  metricHelpProjectColTimeBody:
391
- "Total des minutes enregistrées sur les tâches pour ce @projet sur les sept jours de la ligne (durationMs).",
391
+ "Total des minutes enregistrées sur les tâches pour ce projet sur les sept jours de la ligne (durationMs), pour le registre Travail ou Personnel sélectionné.",
392
392
  metricHelpWorkspaceTitleAria: "Aide : instantané du dossier ouvert",
393
393
  metricHelpWorkspaceTitleBody:
394
394
  "Instantané du premier dossier du workspace : lignes par langage à partir des fichiers suivis par Git ou d’un parcours du dossier. Indépendant des sessions Kronosys ; mis en cache ~5 minutes. Voir le paragraphe descriptif sous le titre.",