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

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 (112) hide show
  1. package/README.md +28 -1
  2. package/app/api/action/route.ts +39 -3
  3. package/app/api/action-logs/route.ts +24 -0
  4. package/app/api/backup/route.ts +1 -1
  5. package/app/api/restore/route.ts +145 -0
  6. package/app/changelog/page.tsx +71 -4
  7. package/app/globals.css +127 -0
  8. package/app/guide/page.tsx +61 -15
  9. package/app/implementation/page.tsx +700 -0
  10. package/app/layout.tsx +14 -3
  11. package/app/licenses/page.tsx +99 -37
  12. package/app/logs/page.tsx +258 -0
  13. package/app/manifest.ts +5 -5
  14. package/app/page.tsx +784 -229
  15. package/app/reporting/page.tsx +1266 -474
  16. package/app/settings/page.tsx +252 -18
  17. package/bin/kronosys.mjs +140 -15
  18. package/components/KronosysPayloadProvider.tsx +2 -0
  19. package/components/RouteTransition.tsx +18 -0
  20. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +17 -0
  21. package/components/dashboard/AppShellHeaderSessionMeta.tsx +210 -0
  22. package/components/dashboard/AppShellHeaderWallClock.tsx +54 -0
  23. package/components/dashboard/AppShellLiveSessionDrawer.tsx +154 -38
  24. package/components/dashboard/AppShellRouteNav.tsx +323 -48
  25. package/components/dashboard/DashboardPauseBackdrop.tsx +50 -0
  26. package/components/dashboard/DashboardSimpleModal.tsx +168 -25
  27. package/components/dashboard/DashboardTour.tsx +115 -29
  28. package/components/dashboard/GlobalPauseConfirmModal.tsx +183 -0
  29. package/components/dashboard/KronosysDatetimePopoverField.tsx +167 -122
  30. package/components/dashboard/KronosysTimePopoverField.tsx +54 -12
  31. package/components/dashboard/NewSessionScopeModal.tsx +211 -20
  32. package/components/dashboard/PlannedTaskBoundaryConflictWatcher.tsx +275 -0
  33. package/components/dashboard/ReportingTour.tsx +87 -21
  34. package/components/dashboard/SavedProjectPicker.tsx +16 -3
  35. package/components/dashboard/SelectedSessionSidebarBlock.tsx +512 -142
  36. package/components/dashboard/SessionListPanel.tsx +327 -44
  37. package/components/dashboard/SettingsTagsProjectsSection.tsx +1073 -264
  38. package/components/dashboard/SettingsTaskTemplatesSection.tsx +316 -0
  39. package/components/dashboard/SettingsTour.tsx +86 -21
  40. package/components/dashboard/TagPills.tsx +14 -1
  41. package/components/dashboard/TaskFocusPanel.tsx +1081 -478
  42. package/components/dashboard/TaskSessionLiveCard.tsx +650 -135
  43. package/components/dashboard/TaskTimelineGanttModal.tsx +601 -0
  44. package/components/dashboard/taskFieldStyles.ts +20 -4
  45. package/components/dashboard/useReportingInteractionState.ts +80 -0
  46. package/lib/appShellHeaderClasses.ts +13 -0
  47. package/lib/businessRulesMatrix.ts +210 -0
  48. package/lib/copyToClipboard.ts +43 -0
  49. package/lib/dashboardCopy.ts +494 -84
  50. package/lib/dashboardQuickSearch.ts +54 -2
  51. package/lib/dashboardTimeZone.ts +109 -0
  52. package/lib/formatAppShellWallClock.ts +66 -0
  53. package/lib/formatSessionNameTemplate.ts +141 -0
  54. package/lib/generatedUserChangelog.ts +177 -6
  55. package/lib/globalPausePreview.ts +292 -0
  56. package/lib/implementationNotes.ts +1188 -0
  57. package/lib/kronosysApi.ts +6 -0
  58. package/lib/kronosysDashboardModalGates.ts +24 -0
  59. package/lib/plannedBoundaryAttention.ts +9 -0
  60. package/lib/plannedBoundaryConflict.ts +23 -0
  61. package/lib/reportingAggregate.ts +517 -75
  62. package/lib/reportingMetricHelp.ts +8 -0
  63. package/lib/reportingStrings.ts +37 -3
  64. package/lib/sessionListMerge.ts +4 -0
  65. package/lib/sessionTaskSidebarStats.ts +182 -21
  66. package/lib/settingsCopy.ts +178 -4
  67. package/lib/taskParsing.ts +360 -103
  68. package/lib/taskTemplateDraft.ts +135 -0
  69. package/lib/taskTimelineGantt.ts +265 -0
  70. package/lib/temporalDisplayPlanned.ts +71 -0
  71. package/lib/userGuideCopy.ts +121 -47
  72. package/next.config.ts +7 -0
  73. package/package.json +12 -24
  74. package/server/actionDispatch.ts +1000 -77
  75. package/server/actionTaskSession.ts +337 -24
  76. package/server/db.ts +7 -15
  77. package/server/dbSchema.ts +24 -0
  78. package/server/defaultCfg.ts +5 -0
  79. package/server/gitlabTokenStore.ts +0 -12
  80. package/server/liveHistorySync.ts +53 -0
  81. package/server/mainTimerHydrate.ts +38 -2
  82. package/server/payloadStore.ts +33 -11
  83. package/server/sessionWallHydrate.ts +66 -3
  84. package/server/userActionLog.ts +126 -0
  85. package/sonar-project.properties +11 -0
  86. package/tsconfig.json +2 -1
  87. package/components/dashboard/IssuePickerModal.tsx +0 -168
  88. package/components/dashboard/ThemeToggle.test.tsx +0 -26
  89. package/lib/backupCsvExport.test.ts +0 -149
  90. package/lib/dashboardQuickSearchQuery.test.ts +0 -63
  91. package/lib/dataDir.test.ts +0 -87
  92. package/lib/formatIsoShort.test.ts +0 -46
  93. package/lib/kronoFocusRhythm.test.ts +0 -130
  94. package/lib/kronoFocusTimerUrgency.test.ts +0 -74
  95. package/lib/legacyKronoFocusStorageKeys.test.ts +0 -29
  96. package/lib/reportingAggregate.test.ts +0 -325
  97. package/lib/reportingNonFinalIndicators.test.ts +0 -157
  98. package/lib/reportingTagWeekBreakdown.test.ts +0 -141
  99. package/lib/reportingWeekLayout.test.ts +0 -239
  100. package/lib/sessionAssiduity.test.ts +0 -25
  101. package/lib/sessionEndWarnings.test.ts +0 -200
  102. package/lib/sessionListMerge.test.ts +0 -101
  103. package/lib/sessionTaskSidebarStats.test.ts +0 -24
  104. package/lib/taskParsing.test.ts +0 -153
  105. package/lib/usageProfile.test.ts +0 -84
  106. package/server/actionDispatch.test.ts +0 -723
  107. package/server/actionTaskSession.test.ts +0 -713
  108. package/server/kronoFocusHydrate.test.ts +0 -142
  109. package/server/kronoFocusMigrate.test.ts +0 -53
  110. package/server/mainTimerHydrate.test.ts +0 -65
  111. package/server/payloadStore.test.ts +0 -78
  112. package/server/sessionWallHydrate.test.ts +0 -46
