@nightkatana/kronosys-app 1.0.0-beta.12 → 1.0.0-beta.13

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.
@@ -8,12 +8,17 @@ import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
8
8
  import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
9
9
  import {
10
10
  buildStartTaskFromDraft,
11
+ formatDuration,
11
12
  formatProjectDisplay,
12
13
  mergeTagsForDisplay,
14
+ normalizeProjectKey,
15
+ normalizeTagKey,
13
16
  parseTaskWithAutoTags,
14
17
  removeProjectFromDraft,
15
18
  removeSavedTagFromDraft,
19
+ taskTitleForDisplay,
16
20
  } from "@/lib/taskParsing";
21
+ import { formatIsoInstantShort } from "@/lib/formatIsoShort";
17
22
  import { TagPills } from "./TagPills";
18
23
  import {
19
24
  PROJECT_CHIP_APPLIED_CLASS,
@@ -88,8 +93,8 @@ function runningTasksFromSession(
88
93
  Array.isArray(session.activeTasks) && session.activeTasks.length > 0
89
94
  ? session.activeTasks
90
95
  : session.activeTask
91
- ? [session.activeTask]
92
- : [];
96
+ ? [session.activeTask]
97
+ : [];
93
98
  return raw.filter((t) => t && !t.isDone && !t.manualTaskTimerPaused);
94
99
  }
95
100
 
@@ -156,9 +161,9 @@ export function TaskFocusPanel({
156
161
  const knownTags = (payload.knownTags || []) as string[];
157
162
  const knownProjects = (payload.knownProjects || []) as string[];
158
163
  const viewingSession = archiveColumnId
159
- ? (history.find((s) => s.sessionId === archiveColumnId) ??
164
+ ? history.find((s) => s.sessionId === archiveColumnId) ??
160
165
  historyArchivedList.find((s) => s.sessionId === archiveColumnId) ??
161
- null)
166
+ null
162
167
  : null;
163
168
  const sessionCurrent = viewingSession ?? live;
164
169
  const isInspecting = !!viewingSession;
@@ -217,8 +222,8 @@ export function TaskFocusPanel({
217
222
  Array.isArray(sess.activeTasks) && sess.activeTasks.length > 0
218
223
  ? [...(sess.activeTasks as TaskRow[])]
219
224
  : sess.activeTask
220
- ? [sess.activeTask as TaskRow]
221
- : [];
225
+ ? [sess.activeTask as TaskRow]
226
+ : [];
222
227
  for (const t of stack) {
223
228
  if (t?.id) {
224
229
  map.set(String(t.id), t);
@@ -227,6 +232,78 @@ export function TaskFocusPanel({
227
232
  return [...map.values()];
228
233
  }, [taskSessionForBuckets]);
229
234
 
235
+ const taskTimelineEntries = useMemo(() => {
236
+ const rows = mergedTasksForBuckets
237
+ .map((task) => {
238
+ const startMs =
239
+ typeof task.startTime === "string"
240
+ ? Date.parse(task.startTime)
241
+ : Number.NaN;
242
+ const endMs =
243
+ typeof task.endTime === "string"
244
+ ? Date.parse(task.endTime)
245
+ : Number.NaN;
246
+ if (!Number.isFinite(startMs) && !Number.isFinite(endMs)) {
247
+ return null;
248
+ }
249
+ const startLabel = Number.isFinite(startMs)
250
+ ? formatIsoInstantShort(
251
+ new Date(startMs).toISOString(),
252
+ lang,
253
+ displayTimeZone,
254
+ use24HourClock,
255
+ )
256
+ : "—";
257
+ const endLabel = Number.isFinite(endMs)
258
+ ? formatIsoInstantShort(
259
+ new Date(endMs).toISOString(),
260
+ lang,
261
+ displayTimeZone,
262
+ use24HourClock,
263
+ )
264
+ : isInspecting
265
+ ? "—"
266
+ : lang === "fr"
267
+ ? "en cours"
268
+ : "running";
269
+ const durationMin =
270
+ typeof task.durationMs === "number" &&
271
+ Number.isFinite(task.durationMs)
272
+ ? task.durationMs / 60000
273
+ : Number.NaN;
274
+ return {
275
+ id: String(task.id),
276
+ name: task.name,
277
+ project: task.project ?? null,
278
+ startSort: Number.isFinite(startMs)
279
+ ? startMs
280
+ : Number.isFinite(endMs)
281
+ ? endMs
282
+ : Number.MAX_SAFE_INTEGER,
283
+ endSort: Number.isFinite(endMs)
284
+ ? endMs
285
+ : Number.isFinite(startMs)
286
+ ? startMs
287
+ : Number.MAX_SAFE_INTEGER,
288
+ startLabel,
289
+ endLabel,
290
+ durationLabel:
291
+ Number.isFinite(durationMin) && durationMin > 0
292
+ ? formatDuration(durationMin)
293
+ : null,
294
+ };
295
+ })
296
+ .filter((entry): entry is NonNullable<typeof entry> => entry !== null);
297
+ rows.sort((a, b) => a.startSort - b.startSort || a.endSort - b.endSort);
298
+ return rows;
299
+ }, [
300
+ mergedTasksForBuckets,
301
+ lang,
302
+ displayTimeZone,
303
+ use24HourClock,
304
+ isInspecting,
305
+ ]);
306
+
230
307
  /**
231
308
  * Bandeau TRACKING : uniquement si la collecte n’est pas en pause session
232
309
  * (sinon le minuteur de suivi n’est pas actif, mais l’interpolation locale faisait défiler l’horloge).
@@ -356,6 +433,39 @@ export function TaskFocusPanel({
356
433
  setStartKronoFocusWithTask(false);
357
434
  }, []);
358
435
 
436
+ const duplicateTaskToDraft = useCallback(
437
+ (task: TaskRow) => {
438
+ const targetSessionId = inspectingId ?? live?.sessionId;
439
+ const title = taskTitleForDisplay(task.name).trim();
440
+ const tags = (task.tags ?? []).map((tag) => `#${normalizeTagKey(tag)}`);
441
+ const project =
442
+ typeof task.project === "string" && task.project.trim()
443
+ ? `@${normalizeProjectKey(task.project)}`
444
+ : "";
445
+ const nextInput = [title, ...tags, project]
446
+ .filter(Boolean)
447
+ .join(" ")
448
+ .trim();
449
+ setTaskInput(nextInput);
450
+ if (
451
+ typeof task.startTime === "string" &&
452
+ task.startTime.trim() &&
453
+ typeof task.endTime === "string" &&
454
+ task.endTime.trim() &&
455
+ targetSessionId
456
+ ) {
457
+ const start = new Date(task.startTime);
458
+ const end = new Date(task.endTime);
459
+ if (!Number.isNaN(start.getTime()) && !Number.isNaN(end.getTime())) {
460
+ setTaskEntryMode("past");
461
+ setPastStartLocal(formatDatetimeLocalValue(start));
462
+ setPastEndLocal(formatDatetimeLocalValue(end));
463
+ }
464
+ }
465
+ },
466
+ [inspectingId, live?.sessionId],
467
+ );
468
+
359
469
  const resolveConcurrentAndStartLive = useCallback(
360
470
  async (mode: ConcurrentTaskStartPreference, persistPreference: boolean) => {
361
471
  const draft = pendingLiveStartRef.current;
@@ -397,8 +507,8 @@ export function TaskFocusPanel({
397
507
  typeof error === "string"
398
508
  ? error
399
509
  : error instanceof Error
400
- ? error.message
401
- : "Failed to resolve concurrent tasks",
510
+ ? error.message
511
+ : "Failed to resolve concurrent tasks",
402
512
  );
403
513
  pendingLiveStartRef.current = null;
404
514
  setConcurrentStartModalOpen(false);
@@ -1061,6 +1171,7 @@ export function TaskFocusPanel({
1061
1171
  startKronoFocusFromTask={() =>
1062
1172
  void startKronoFocusFromTask()
1063
1173
  }
1174
+ onDuplicateTask={duplicateTaskToDraft}
1064
1175
  pausePlayMode="pause"
1065
1176
  onPausePlay={() =>
1066
1177
  void post({
@@ -1120,6 +1231,7 @@ export function TaskFocusPanel({
1120
1231
  startKronoFocusFromTask={() =>
1121
1232
  void startKronoFocusFromTask()
1122
1233
  }
1234
+ onDuplicateTask={duplicateTaskToDraft}
1123
1235
  pausePlayMode="resume"
1124
1236
  onPausePlay={() =>
1125
1237
  void post({ type: "resumePausedTask", taskId: task.id })
@@ -1163,6 +1275,7 @@ export function TaskFocusPanel({
1163
1275
  startKronoFocusFromTask={() =>
1164
1276
  void startKronoFocusFromTask()
1165
1277
  }
1278
+ onDuplicateTask={duplicateTaskToDraft}
1166
1279
  pausePlayMode={null}
1167
1280
  onPausePlay={() => {}}
1168
1281
  anchorId={`kronosys-task-${task.id}`}
@@ -1176,6 +1289,56 @@ export function TaskFocusPanel({
1176
1289
  </div>
1177
1290
  ) : null}
1178
1291
 
1292
+ {isInspecting || hasLiveSession ? (
1293
+ <section className="mt-10 space-y-2">
1294
+ <div className="flex items-center justify-between gap-2">
1295
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
1296
+ {t.tasksTimelineHeading}
1297
+ </h4>
1298
+ {taskTimelineEntries.length > 0 ? (
1299
+ <span className="rounded-full border border-zinc-600/80 px-2 py-0.5 text-[0.65rem] text-zinc-500">
1300
+ {taskTimelineEntries.length}
1301
+ </span>
1302
+ ) : null}
1303
+ </div>
1304
+ {taskTimelineEntries.length === 0 ? (
1305
+ <p className="text-sm text-zinc-500">{t.tasksTimelineEmpty}</p>
1306
+ ) : (
1307
+ <div className="space-y-2">
1308
+ {taskTimelineEntries.map((entry) => (
1309
+ <article
1310
+ key={`timeline-${entry.id}`}
1311
+ className="rounded-lg border border-zinc-200/80 bg-zinc-100/70 px-3 py-2 dark:border-zinc-700/80 dark:bg-zinc-900/40"
1312
+ >
1313
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-zinc-500 dark:text-zinc-400">
1314
+ <span className="font-mono tabular-nums">
1315
+ {entry.startLabel}
1316
+ </span>
1317
+ <span>→</span>
1318
+ <span className="font-mono tabular-nums">
1319
+ {entry.endLabel}
1320
+ </span>
1321
+ {entry.durationLabel ? (
1322
+ <span className="rounded-full border border-zinc-500/40 px-1.5 py-0.5 text-[0.65rem]">
1323
+ {entry.durationLabel}
1324
+ </span>
1325
+ ) : null}
1326
+ </div>
1327
+ <p className="mt-1 text-sm text-zinc-900 dark:text-zinc-100">
1328
+ {taskTitleForDisplay(entry.name)}
1329
+ </p>
1330
+ {typeof entry.project === "string" && entry.project.trim() ? (
1331
+ <p className="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
1332
+ {formatProjectDisplay(entry.project)}
1333
+ </p>
1334
+ ) : null}
1335
+ </article>
1336
+ ))}
1337
+ </div>
1338
+ )}
1339
+ </section>
1340
+ ) : null}
1341
+
1179
1342
  {issuePickerOpen ? (
1180
1343
  <IssuePickerModal
1181
1344
  t={t}
@@ -8,6 +8,7 @@ import {
8
8
  CheckCircle2,
9
9
  Trash2,
10
10
  GitCommit,
11
+ Copy,
11
12
  } from "lucide-react";
12
13
  import type { DashboardStrings, Lang } from "@/lib/dashboardCopy";
13
14
  import { formatIsoInstantShort } from "@/lib/formatIsoShort";
@@ -181,6 +182,7 @@ export function TaskSessionLiveCard({
181
182
  kronoFocusIsRunningOrPaused,
182
183
  showKronoFocusTaskActions = true,
183
184
  startKronoFocusFromTask,
185
+ onDuplicateTask,
184
186
  pausePlayMode,
185
187
  onPausePlay,
186
188
  anchorId,
@@ -203,6 +205,7 @@ export function TaskSessionLiveCard({
203
205
  /** Bouton « démarrer le KronoFocus » sur les tâches non terminées (paramètre tableau de bord). */
204
206
  showKronoFocusTaskActions?: boolean;
205
207
  startKronoFocusFromTask: () => void;
208
+ onDuplicateTask: (task: TaskSessionLiveCardTask) => void;
206
209
  /** `null` : tâche terminée (pas de pause / reprise). */
207
210
  pausePlayMode: "pause" | "resume" | null;
208
211
  onPausePlay: () => void;
@@ -283,12 +286,12 @@ export function TaskSessionLiveCard({
283
286
  const showDoneTimingRow =
284
287
  isDone && (startFmt !== null || endFmt !== null || durationLabel !== null);
285
288
  const canEditTaskStartTime =
286
- allowTaskStartTimeEdit &&
289
+ (isDone ? true : allowTaskStartTimeEdit) &&
287
290
  typeof task.startTime === "string" &&
288
291
  task.startTime.trim() !== "";
289
292
  const canEditTaskEndTime =
290
- allowTaskEndTimeEdit &&
291
293
  isDone &&
294
+ (isDone ? true : allowTaskEndTimeEdit) &&
292
295
  typeof task.startTime === "string" &&
293
296
  task.startTime.trim() !== "" &&
294
297
  typeof task.endTime === "string" &&
@@ -621,15 +624,26 @@ export function TaskSessionLiveCard({
621
624
  </span>
622
625
  ) : null}
623
626
  {isInspecting ? (
624
- <button
625
- type="button"
626
- className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 text-zinc-600 hover:border-red-600/60 hover:bg-red-50 hover:text-red-800 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-red-800/70 dark:hover:bg-red-950/45 dark:hover:text-red-300"
627
- title={t.taskDeleteBtn}
628
- aria-label={t.taskDeleteBtn}
629
- onClick={() => confirmDeleteTask(task.id)}
630
- >
631
- <Trash2 size={20} />
632
- </button>
627
+ <div className="flex h-10 shrink-0 items-center gap-1">
628
+ <button
629
+ type="button"
630
+ className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
631
+ title={t.taskDuplicateBtn}
632
+ aria-label={t.taskDuplicateBtn}
633
+ onClick={() => onDuplicateTask(task)}
634
+ >
635
+ <Copy size={18} />
636
+ </button>
637
+ <button
638
+ type="button"
639
+ className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-zinc-300 text-zinc-600 hover:border-red-600/60 hover:bg-red-50 hover:text-red-800 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-red-800/70 dark:hover:bg-red-950/45 dark:hover:text-red-300"
640
+ title={t.taskDeleteBtn}
641
+ aria-label={t.taskDeleteBtn}
642
+ onClick={() => confirmDeleteTask(task.id)}
643
+ >
644
+ <Trash2 size={20} />
645
+ </button>
646
+ </div>
633
647
  ) : (
634
648
  <div className="flex h-10 shrink-0 items-center gap-1">
635
649
  {!isDone && showKronoFocusTaskActions ? (
@@ -706,6 +720,15 @@ export function TaskSessionLiveCard({
706
720
  >
707
721
  <CheckCircle2 size={20} />
708
722
  </button>
723
+ <button
724
+ type="button"
725
+ className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-300 bg-white text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
726
+ title={t.taskDuplicateBtn}
727
+ aria-label={t.taskDuplicateBtn}
728
+ onClick={() => onDuplicateTask(task)}
729
+ >
730
+ <Copy size={18} />
731
+ </button>
709
732
  <button
710
733
  type="button"
711
734
  className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-300 text-zinc-600 hover:border-red-600/60 hover:bg-red-50 hover:text-red-800 dark:border-zinc-600 dark:text-zinc-400 dark:hover:border-red-800/70 dark:hover:bg-red-950/45 dark:hover:text-red-300"
@@ -115,6 +115,8 @@ export type DashboardStrings = {
115
115
  finishTaskOpenSubtasksWarnTitle: string;
116
116
  /** Corps du modal : les sous-tâches ouvertes seront toutes marquées terminées. */
117
117
  finishTaskOpenSubtasksWarnBody: string;
118
+ /** Dupliquer une tâche vers le formulaire pour retouche avant enregistrement. */
119
+ taskDuplicateBtn: string;
118
120
  taskDeleteBtn: string;
119
121
  /** Texte complet du modal avant suppression d’une tâche. */
120
122
  taskDeleteConfirm: string;
@@ -141,6 +143,10 @@ export type DashboardStrings = {
141
143
  tasksCompletedHeading: string;
142
144
  /** Sous-titre : tâches au minuteur (sous le formulaire d’ajout). */
143
145
  tasksRunningHeading: string;
146
+ /** Titre de la chronologie des tâches de la session affichée. */
147
+ tasksTimelineHeading: string;
148
+ /** Message quand aucune tâche de session n'a d'horodatage exploitable. */
149
+ tasksTimelineEmpty: string;
144
150
  hideTaskList: string;
145
151
  showAllTasks: string;
146
152
  importGitIssue: string;
@@ -766,6 +772,7 @@ const en: DashboardStrings = {
766
772
  finishTaskOpenSubtasksWarnTitle: "Finish task?",
767
773
  finishTaskOpenSubtasksWarnBody:
768
774
  "This task still has open detail items. They will all be marked done when you finish the task.",
775
+ taskDuplicateBtn: "Duplicate to draft",
769
776
  taskDeleteBtn: "Delete",
770
777
  taskDeleteConfirm:
771
778
  "Delete this entry? Recorded time will be lost. This cannot be undone.",
@@ -786,6 +793,8 @@ const en: DashboardStrings = {
786
793
  tasksPausedHeading: "Paused",
787
794
  tasksCompletedHeading: "Completed",
788
795
  tasksRunningHeading: "Running now",
796
+ tasksTimelineHeading: "Task timeline",
797
+ tasksTimelineEmpty: "No dated task entries are available for this session yet.",
789
798
  hideTaskList: "Hide list",
790
799
  showAllTasks: "Show full list",
791
800
  importGitIssue: "Import issue from Git",
@@ -1328,6 +1337,7 @@ const fr: DashboardStrings = {
1328
1337
  finishTaskOpenSubtasksWarnTitle: "Terminer la tâche ?",
1329
1338
  finishTaskOpenSubtasksWarnBody:
1330
1339
  "Il reste des points de détail non cochés. Ils seront tous marqués comme terminés lorsque vous terminez la tâche.",
1340
+ taskDuplicateBtn: "Dupliquer vers le brouillon",
1331
1341
  taskDeleteBtn: "Supprimer",
1332
1342
  taskDeleteConfirm:
1333
1343
  "Supprimer cette entrée ? Le temps enregistré sera perdu. Cette action est irréversible.",
@@ -1348,6 +1358,9 @@ const fr: DashboardStrings = {
1348
1358
  tasksPausedHeading: "En pause",
1349
1359
  tasksCompletedHeading: "Terminées",
1350
1360
  tasksRunningHeading: "En cours (minuteur)",
1361
+ tasksTimelineHeading: "Timeline des tâches",
1362
+ tasksTimelineEmpty:
1363
+ "Aucune entrée datée de tâche n’est encore disponible pour cette session.",
1351
1364
  hideTaskList: "Masquer la liste",
1352
1365
  showAllTasks: "Afficher toute la liste",
1353
1366
  importGitIssue: "Importer une issue Git",
@@ -6,21 +6,76 @@ export type UserChangelogEntry = {
6
6
 
7
7
  export const USER_CHANGELOG_ENTRIES: UserChangelogEntry[] = [
8
8
  {
9
- "version": "3.0.0",
9
+ "version": "1.0.0-beta.12",
10
10
  "items": [
11
- "aefaf9e: Première version officielle publiée."
11
+ "Correctif du runtime CLI: installation des dépendances avec `--ignore-scripts` pour éviter les erreurs Husky hors dépôt."
12
12
  ]
13
13
  },
14
14
  {
15
- "version": "2.0.0",
15
+ "version": "1.0.0-beta.11",
16
16
  "items": [
17
- "aefaf9e: Première version officielle publiée."
17
+ "Correctif Windows du lanceur CLI (PowerShell + installation runtime locale).",
18
+ "Rebuild explicite de `better-sqlite3` dans le runtime local."
18
19
  ]
19
20
  },
20
21
  {
21
- "version": "1.0.0",
22
+ "version": "1.0.0-beta.10",
22
23
  "items": [
23
- "aefaf9e: Première version officielle publiée."
24
+ "Correctif de dispatch des commandes Windows pour le runtime CLI."
25
+ ]
26
+ },
27
+ {
28
+ "version": "1.0.0-beta.9",
29
+ "items": [
30
+ "Correctif de quoting des commandes Windows dans le lanceur CLI."
31
+ ]
32
+ },
33
+ {
34
+ "version": "1.0.0-beta.8",
35
+ "items": [
36
+ "Correctif initial Windows du launcher (`npm.cmd`/`npx.cmd`)."
37
+ ]
38
+ },
39
+ {
40
+ "version": "1.0.0-beta.7",
41
+ "items": [
42
+ "Stabilisation du packaging bêta et génération des archives de distribution."
43
+ ]
44
+ },
45
+ {
46
+ "version": "1.0.0-beta.6",
47
+ "items": [
48
+ "Correctif de résolution d’alias d’import (`@/...`) pour le build de production."
49
+ ]
50
+ },
51
+ {
52
+ "version": "1.0.0-beta.5",
53
+ "items": [
54
+ "Retour temporaire au mode build à la première exécution pour améliorer la compatibilité multiplateforme."
55
+ ]
56
+ },
57
+ {
58
+ "version": "1.0.0-beta.4",
59
+ "items": [
60
+ "Ajustements de packaging et de lancement CLI pour la série bêta."
61
+ ]
62
+ },
63
+ {
64
+ "version": "1.0.0-beta.2",
65
+ "items": [
66
+ "Correctif des dépendances de build nécessaires au runtime CLI."
67
+ ]
68
+ },
69
+ {
70
+ "version": "1.0.0-beta.1",
71
+ "items": [
72
+ "`kronosys` lance maintenant le build de production automatiquement si `.next` est absent."
73
+ ]
74
+ },
75
+ {
76
+ "version": "1.0.0-beta.0",
77
+ "items": [
78
+ "Première version bêta publiée."
24
79
  ]
25
80
  },
26
81
  {
@@ -134,6 +134,9 @@ function frBundle(): UserGuideBundle {
134
134
  "**Terminer la session** quand le bloc a une fin (bouton d’en-tête ou bannière) ; une **raison** de clôture **facultative** (dans les temps, en avance, dépassement, autre + note) : pour poser mots sur la **fin** du cycle — d’**abord** pour vous, pas pour un **audit** froid."
135
135
  ],
136
136
  bullets: [
137
+ "Les tâches **terminées** restent **éditables** (titre, #étiquettes, @projet, heures de début/fin) pour corriger une entrée après coup sans bricolage.",
138
+ "Le bouton **Dupliquer vers le brouillon** copie une tâche dans le formulaire, vous laissez retoucher puis enregistrer la nouvelle entrée quand vous êtes prêt.",
139
+ "Quand vous consultez une session (active ou terminée), une **timeline des tâches** affiche l’enchaînement des entrées datées (début → fin + durée).",
137
140
  "L’**option** **Commit** à la fin d’une tâche, quand elle apparaît, concerne **Git** (voir [Synchronisation Git](/settings#settings-git) côté paramètres) : elle n’est **là** que **si** l’intégration est en place — les **équipiers** **non** devs peuvent l’**ignorer** sereinement."
138
141
  ],
139
142
  },
@@ -332,6 +335,9 @@ function enBundle(): UserGuideBundle {
332
335
  "**End session** from the header or banner: optional end reason (on time, early, overrun, other + note) — a word for **how** the block **ended**, not a cold verdict.",
333
336
  ],
334
337
  bullets: [
338
+ "**Completed** tasks stay **editable** (title, #tags, @project, start/end times) so you can correct entries after the fact without hacks.",
339
+ "Use **Duplicate to draft** to copy a task into the form, tweak it, then save the new entry only when it looks right.",
340
+ "When viewing a session (live or finished), a **task timeline** shows dated entries in order (start → end + duration).",
335
341
  "When **Commit** on **finish** appears, it is about **Git** (see [Git sync](/settings#settings-git) in **Settings**) — you can **ignore** it with a clear head if you are not in that workflow.",
336
342
  ],
337
343
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nightkatana/kronosys-app",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.13",
4
4
  "description": "Kronosys — application Next.js (UI + API + SQLite).",
5
5
  "license": "MIT",
6
6
  "author": "nightkatana",