@@ -1,142 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
4
-
5
- import { hydrateKronoFocusInPayload } from "./kronoFocusHydrate";
6
-
7
- function basePayload(pm: Record<string, unknown>): KronosysUpdatePayload {
8
- return {
9
- viewType: "dashboard",
10
- current: {
11
- sessionId: "s1",
12
- kronoFocus: pm,
13
- },
14
- cfg: {},
15
- } as KronosysUpdatePayload;
16
- }
17
-
18
- describe("hydrateKronoFocusInPayload", () => {
19
- it("ancre une échéance pour un KronoFocus running sans deadline (données héritées)", () => {
20
- const t0 = 1_000_000_000;
21
- const p = basePayload({
22
- mode: "work",
23
- status: "running",
24
- timeLeftSeconds: 100,
25
- });
26
- const dirty = hydrateKronoFocusInPayload(p, t0);
27
- expect(dirty).toBe(true);
28
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
29
- expect(pm.kronoFocusDeadlineAtMs).toBe(t0 + 100_000);
30
- expect(pm.timeLeftSeconds).toBe(100);
31
- });
32
-
33
- it("décompte les secondes restantes avant l’échéance", () => {
34
- const t0 = 1_000_000_000;
35
- const p = basePayload({
36
- mode: "work",
37
- status: "running",
38
- timeLeftSeconds: 9999,
39
- kronoFocusDeadlineAtMs: t0 + 60_000,
40
- });
41
- const dirty = hydrateKronoFocusInPayload(p, t0 + 30_000);
42
- expect(dirty).toBe(false);
43
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
44
- expect(pm.timeLeftSeconds).toBe(30);
45
- expect(pm.status).toBe("running");
46
- });
47
-
48
- it("à l’échéance d’un bloc travail : utilise shortBreakDurationSeconds du payload", () => {
49
- const t0 = 1_000_000_000;
50
- const p = basePayload({
51
- mode: "work",
52
- status: "running",
53
- timeLeftSeconds: 10,
54
- sessionsCompleted: 0,
55
- shortBreakDurationSeconds: 10 * 60,
56
- kronoFocusDeadlineAtMs: t0 + 10_000,
57
- });
58
- const dirty = hydrateKronoFocusInPayload(p, t0 + 10_000);
59
- expect(dirty).toBe(true);
60
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
61
- expect(pm.status).toBe("paused");
62
- expect(pm.mode).toBe("break");
63
- expect(pm.timeLeftSeconds).toBe(10 * 60);
64
- expect(pm.sessionsCompleted).toBe(1);
65
- expect(pm.kronoFocusDeadlineAtMs).toBeUndefined();
66
- });
67
-
68
- it("à l’échéance d’un bloc travail : pause, pause courte, incrémente sessionsCompleted", () => {
69
- const t0 = 1_000_000_000;
70
- const p = basePayload({
71
- mode: "work",
72
- status: "running",
73
- timeLeftSeconds: 10,
74
- sessionsCompleted: 0,
75
- kronoFocusDeadlineAtMs: t0 + 10_000,
76
- });
77
- const dirty = hydrateKronoFocusInPayload(p, t0 + 10_000);
78
- expect(dirty).toBe(true);
79
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
80
- expect(pm.status).toBe("paused");
81
- expect(pm.mode).toBe("break");
82
- expect(pm.timeLeftSeconds).toBe(5 * 60);
83
- expect(pm.sessionsCompleted).toBe(1);
84
- expect(pm.kronoFocusDeadlineAtMs).toBeUndefined();
85
- });
86
-
87
- it("après 4 cycles travail : pause longue 15 min", () => {
88
- const t0 = 1_000_000_000;
89
- const p = basePayload({
90
- mode: "work",
91
- status: "running",
92
- timeLeftSeconds: 1,
93
- sessionsCompleted: 3,
94
- kronoFocusDeadlineAtMs: t0 + 1000,
95
- });
96
- hydrateKronoFocusInPayload(p, t0 + 1000);
97
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
98
- expect(pm.sessionsCompleted).toBe(4);
99
- expect(pm.mode).toBe("longBreak");
100
- expect(pm.timeLeftSeconds).toBe(15 * 60);
101
- });
102
-
103
- it("après 4 cycles travail : utilise longBreakDurationSeconds du payload", () => {
104
- const t0 = 1_000_000_000;
105
- const p = basePayload({
106
- mode: "work",
107
- status: "running",
108
- timeLeftSeconds: 1,
109
- sessionsCompleted: 3,
110
- longBreakDurationSeconds: 22 * 60,
111
- kronoFocusDeadlineAtMs: t0 + 1000,
112
- });
113
- hydrateKronoFocusInPayload(p, t0 + 1000);
114
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
115
- expect(pm.sessionsCompleted).toBe(4);
116
- expect(pm.mode).toBe("longBreak");
117
- expect(pm.timeLeftSeconds).toBe(22 * 60);
118
- });
119
-
120
- it("fin d’une pause : retour au travail avec durée par défaut", () => {
121
- const t0 = 1_000_000_000;
122
- const p = basePayload({
123
- mode: "break",
124
- status: "running",
125
- timeLeftSeconds: 1,
126
- sessionsCompleted: 1,
127
- kronoFocusDeadlineAtMs: t0 + 1000,
128
- });
129
- hydrateKronoFocusInPayload(p, t0 + 1000);
130
- const pm = (p.current as Record<string, unknown>).kronoFocus as Record<string, unknown>;
131
- expect(pm.mode).toBe("work");
132
- expect(pm.status).toBe("paused");
133
- expect(pm.timeLeftSeconds).toBe(25 * 60);
134
- });
135
-
136
- it("n’agit pas si pas de session courante ou KronoFocus absent", () => {
137
- const p: KronosysUpdatePayload = { viewType: "dashboard", cfg: {} } as KronosysUpdatePayload;
138
- expect(hydrateKronoFocusInPayload(p, 0)).toBe(false);
139
- const p2 = basePayload({ mode: "work", status: "idle", timeLeftSeconds: 100 });
140
- expect(hydrateKronoFocusInPayload(p2, 0)).toBe(false);
141
- });
142
- });
@@ -1,53 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
4
- import {
5
- LEGACY_SESSION_TIMER_OBJECT_KEY,
6
- LEGACY_TASK_CYCLES_KEY,
7
- LEGACY_TIMER_DEADLINE_MS_KEY,
8
- } from "@/lib/legacyKronoFocusStorageKeys";
9
-
10
- import { migrateLegacyKronoFocusPayload } from "./kronoFocusMigrate";
11
-
12
- describe("migrateLegacyKronoFocusPayload", () => {
13
- it("fusionne l’objet minuteur hérité sur la session vers kronoFocus et renomme l’échéance", () => {
14
- const legacyBlock: Record<string, unknown> = {
15
- mode: "work",
16
- status: "paused",
17
- timeLeftSeconds: 120,
18
- sessionsCompleted: 1,
19
- };
20
- legacyBlock[LEGACY_TIMER_DEADLINE_MS_KEY] = 999;
21
-
22
- const p = {
23
- viewType: "dashboard",
24
- current: {
25
- sessionId: "s1",
26
- [LEGACY_SESSION_TIMER_OBJECT_KEY]: legacyBlock,
27
- },
28
- } as unknown as KronosysUpdatePayload;
29
-
30
- expect(migrateLegacyKronoFocusPayload(p)).toBe(true);
31
- const cur = p.current as Record<string, unknown>;
32
- expect(cur[LEGACY_SESSION_TIMER_OBJECT_KEY]).toBeUndefined();
33
- const kf = cur.kronoFocus as Record<string, unknown>;
34
- expect(kf.status).toBe("paused");
35
- expect(kf.kronoFocusDeadlineAtMs).toBe(999);
36
- expect(kf[LEGACY_TIMER_DEADLINE_MS_KEY]).toBeUndefined();
37
- });
38
-
39
- it("renomme les cycles hérités sur les tâches en kronoFocusCycles", () => {
40
- const p = {
41
- viewType: "dashboard",
42
- current: {
43
- sessionId: "s1",
44
- tasks: [{ id: "t1", name: "a", [LEGACY_TASK_CYCLES_KEY]: 3 }],
45
- },
46
- } as unknown as KronosysUpdatePayload;
47
-
48
- expect(migrateLegacyKronoFocusPayload(p)).toBe(true);
49
- const tasks = (p.current as Record<string, unknown>).tasks as Record<string, unknown>[];
50
- expect(tasks[0].kronoFocusCycles).toBe(3);
51
- expect(tasks[0][LEGACY_TASK_CYCLES_KEY]).toBeUndefined();
52
- });
53
- });
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
3
- import { MAIN_TIMER_SEGMENT_STARTED_AT } from "./actionTaskSession";
4
- import { materializeRunningMainTimersInPayload } from "./mainTimerHydrate";
5
-
6
- describe("materializeRunningMainTimersInPayload", () => {
7
- it("avance durationMs pour une tâche au minuteur principal entre deux appels", () => {
8
- const t0 = Date.UTC(2026, 0, 10, 12, 0, 0);
9
- const task = {
10
- id: "t1",
11
- name: "Tâche",
12
- durationMs: 1000,
13
- isDone: false,
14
- manualTaskTimerPaused: false,
15
- subtasks: [],
16
- [MAIN_TIMER_SEGMENT_STARTED_AT]: new Date(t0).toISOString(),
17
- };
18
- const p = {
19
- viewType: "dashboard",
20
- cfg: {},
21
- current: {
22
- sessionId: "sid",
23
- tasks: [],
24
- activeTasks: [task],
25
- activeTask: task,
26
- },
27
- } as unknown as KronosysUpdatePayload;
28
-
29
- const t1 = t0 + 5000;
30
- const w1 = materializeRunningMainTimersInPayload(p, t1);
31
- expect(w1).toBe(true);
32
- expect(task.durationMs).toBe(6000);
33
- expect(typeof task[MAIN_TIMER_SEGMENT_STARTED_AT]).toBe("string");
34
-
35
- const t2 = t1 + 3000;
36
- const w2 = materializeRunningMainTimersInPayload(p, t2);
37
- expect(w2).toBe(true);
38
- expect(task.durationMs).toBe(9000);
39
- });
40
-
41
- it("ignore les tâches en pause manuelle", () => {
42
- const task = {
43
- id: "t1",
44
- name: "Tâche",
45
- durationMs: 1000,
46
- isDone: false,
47
- manualTaskTimerPaused: true,
48
- subtasks: [],
49
- };
50
- const p = {
51
- viewType: "dashboard",
52
- cfg: {},
53
- current: {
54
- sessionId: "sid",
55
- tasks: [],
56
- activeTasks: [task],
57
- activeTask: task,
58
- },
59
- } as unknown as KronosysUpdatePayload;
60
-
61
- const w = materializeRunningMainTimersInPayload(p, Date.UTC(2026, 0, 10, 12, 0, 10));
62
- expect(w).toBe(false);
63
- expect(task.durationMs).toBe(1000);
64
- });
65
- });
@@ -1,78 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as os from "node:os";
3
- import * as path from "node:path";
4
-
5
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
-
7
- import { resetSqliteConnection } from "@/server/db";
8
- import { readPayload, writePayload } from "@/server/payloadStore";
9
-
10
- describe("payloadStore (intégration SQLite)", () => {
11
- let tmpDir: string;
12
-
13
- beforeEach(() => {
14
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kronosys-payload-"));
15
- process.env.TRACE_DATA_DIR = tmpDir;
16
- resetSqliteConnection();
17
- });
18
-
19
- afterEach(() => {
20
- resetSqliteConnection();
21
- delete process.env.TRACE_DATA_DIR;
22
- fs.rmSync(tmpDir, { recursive: true, force: true });
23
- });
24
-
25
- it("readPayload retourne un payload initial valide sur base vierge", () => {
26
- const p = readPayload();
27
- expect(p.viewType).toBe("dashboard");
28
- expect(Array.isArray(p.history)).toBe(true);
29
- expect(p.cfg).toBeDefined();
30
- expect(p.dashboardDataOrigin).toBe("local_next");
31
- });
32
-
33
- it("writePayload puis readPayload retourne les mêmes données", () => {
34
- const initial = readPayload();
35
- const modified = { ...initial, knownTags: ["dev", "design"] };
36
- writePayload(modified);
37
- const back = readPayload();
38
- expect(back.knownTags).toEqual(["dev", "design"]);
39
- });
40
-
41
- it("writePayload est idempotent (INSERT OR REPLACE)", () => {
42
- const p = readPayload();
43
- writePayload(p);
44
- writePayload(p);
45
- const back = readPayload();
46
- expect(back.viewType).toBe("dashboard");
47
- });
48
-
49
- it("readPayload retourne un payload initial si la valeur stockée est du JSON invalide", async () => {
50
- // Écrire manuellement du JSON corrompu dans la DB
51
- const { getSqlite } = await import("@/server/db");
52
- const db = getSqlite();
53
- db.exec(`CREATE TABLE IF NOT EXISTS kv_store (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL);`);
54
- db.prepare("INSERT OR REPLACE INTO kv_store (k, v) VALUES (?, ?)").run(
55
- "dashboard_payload_v1",
56
- "{ invalide json [[["
57
- );
58
- const recovered = readPayload();
59
- expect(recovered.viewType).toBe("dashboard");
60
- });
61
-
62
- it("readPayload fusionne les défauts cfg si une clé est manquante dans le payload stocké", () => {
63
- const p = readPayload();
64
- // Supprimer une clé du cfg et réécrire
65
- const cfg = { ...p.cfg } as Record<string, unknown>;
66
- delete cfg["taskDefaultTagBucketEnabled"];
67
- writePayload({ ...p, cfg });
68
- const back = readPayload();
69
- // Le defaultCfg doit remettre la clé manquante
70
- expect(back.cfg).toHaveProperty("taskDefaultTagBucketEnabled");
71
- });
72
-
73
- it("knownProjects et knownTags sont des tableaux après lecture initiale", () => {
74
- const p = readPayload();
75
- expect(Array.isArray(p.knownProjects)).toBe(true);
76
- expect(Array.isArray(p.knownTags)).toBe(true);
77
- });
78
- });
@@ -1,46 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import type { KronosysUpdatePayload } from "@/lib/kronosysApi";
3
- import { materializeSessionWallClockInPayload, SESSION_WALL_SEGMENT_STARTED_AT } from "./sessionWallHydrate";
4
-
5
- describe("materializeSessionWallClockInPayload", () => {
6
- it("accumule la durée murale entre deux appels", () => {
7
- const t0 = Date.UTC(2026, 2, 1, 8, 0, 0);
8
- const cur = {
9
- sessionId: "sid-1",
10
- archived: false,
11
- isPaused: false,
12
- endAt: null,
13
- sessionDurationMinutes: 0,
14
- [SESSION_WALL_SEGMENT_STARTED_AT]: new Date(t0).toISOString(),
15
- };
16
- const p = {
17
- viewType: "dashboard",
18
- cfg: {},
19
- current: cur,
20
- } as unknown as KronosysUpdatePayload;
21
-
22
- const t1 = t0 + 120_000;
23
- const w1 = materializeSessionWallClockInPayload(p, t1);
24
- expect(w1).toBe(true);
25
- expect(cur.sessionDurationMinutes).toBeCloseTo(2, 5);
26
-
27
- const t2 = t1 + 60_000;
28
- const w2 = materializeSessionWallClockInPayload(p, t2);
29
- expect(w2).toBe(true);
30
- expect(cur.sessionDurationMinutes).toBeCloseTo(3, 5);
31
- });
32
-
33
- it("ne matérialise pas si la session est en pause", () => {
34
- const cur = {
35
- sessionId: "sid",
36
- archived: false,
37
- isPaused: true,
38
- endAt: null,
39
- sessionDurationMinutes: 1,
40
- [SESSION_WALL_SEGMENT_STARTED_AT]: new Date().toISOString(),
41
- };
42
- const p = { viewType: "dashboard", cfg: {}, current: cur } as unknown as KronosysUpdatePayload;
43
- expect(materializeSessionWallClockInPayload(p, Date.UTC(2026, 2, 1, 9, 0, 0))).toBe(false);
44
- expect(cur.sessionDurationMinutes).toBe(1);
45
- });
46
- });