@knpkv/jcf-web 0.2.0

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +315 -0
  3. package/dist/client/assets/AgentSettingsPanel-NhTzwiq6.js +1 -0
  4. package/dist/client/assets/Schema-DUFTUil2.js +2 -0
  5. package/dist/client/assets/UrlParams-ZN2s8OM_.js +6 -0
  6. package/dist/client/assets/agentSettings-nw9lCgBQ.js +1 -0
  7. package/dist/client/assets/api-BQCWhiKV.js +4 -0
  8. package/dist/client/assets/decoding-CebF86HD.js +1 -0
  9. package/dist/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  10. package/dist/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  11. package/dist/client/assets/index-Ch0W9DhA.css +1 -0
  12. package/dist/client/assets/index-N2vhoKYl.js +51 -0
  13. package/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
  14. package/dist/client/assets/transport-AcByYfLh.js +1 -0
  15. package/dist/client/index.html +19 -0
  16. package/dist/index.d.ts +11 -0
  17. package/dist/index.js +12 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/main.d.ts +2 -0
  20. package/dist/main.js +32 -0
  21. package/dist/main.js.map +1 -0
  22. package/dist/server/Api.d.ts +789 -0
  23. package/dist/server/Api.js +98 -0
  24. package/dist/server/Api.js.map +1 -0
  25. package/dist/server/Confirm.d.ts +94 -0
  26. package/dist/server/Confirm.js +243 -0
  27. package/dist/server/Confirm.js.map +1 -0
  28. package/dist/server/Handlers.d.ts +14 -0
  29. package/dist/server/Handlers.js +157 -0
  30. package/dist/server/Handlers.js.map +1 -0
  31. package/dist/server/HttpApplication.d.ts +7 -0
  32. package/dist/server/HttpApplication.js +62 -0
  33. package/dist/server/HttpApplication.js.map +1 -0
  34. package/dist/server/OwnerSession.d.ts +62 -0
  35. package/dist/server/OwnerSession.js +219 -0
  36. package/dist/server/OwnerSession.js.map +1 -0
  37. package/dist/server/RowDescriptions.d.ts +19 -0
  38. package/dist/server/RowDescriptions.js +42 -0
  39. package/dist/server/RowDescriptions.js.map +1 -0
  40. package/dist/server/SavedEntryOperations.d.ts +27 -0
  41. package/dist/server/SavedEntryOperations.js +171 -0
  42. package/dist/server/SavedEntryOperations.js.map +1 -0
  43. package/dist/server/Server.d.ts +13 -0
  44. package/dist/server/Server.js +29 -0
  45. package/dist/server/Server.js.map +1 -0
  46. package/dist/server/WeekPlan.d.ts +110 -0
  47. package/dist/server/WeekPlan.js +258 -0
  48. package/dist/server/WeekPlan.js.map +1 -0
  49. package/dist/server/WeekPlans.d.ts +30 -0
  50. package/dist/server/WeekPlans.js +190 -0
  51. package/dist/server/WeekPlans.js.map +1 -0
  52. package/dist/server/WeekRead.d.ts +435 -0
  53. package/dist/server/WeekRead.js +189 -0
  54. package/dist/server/WeekRead.js.map +1 -0
  55. package/dist/shared/contracts.d.ts +901 -0
  56. package/dist/shared/contracts.js +365 -0
  57. package/dist/shared/contracts.js.map +1 -0
  58. package/dist/shared/writePlanning.d.ts +6 -0
  59. package/dist/shared/writePlanning.js +19 -0
  60. package/dist/shared/writePlanning.js.map +1 -0
  61. package/dist/tsconfig.tsbuildinfo +1 -0
  62. package/package.json +60 -0
@@ -0,0 +1,98 @@
1
+ /** Authenticated HTTP routes. Browser-safe payload schemas live in shared/contracts. */
2
+ import { SessionAgentSettings } from "@knpkv/jira-clockify/agent/agentSettings.js";
3
+ import { Schema } from "effect";
4
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, HttpApiSecurity } from "effect/http-api";
5
+ import { ApiError, ConfirmPayload, Day, DescribeRowRequest, DescribeRowResponse, DescribeSavedEntryRequest, DescribeSavedEntryResponse, ForbiddenApiError, ManualPayload, OwnershipPayload, OwnershipResult, PlanExpiredError, ProposalRejectedError, SavedWeek, StandingPayload, StandingResult, UnauthorizedApiError, UpdateSavedEntryRequest, UpdateSavedEntryResponse, WeekPlan, WeekScope, WriteResult } from "../shared/contracts.js";
6
+ export * from "../shared/contracts.js";
7
+ export class OwnerSessionAuth extends HttpApiMiddleware.Service()("@knpkv/jcf-web/OwnerSessionAuth", {
8
+ error: [UnauthorizedApiError, ForbiddenApiError],
9
+ security: {
10
+ ownerCookie: HttpApiSecurity.apiKey({ in: "cookie", key: "jcf_owner" })
11
+ }
12
+ }) {
13
+ }
14
+ export class WeekGroup extends HttpApiGroup.make("week")
15
+ .add(HttpApiEndpoint.get("saved", "/saved", {
16
+ query: Schema.Struct({ monday: Schema.optional(Day), only: Schema.optional(WeekScope) }),
17
+ success: SavedWeek
18
+ }))
19
+ .add(HttpApiEndpoint.get("read", "/", {
20
+ error: [ApiError, PlanExpiredError],
21
+ query: Schema.Struct({ monday: Schema.optional(Day), only: Schema.optional(WeekScope) }),
22
+ success: WeekPlan
23
+ }))
24
+ .add(HttpApiEndpoint.get("stream", "/stream", {
25
+ error: [ApiError, PlanExpiredError],
26
+ query: Schema.Struct({ monday: Schema.optional(Day), only: Schema.optional(WeekScope) }),
27
+ success: HttpApiSchema.StreamUint8Array({ contentType: "application/x-ndjson" })
28
+ }))
29
+ .add(HttpApiEndpoint.get("recorded", "/recorded", {
30
+ error: [ApiError, PlanExpiredError],
31
+ query: Schema.Struct({ planId: Schema.String }),
32
+ success: HttpApiSchema.StreamUint8Array({ contentType: "application/x-ndjson" })
33
+ }))
34
+ .add(HttpApiEndpoint.get("recordedOnly", "/recorded-only", {
35
+ error: [ApiError, PlanExpiredError],
36
+ query: Schema.Struct({ monday: Schema.optional(Day), only: Schema.optional(WeekScope) }),
37
+ success: HttpApiSchema.StreamUint8Array({ contentType: "application/x-ndjson" })
38
+ }))
39
+ .prefix("/api/week") {
40
+ }
41
+ export class RowsGroup extends HttpApiGroup.make("rows")
42
+ .add(HttpApiEndpoint.post("describe", "/describe", {
43
+ error: [ApiError, PlanExpiredError],
44
+ payload: DescribeRowRequest,
45
+ success: DescribeRowResponse
46
+ }))
47
+ .add(HttpApiEndpoint.post("confirm", "/confirm", {
48
+ error: [ApiError, PlanExpiredError, ProposalRejectedError],
49
+ payload: ConfirmPayload,
50
+ success: WriteResult
51
+ }))
52
+ .add(HttpApiEndpoint.post("manual", "/manual", {
53
+ error: [ApiError, ProposalRejectedError],
54
+ payload: ManualPayload,
55
+ success: WriteResult
56
+ }))
57
+ .prefix("/api/rows") {
58
+ }
59
+ export class ConfigGroup extends HttpApiGroup.make("config")
60
+ .add(HttpApiEndpoint.get("agent", "/agent", { success: SessionAgentSettings }))
61
+ .add(HttpApiEndpoint.post("saveAgent", "/agent", {
62
+ payload: SessionAgentSettings,
63
+ success: SessionAgentSettings,
64
+ error: ApiError
65
+ }))
66
+ .add(HttpApiEndpoint.post("standing", "/standing", {
67
+ error: [ApiError, ProposalRejectedError],
68
+ payload: StandingPayload,
69
+ success: StandingResult
70
+ }))
71
+ .add(HttpApiEndpoint.post("mine", "/mine", {
72
+ error: [ApiError, ProposalRejectedError],
73
+ payload: OwnershipPayload,
74
+ success: OwnershipResult
75
+ }))
76
+ .prefix("/api/config") {
77
+ }
78
+ export class EntriesGroup extends HttpApiGroup.make("entries")
79
+ .add(HttpApiEndpoint.post("update", "/update", {
80
+ error: [ApiError, PlanExpiredError, ProposalRejectedError],
81
+ payload: UpdateSavedEntryRequest,
82
+ success: UpdateSavedEntryResponse
83
+ }))
84
+ .add(HttpApiEndpoint.post("describe", "/describe", {
85
+ error: [ApiError, PlanExpiredError],
86
+ payload: DescribeSavedEntryRequest,
87
+ success: DescribeSavedEntryResponse
88
+ }))
89
+ .prefix("/api/entries") {
90
+ }
91
+ export class JcfWebApi extends HttpApi.make("JcfWebApi")
92
+ .add(WeekGroup)
93
+ .add(RowsGroup)
94
+ .add(ConfigGroup)
95
+ .add(EntriesGroup)
96
+ .middleware(OwnerSessionAuth) {
97
+ }
98
+ //# sourceMappingURL=Api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Api.js","sourceRoot":"","sources":["../../src/server/Api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Accepting a row, and logging time by hand.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - **Held evidence, live arithmetic.** The plan says what the sessions accounted for; Jira and
7
+ * Clockify are asked again, at this moment, what they already hold. The write is the difference.
8
+ * That is the property that makes confirming the same row twice safe, and it is why nothing here
9
+ * trusts a number that came from a browser except the two a person is allowed to choose.
10
+ * - **An override re-tallies the bucket it moves to.** A row moved to another Issue Key may land on a
11
+ * day that already holds time the plan knew nothing about; sizing the write from the plan would log
12
+ * that time twice.
13
+ * - **Outcomes, not HTTP.** Every refusal is a tagged value, so the transport maps them to status
14
+ * codes and a test reads them directly.
15
+ *
16
+ * @module
17
+ */
18
+ import { AgentWrite, ReconcileService } from "@knpkv/jira-clockify";
19
+ import { Effect } from "effect";
20
+ import { MINIMUM_WRITE_SECONDS } from "../shared/writePlanning.js";
21
+ import { ProposalRejectedError, type WriteResultResponse } from "./Api.js";
22
+ import { type HeldPlan } from "./WeekPlan.js";
23
+ /** The engine operations these functions need. Narrow, so a test provides only what it must. */
24
+ export type WriteCapableService = Pick<ReconcileService.ReconcileServiceContract, "applyToClockify" | "applyToJira" | "refreshRecordedTime">;
25
+ /** What a confirmation did, or why it did nothing. */
26
+ export type ConfirmOutcome = {
27
+ readonly _tag: "Written";
28
+ readonly result: WriteResultResponse;
29
+ } | {
30
+ readonly _tag: "NothingOwed";
31
+ readonly result: WriteResultResponse;
32
+ } | {
33
+ readonly _tag: "UnknownRow";
34
+ }
35
+ /** A block position that is not part of this row — a page confirming against a plan that moved. */
36
+ | {
37
+ readonly _tag: "UnknownBlocks";
38
+ } | {
39
+ readonly _tag: "PastEvidence";
40
+ readonly maxSeconds: number;
41
+ } | {
42
+ readonly _tag: "BelowMinimum";
43
+ readonly minimumSeconds: number;
44
+ } | {
45
+ readonly _tag: "RunningTimer";
46
+ readonly reason: string;
47
+ } | {
48
+ readonly _tag: "NoTargets";
49
+ };
50
+ export interface ConfirmRequest {
51
+ readonly rowId: string;
52
+ /** Which of the row's blocks to write, by position. Absent means the whole row. */
53
+ readonly blocks: ReadonlyArray<number> | undefined;
54
+ readonly seconds: number | undefined;
55
+ readonly ticketKey: string | undefined;
56
+ readonly note: string | undefined;
57
+ /** Which systems to write. Absent means the ones the plan was read under. */
58
+ readonly targets: AgentWrite.WriteTargets | undefined;
59
+ }
60
+ /**
61
+ * Write one confirmed row.
62
+ *
63
+ * `summaryOf` supplies the issue title Clockify has no other way to know. It is asked for after the
64
+ * row is known to be writable and is expected to answer null on failure: a missing title must never
65
+ * cost a write that is otherwise correct.
66
+ */
67
+ export declare const confirmProposal: (options: {
68
+ readonly service: WriteCapableService;
69
+ readonly plan: HeldPlan;
70
+ readonly request: ConfirmRequest;
71
+ readonly summaryOf: (ticketKey: string) => Effect.Effect<string | null>;
72
+ }) => Effect.Effect<ConfirmOutcome, ReconcileService.ReconcileError>;
73
+ export interface ManualRequest {
74
+ readonly day: string;
75
+ readonly ticketKey: string;
76
+ readonly seconds: number;
77
+ readonly startClock: string | undefined;
78
+ readonly note: string | undefined;
79
+ readonly targets: AgentWrite.WriteTargets;
80
+ }
81
+ /**
82
+ * Log time no session evidences.
83
+ *
84
+ * Not a proposal, so not sized to a gap and not routed through `applyProposal`: a person typing
85
+ * thirty minutes means add thirty minutes to both systems, the way `jcf timer log` has always
86
+ * behaved. Nothing is subtracted, so typing it twice logs it twice — which is what typing it twice
87
+ * means.
88
+ */
89
+ export declare const logManualEntry: (options: {
90
+ readonly service: Pick<WriteCapableService, "applyToClockify" | "applyToJira">;
91
+ readonly request: ManualRequest;
92
+ readonly summaryOf: (ticketKey: string) => Effect.Effect<string | null>;
93
+ }) => Effect.Effect<WriteResultResponse, ProposalRejectedError>;
94
+ export { MINIMUM_WRITE_SECONDS };
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Accepting a row, and logging time by hand.
3
+ *
4
+ * **Mental model**
5
+ *
6
+ * - **Held evidence, live arithmetic.** The plan says what the sessions accounted for; Jira and
7
+ * Clockify are asked again, at this moment, what they already hold. The write is the difference.
8
+ * That is the property that makes confirming the same row twice safe, and it is why nothing here
9
+ * trusts a number that came from a browser except the two a person is allowed to choose.
10
+ * - **An override re-tallies the bucket it moves to.** A row moved to another Issue Key may land on a
11
+ * day that already holds time the plan knew nothing about; sizing the write from the plan would log
12
+ * that time twice.
13
+ * - **Outcomes, not HTTP.** Every refusal is a tagged value, so the transport maps them to status
14
+ * codes and a test reads them directly.
15
+ *
16
+ * @module
17
+ */
18
+ import { AgentWrite, ReconcileService, SourceConsumption, Time } from "@knpkv/jira-clockify";
19
+ import { Clock, Effect } from "effect";
20
+ import { MINIMUM_WRITE_SECONDS, prepareProposal } from "../shared/writePlanning.js";
21
+ import { ProposalRejectedError } from "./Api.js";
22
+ import { evidenceBlockKey, evidenceMarker, reconcileConsumption } from "./WeekPlan.js";
23
+ import { planSides } from "./WeekPlan.js";
24
+ /**
25
+ * Nothing was owed on the sides that were asked for.
26
+ *
27
+ * A side nobody asked about is `Skipped` rather than `NothingOwed`: its gap may well still be there,
28
+ * and saying "already holds this time" about a system that was never read would be a claim this run
29
+ * cannot make.
30
+ */
31
+ const nothingOwed = (description, targets) => {
32
+ const asked = { _tag: "NothingOwed" };
33
+ const unasked = { _tag: "Skipped" };
34
+ const clockify = targets.clockify ? asked : unasked;
35
+ const jira = targets.jira ? asked : unasked;
36
+ return {
37
+ clockify,
38
+ description,
39
+ jira,
40
+ lines: ["· already logged where asked — nothing written", ...AgentWrite.writeOutcomeLines({ clockify, jira })]
41
+ };
42
+ };
43
+ /**
44
+ * Write one confirmed row.
45
+ *
46
+ * `summaryOf` supplies the issue title Clockify has no other way to know. It is asked for after the
47
+ * row is known to be writable and is expected to answer null on failure: a missing title must never
48
+ * cost a write that is otherwise correct.
49
+ */
50
+ export const confirmProposal = (options) => Effect.gen(function* () {
51
+ const evidence = options.plan.evidence.get(options.request.rowId);
52
+ if (evidence === undefined)
53
+ return { _tag: "UnknownRow" };
54
+ const proposal = evidence.proposal;
55
+ let consumption = options.plan.consumption;
56
+ const prepared = prepareProposal({
57
+ evidence: { ...proposal, credited: proposal.sessionSeconds },
58
+ request: options.request,
59
+ targets: planSides(options.plan),
60
+ consumed: (block, source) => consumption.get(evidenceBlockKey(evidence.rowId, block))?.[source] ?? 0
61
+ });
62
+ if (prepared._tag !== "Prepared")
63
+ return prepared;
64
+ // Re-read the requested providers and timer state from retained evidence. Cached browser totals
65
+ // never authorize writes, and a timer started after the week was read must still withhold its day.
66
+ const refreshed = yield* options.service.refreshRecordedTime(Time.isoWeekPeriod(new Date(`${options.plan.plan.monday}T12:00:00`)), { ...options.plan.report, sides: prepared.targets }, { jiraIssueKeys: [...options.plan.jiraReceiptIssueKeys, prepared.ticketKey] });
67
+ const retainedScopes = options.plan.boundScopes;
68
+ const currentScopes = refreshed.sourceScopes;
69
+ const expectedScopes = currentScopes === undefined
70
+ ? undefined
71
+ : {
72
+ clockify: currentScopes.clockify === null
73
+ ? null
74
+ : retainedScopes.clockify ?? currentScopes.clockify,
75
+ jira: currentScopes.jira === null ? null : retainedScopes.jira ?? currentScopes.jira
76
+ };
77
+ if (expectedScopes === undefined || currentScopes === undefined ||
78
+ (prepared.targets.clockify &&
79
+ expectedScopes.clockify !== null && expectedScopes.clockify !== currentScopes.clockify) ||
80
+ (prepared.targets.jira && expectedScopes.jira !== null && expectedScopes.jira !== currentScopes.jira)) {
81
+ return yield* new ReconcileService.ReconcileError({
82
+ message: "The provider account changed or could not be verified; reload the week before confirming"
83
+ });
84
+ }
85
+ if (prepared.targets.clockify && expectedScopes.clockify !== null) {
86
+ options.plan.boundScopes.clockify = expectedScopes.clockify;
87
+ }
88
+ if (prepared.targets.jira && expectedScopes.jira !== null) {
89
+ options.plan.boundScopes.jira = expectedScopes.jira;
90
+ }
91
+ consumption = reconcileConsumption(refreshed, consumption);
92
+ options.plan.consumption.clear();
93
+ for (const [key, value] of consumption)
94
+ options.plan.consumption.set(key, value);
95
+ const excluded = refreshed.excludedDays.find(({ day }) => day === prepared.day);
96
+ if (excluded !== undefined) {
97
+ return { _tag: "RunningTimer", reason: excluded.reason };
98
+ }
99
+ const unlinkedRows = refreshed.unlinkedClockify.map((slice) => ({
100
+ intervals: slice.entry === undefined ? [] : [{ entry: slice.entry }]
101
+ }));
102
+ const ambiguous = SourceConsumption.unlinkedClockifyOverlaps(proposal.blocks, unlinkedRows, refreshed.sourceEntries);
103
+ const freshPrepared = prepareProposal({
104
+ evidence: {
105
+ ...proposal,
106
+ blocks: proposal.blocks.map((block, index) => ({
107
+ ...block,
108
+ clockifyRefusal: ambiguous[index] === true ? "unlinked-overlap" : undefined
109
+ })),
110
+ credited: proposal.sessionSeconds
111
+ },
112
+ request: options.request,
113
+ targets: planSides(options.plan),
114
+ consumed: (block, source) => consumption.get(evidenceBlockKey(evidence.rowId, block))?.[source] ?? 0
115
+ });
116
+ if (freshPrepared._tag !== "Prepared")
117
+ return freshPrepared;
118
+ const write = freshPrepared.plan(ReconcileService.recordedForExecution(refreshed.recorded, refreshed.jiraAvailability));
119
+ if (write._tag === "PastEvidence" || write._tag === "BelowMinimum")
120
+ return write;
121
+ const retainedTitle = options.plan.ownership.facts.get(prepared.ticketKey)?.title ?? null;
122
+ const summary = retainedTitle ??
123
+ (freshPrepared.targets.jira ? yield* options.summaryOf(freshPrepared.ticketKey) : null);
124
+ const description = AgentWrite.entryDescription({
125
+ note: options.request.note ?? null,
126
+ provenance: freshPrepared.provenance,
127
+ summary
128
+ });
129
+ // The suffix helps import legacy writes. The private provider-ID binding remains authoritative
130
+ // when a saved entry is relabeled or its description is edited outside jcf.
131
+ const providerWrite = write._tag === "NothingOwed"
132
+ ? {
133
+ _tag: "Write",
134
+ ticketKey: freshPrepared.ticketKey,
135
+ day: freshPrepared.day,
136
+ targets: freshPrepared.targets,
137
+ clockify: { seconds: 0, segments: [], startedAt: undefined },
138
+ jira: { seconds: 0, segments: [], startedAt: undefined }
139
+ }
140
+ : {
141
+ ...write,
142
+ clockify: {
143
+ ...write.clockify,
144
+ segments: write.clockify.segments.map((segment) => ({
145
+ ...segment,
146
+ description: `${description}\n${evidenceMarker(evidence.rowId, segment.block)}`
147
+ }))
148
+ },
149
+ jira: {
150
+ ...write.jira,
151
+ segments: write.jira.segments.map((segment) => ({
152
+ ...segment,
153
+ description: `${description}\n${evidenceMarker(evidence.rowId, segment.block)}`
154
+ }))
155
+ }
156
+ };
157
+ const outcome = yield* AgentWrite.applyPlannedWrite(options.service, providerWrite, description, evidence.rowId, expectedScopes, refreshed.jiraAvailability);
158
+ if (write._tag === "NothingOwed" &&
159
+ outcome.clockify._tag === (freshPrepared.targets.clockify ? "NothingOwed" : "Skipped") &&
160
+ outcome.jira._tag === (freshPrepared.targets.jira ? "NothingOwed" : "Skipped")) {
161
+ return { _tag: "NothingOwed", result: nothingOwed(description, freshPrepared.targets) };
162
+ }
163
+ for (const source of ["clockify", "jira"]) {
164
+ const side = outcome[source];
165
+ if (side._tag !== "Written" && side._tag !== "PartiallyWritten")
166
+ continue;
167
+ if (source === "jira")
168
+ options.plan.jiraReceiptIssueKeys.add(prepared.ticketKey);
169
+ let remaining = side.seconds;
170
+ for (const segment of providerWrite[source].segments) {
171
+ if (remaining <= 0)
172
+ break;
173
+ const block = segment.block;
174
+ const key = evidenceBlockKey(evidence.rowId, block);
175
+ const previous = consumption.get(key) ?? { clockify: 0, jira: 0 };
176
+ const added = Math.min(remaining, segment.seconds, Math.max(0, block.seconds - previous[source]));
177
+ const value = { ...previous, [source]: previous[source] + added };
178
+ consumption.set(key, value);
179
+ options.plan.consumption.set(key, value);
180
+ remaining -= added;
181
+ }
182
+ }
183
+ return {
184
+ _tag: "Written",
185
+ result: {
186
+ clockify: outcome.clockify,
187
+ description,
188
+ jira: outcome.jira,
189
+ lines: AgentWrite.writeOutcomeLines(outcome)
190
+ }
191
+ };
192
+ });
193
+ /**
194
+ * Log time no session evidences.
195
+ *
196
+ * Not a proposal, so not sized to a gap and not routed through `applyProposal`: a person typing
197
+ * thirty minutes means add thirty minutes to both systems, the way `jcf timer log` has always
198
+ * behaved. Nothing is subtracted, so typing it twice logs it twice — which is what typing it twice
199
+ * means.
200
+ */
201
+ export const logManualEntry = (options) => Effect.gen(function* () {
202
+ const { request } = options;
203
+ const startedAt = new Date(`${request.day}T${request.startClock ?? "12:00"}:00`);
204
+ const startMs = startedAt.getTime();
205
+ if (!Number.isFinite(startMs) || Time.localDay(startedAt) !== request.day ||
206
+ (request.startClock !== undefined && Time.formatClock(startedAt) !== request.startClock)) {
207
+ return yield* new ProposalRejectedError({ message: "That local start time does not exist on the chosen day" });
208
+ }
209
+ const nowMs = yield* Clock.currentTimeMillis;
210
+ if (startMs > nowMs) {
211
+ return yield* new ProposalRejectedError({ message: "Start time is in the future" });
212
+ }
213
+ if (startMs + request.seconds * 1000 > nowMs) {
214
+ return yield* new ProposalRejectedError({ message: "End time is in the future" });
215
+ }
216
+ const description = AgentWrite.entryDescription({
217
+ note: request.note ?? null,
218
+ provenance: AgentWrite.byHand,
219
+ summary: request.targets.jira ? yield* options.summaryOf(request.ticketKey) : null
220
+ });
221
+ const clockify = !request.targets.clockify
222
+ ? { _tag: "Skipped" }
223
+ : yield* options.service
224
+ .applyToClockify(request.ticketKey, request.day, request.seconds, description, startedAt)
225
+ .pipe(Effect.map((created) => created
226
+ ? { _tag: "Written", seconds: request.seconds }
227
+ : { _tag: "Refused", message: "the entry was not created" }), Effect.catch((error) => Effect.succeed({ _tag: "Refused", message: error.message })));
228
+ if (!request.targets.jira) {
229
+ const jira = { _tag: "Skipped" };
230
+ return { clockify, description, jira, lines: AgentWrite.writeOutcomeLines({ clockify, jira }) };
231
+ }
232
+ const posted = yield* options.service.applyToJira(request.ticketKey, request.day, request.seconds, description, startedAt);
233
+ const jira = posted._tag === "Posted"
234
+ ? { _tag: "Written", seconds: request.seconds }
235
+ : posted._tag === "NotLoggedIn"
236
+ ? { _tag: "NotLoggedIn" }
237
+ : posted._tag === "VerificationUnavailable"
238
+ ? { _tag: "Refused", message: "the Jira account could not be verified; refresh before writing" }
239
+ : { _tag: "Refused", message: posted.message };
240
+ return { clockify, description, jira, lines: AgentWrite.writeOutcomeLines({ clockify, jira }) };
241
+ });
242
+ export { MINIMUM_WRITE_SECONDS };
243
+ //# sourceMappingURL=Confirm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Confirm.js","sourceRoot":"","sources":["../../src/server/Confirm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAE5F,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACtC,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AACnF,OAAO,EAAE,qBAAqB,EAA4B,MAAM,UAAU,CAAA;AAC1E,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAiB,oBAAoB,EAAE,MAAM,eAAe,CAAA;AACrG,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AA+BzC;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,CAAC,WAAmB,EAAE,OAAgC,EAAuB,EAAE;IACjG,MAAM,KAAK,GAA2B,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAC7D,MAAM,OAAO,GAA2B,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAA;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAA;IAC3C,OAAO;QACL,QAAQ;QACR,WAAW;QACX,IAAI;QACJ,KAAK,EAAE,CAAC,gDAAgD,EAAE,GAAG,UAAU,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;KAC/G,CAAA;AACH,CAAC,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAK/B,EAAkE,EAAE,CACnE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACjE,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAA2B,CAAA;IAClF,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAA;IAClC,IAAI,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAA;IAC1C,MAAM,QAAQ,GAAG,eAAe,CAAC;QAC/B,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,cAAc,EAAE;QAC5D,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;QAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,gBAAgB,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;KACrG,CAAC,CAAA;IACF,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,QAAQ,CAAA;IAEjD,gGAAgG;IAChG,mGAAmG;IACnG,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAC1D,IAAI,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,WAAW,CAAC,CAAC,EACpE,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,EACnD,EAAE,aAAa,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAC9E,CAAA;IACD,MAAM,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAA;IAC/C,MAAM,aAAa,GAAG,SAAS,CAAC,YAAY,CAAA;IAC5C,MAAM,cAAc,GAAG,aAAa,KAAK,SAAS;QAChD,CAAC,CAAC,SAAS;QACX,CAAC,CAAC;YACA,QAAQ,EAAE,aAAa,CAAC,QAAQ,KAAK,IAAI;gBACvC,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,cAAc,CAAC,QAAQ,IAAI,aAAa,CAAC,QAAQ;YACrD,IAAI,EAAE,aAAa,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,IAAI,aAAa,CAAC,IAAI;SACrF,CAAA;IACH,IACE,cAAc,KAAK,SAAS,IAAI,aAAa,KAAK,SAAS;QAC3D,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ;YACxB,cAAc,CAAC,QAAQ,KAAK,IAAI,IAAI,cAAc,CAAC,QAAQ,KAAK,aAAa,CAAC,QAAQ,CAAC;QACzF,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI,KAAK,IAAI,IAAI,cAAc,CAAC,IAAI,KAAK,aAAa,CAAC,IAAI,CAAC,EACrG,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,IAAI,gBAAgB,CAAC,cAAc,CAAC;YAChD,OAAO,EAAE,0FAA0F;SACpG,CAAC,CAAA;IACJ,CAAC;IACD,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,IAAI,cAAc,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;QAClE,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAA;IAC7D,CAAC;IACD,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,GAAG,cAAc,CAAC,IAAI,CAAA;IACrD,CAAC;IACD,WAAW,GAAG,oBAAoB,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;IAC1D,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAA;IAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,WAAW;QAAE,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;IAChF,MAAM,QAAQ,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,GAAG,CAAC,CAAA;IAC/E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAA2B,CAAA;IACnF,CAAC;IACD,MAAM,YAAY,GAAG,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC9D,SAAS,EAAE,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC;KACrE,CAAC,CAAC,CAAA;IACH,MAAM,SAAS,GAAG,iBAAiB,CAAC,wBAAwB,CAC1D,QAAQ,CAAC,MAAM,EACf,YAAY,EACZ,SAAS,CAAC,aAAa,CACxB,CAAA;IACD,MAAM,aAAa,GAAG,eAAe,CAAC;QACpC,QAAQ,EAAE;YACR,GAAG,QAAQ;YACX,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC7C,GAAG,KAAK;gBACR,eAAe,EAAE,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS;aAC5E,CAAC,CAAC;YACH,QAAQ,EAAE,QAAQ,CAAC,cAAc;SAClC;QACD,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;QAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,gBAAgB,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;KACrG,CAAC,CAAA;IACF,IAAI,aAAa,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,aAAa,CAAA;IAC3D,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAC9B,gBAAgB,CAAC,oBAAoB,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,gBAAgB,CAAC,CACtF,CAAA;IACD,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,KAAK,CAAA;IAEhF,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,KAAK,IAAI,IAAI,CAAA;IACzF,MAAM,OAAO,GAAG,aAAa;QAC3B,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;IACzF,MAAM,WAAW,GAAG,UAAU,CAAC,gBAAgB,CAAC;QAC9C,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI;QAClC,UAAU,EAAE,aAAa,CAAC,UAAU;QACpC,OAAO;KACR,CAAC,CAAA;IACF,+FAA+F;IAC/F,4EAA4E;IAC5E,MAAM,aAAa,GAAiB,KAAK,CAAC,IAAI,KAAK,aAAa;QAC9D,CAAC,CAAC;YACA,IAAI,EAAE,OAAO;YACb,SAAS,EAAE,aAAa,CAAC,SAAS;YAClC,GAAG,EAAE,aAAa,CAAC,GAAG;YACtB,OAAO,EAAE,aAAa,CAAC,OAAO;YAC9B,QAAQ,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE;YAC5D,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE;SACzD;QACD,CAAC,CAAC;YACA,GAAG,KAAK;YACR,QAAQ,EAAE;gBACR,GAAG,KAAK,CAAC,QAAQ;gBACjB,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;oBAClD,GAAG,OAAO;oBACV,WAAW,EAAE,GAAG,WAAW,KAAK,cAAc,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE;iBAChF,CAAC,CAAC;aACJ;YACD,IAAI,EAAE;gBACJ,GAAG,KAAK,CAAC,IAAI;gBACb,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;oBAC9C,GAAG,OAAO;oBACV,WAAW,EAAE,GAAG,WAAW,KAAK,cAAc,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE;iBAChF,CAAC,CAAC;aACJ;SACF,CAAA;IACH,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,iBAAiB,CACjD,OAAO,CAAC,OAAO,EACf,aAAa,EACb,WAAW,EACX,QAAQ,CAAC,KAAK,EACd,cAAc,EACd,SAAS,CAAC,gBAAgB,CAC3B,CAAA;IACD,IACE,KAAK,CAAC,IAAI,KAAK,aAAa;QAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;QACtF,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,EAC9E,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,WAAW,EAAE,aAAa,CAAC,OAAO,CAAC,EAA2B,CAAA;IAClH,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,CAAC,UAAU,EAAE,MAAM,CAAwD,EAAE,CAAC;QACjG,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,kBAAkB;YAAE,SAAQ;QACzE,IAAI,MAAM,KAAK,MAAM;YAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;QAChF,IAAI,SAAS,GAAG,IAAI,CAAC,OAAO,CAAA;QAC5B,KAAK,MAAM,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;YACrD,IAAI,SAAS,IAAI,CAAC;gBAAE,MAAK;YACzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAA;YAC3B,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACnD,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;YACjE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;YACjG,MAAM,KAAK,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,KAAK,EAAE,CAAA;YACjE,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YAC3B,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YACxC,SAAS,IAAI,KAAK,CAAA;QACpB,CAAC;IACH,CAAC;IACD,OAAO;QACL,IAAI,EAAE,SAAS;QACf,MAAM,EAAE;YACN,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,WAAW;YACX,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,OAAO,CAAC;SAC7C;KACuB,CAAA;AAC5B,CAAC,CAAC,CAAA;AAWJ;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAI9B,EAA6D,EAAE,CAC9D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAC3B,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,UAAU,IAAI,OAAO,KAAK,CAAC,CAAA;IAChF,MAAM,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE,CAAA;IACnC,IACE,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,GAAG;QACrE,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,UAAU,CAAC,EACxF,CAAC;QACD,OAAO,KAAK,CAAC,CAAC,IAAI,qBAAqB,CAAC,EAAE,OAAO,EAAE,wDAAwD,EAAE,CAAC,CAAA;IAChH,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAA;IAC5C,IAAI,OAAO,GAAG,KAAK,EAAE,CAAC;QACpB,OAAO,KAAK,CAAC,CAAC,IAAI,qBAAqB,CAAC,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAC,CAAA;IACrF,CAAC;IACD,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,GAAG,KAAK,EAAE,CAAC;QAC7C,OAAO,KAAK,CAAC,CAAC,IAAI,qBAAqB,CAAC,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC,CAAA;IACnF,CAAC;IACD,MAAM,WAAW,GAAG,UAAU,CAAC,gBAAgB,CAAC;QAC9C,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;QAC1B,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI;KACnF,CAAC,CAAA;IAEF,MAAM,QAAQ,GAA2B,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ;QAChE,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE;QACrB,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO;aACrB,eAAe,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,CAAC;aACxF,IAAI,CACH,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,EAA0B,EAAE,CAC7C,OAAO;YACL,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE;YAC/C,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAC9D,EACD,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAyB,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAC7G,CAAA;IACL,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAC1B,MAAM,IAAI,GAA2B,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;QACxD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;IACjG,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAC/C,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,GAAG,EACX,OAAO,CAAC,OAAO,EACf,WAAW,EACX,SAAS,CACV,CAAA;IACD,MAAM,IAAI,GAA2B,MAAM,CAAC,IAAI,KAAK,QAAQ;QAC3D,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE;QAC/C,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,aAAa;YAC/B,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE;YACzB,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,yBAAyB;gBAC3C,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,gEAAgE,EAAE;gBAChG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;IAChD,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,CAAA;AACjG,CAAC,CAAC,CAAA;AAEJ,OAAO,EAAE,qBAAqB,EAAE,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Authenticated endpoint handlers.
3
+ *
4
+ * Week reads, retained descriptions, and write decisions live in their owning modules.
5
+ * These handlers translate their outcomes and coordinate configuration changes.
6
+ *
7
+ * @module
8
+ */
9
+ import { ConfigService, IssueFacts, ReconcileService } from "@knpkv/jira-clockify";
10
+ import { WeekPlans } from "./WeekPlans.js";
11
+ export declare const WeekLive: import("effect/Layer").Layer<import("effect/http-api/HttpApiGroup").Service<"JcfWebApi", "week">, never, ConfigService.ConfigService | IssueFacts.IssueFacts | import("./Api.js").OwnerSessionAuth | ReconcileService.ReconcileService | WeekPlans>;
12
+ export declare const RowsLive: import("effect/Layer").Layer<import("effect/http-api/HttpApiGroup").Service<"JcfWebApi", "rows">, never, import("@knpkv/jira-api-client").JiraApiClient | import("@knpkv/jira-cli/JiraAuth").JiraAuth | import("./Api.js").OwnerSessionAuth | ReconcileService.ReconcileService | WeekPlans>;
13
+ export declare const ConfigLive: import("effect/Layer").Layer<import("effect/http-api/HttpApiGroup").Service<"JcfWebApi", "config">, never, ConfigService.ConfigService | import("./Api.js").OwnerSessionAuth | WeekPlans>;
14
+ export declare const EntriesLive: import("effect/Layer").Layer<import("effect/http-api/HttpApiGroup").Service<"JcfWebApi", "entries">, never, import("./Api.js").OwnerSessionAuth | WeekPlans>;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Authenticated endpoint handlers.
3
+ *
4
+ * Week reads, retained descriptions, and write decisions live in their owning modules.
5
+ * These handlers translate their outcomes and coordinate configuration changes.
6
+ *
7
+ * @module
8
+ */
9
+ import { ConfigService, FetchTicket, IssueFacts, ReconcileService } from "@knpkv/jira-clockify";
10
+ import { Effect, Semaphore } from "effect";
11
+ import { HttpApiBuilder } from "effect/http-api";
12
+ import { ApiError, JcfWebApi, PlanExpiredError, ProposalRejectedError } from "./Api.js";
13
+ import { confirmProposal, logManualEntry, MINIMUM_WRITE_SECONDS } from "./Confirm.js";
14
+ import { WeekPlans } from "./WeekPlans.js";
15
+ import { readRecordedWeekPlan, readWeekPlan, refreshWeekPlan, savedWeekPlan, streamWeekRead } from "./WeekRead.js";
16
+ const failed = (message) => new ApiError({ message });
17
+ export const WeekLive = HttpApiBuilder.group(JcfWebApi, "week", (handlers) => Effect.gen(function* () {
18
+ const reconcile = yield* ReconcileService.ReconcileService;
19
+ const issues = yield* IssueFacts.IssueFacts;
20
+ const config = yield* ConfigService.ConfigService;
21
+ const plans = yield* WeekPlans;
22
+ const read = (query, report) => readWeekPlan({ query, report, reconcile, issues, config, plans });
23
+ return handlers
24
+ .handle("saved", ({ query }) => savedWeekPlan({ query, plans }))
25
+ .handle("read", ({ query }) => read(query, () => Effect.void))
26
+ .handle("stream", ({ query }) => Effect.succeed(streamWeekRead((report) => read(query, report))))
27
+ .handle("recordedOnly", ({ query }) => Effect.succeed(streamWeekRead((report) => readRecordedWeekPlan({ query, report, reconcile, config, plans }))))
28
+ .handle("recorded", ({ query }) => Effect.succeed(streamWeekRead((report) => refreshWeekPlan({ planId: query.planId, report, reconcile, plans }))));
29
+ }));
30
+ export const RowsLive = HttpApiBuilder.group(JcfWebApi, "rows", (handlers) => Effect.gen(function* () {
31
+ const reconcile = yield* ReconcileService.ReconcileService;
32
+ const plans = yield* WeekPlans;
33
+ const ticketSummary = yield* FetchTicket.ticketSummaryReader;
34
+ return handlers
35
+ .handle("describe", ({ payload }) => plans.describe(payload))
36
+ .handle("confirm", ({ payload }) => plans.withConfirmationPermit(Effect.gen(function* () {
37
+ const plan = yield* plans.find(payload.planId);
38
+ if (plan === undefined) {
39
+ return yield* new PlanExpiredError({ message: "This week was read too long ago — reload it" });
40
+ }
41
+ const outcome = yield* confirmProposal({
42
+ plan,
43
+ request: {
44
+ blocks: payload.blocks,
45
+ note: payload.note,
46
+ rowId: payload.rowId,
47
+ seconds: payload.seconds,
48
+ targets: payload.targets,
49
+ ticketKey: payload.ticketKey
50
+ },
51
+ service: reconcile,
52
+ summaryOf: ticketSummary
53
+ }).pipe(Effect.mapError((error) => failed(error.message)));
54
+ if (outcome._tag === "UnknownRow") {
55
+ return yield* new PlanExpiredError({ message: "That row is not part of this week — reload it" });
56
+ }
57
+ if (outcome._tag === "UnknownBlocks") {
58
+ return yield* new PlanExpiredError({
59
+ message: "Those blocks are not part of this row any more — reload the week"
60
+ });
61
+ }
62
+ if (outcome._tag === "PastEvidence") {
63
+ return yield* new ProposalRejectedError({
64
+ maxSeconds: outcome.maxSeconds,
65
+ message: "That is more time than the sessions evidence. Log the rest as a manual entry."
66
+ });
67
+ }
68
+ if (outcome._tag === "BelowMinimum") {
69
+ return yield* new ProposalRejectedError({
70
+ message: `Jira floors worklogs to the minute, so ${outcome.minimumSeconds}s is the smallest write.`
71
+ });
72
+ }
73
+ if (outcome._tag === "RunningTimer") {
74
+ return yield* new ProposalRejectedError({ message: outcome.reason });
75
+ }
76
+ if (outcome._tag === "NoTargets") {
77
+ return yield* new ProposalRejectedError({
78
+ message: "Pick at least one of Clockify or Jira — a write to neither is not a write."
79
+ });
80
+ }
81
+ return outcome.result;
82
+ })))
83
+ .handle("manual", ({ payload }) => plans.withConfirmationPermit(Effect.gen(function* () {
84
+ const targets = payload.targets ?? { clockify: true, jira: true };
85
+ if (!targets.clockify && !targets.jira) {
86
+ return yield* new ProposalRejectedError({
87
+ message: "Pick at least one of Clockify or Jira — a write to neither is not a write."
88
+ });
89
+ }
90
+ if (targets.jira && payload.seconds < MINIMUM_WRITE_SECONDS) {
91
+ return yield* new ProposalRejectedError({
92
+ message: `Jira floors worklogs to the minute, so ${MINIMUM_WRITE_SECONDS}s is the smallest write.`
93
+ });
94
+ }
95
+ return yield* logManualEntry({
96
+ request: {
97
+ day: payload.day,
98
+ note: payload.note,
99
+ seconds: payload.seconds,
100
+ startClock: payload.startClock,
101
+ targets,
102
+ ticketKey: payload.ticketKey
103
+ },
104
+ service: reconcile,
105
+ summaryOf: ticketSummary
106
+ });
107
+ })));
108
+ }));
109
+ export const ConfigLive = HttpApiBuilder.group(JcfWebApi, "config", (handlers) => Effect.gen(function* () {
110
+ const config = yield* ConfigService.ConfigService;
111
+ const plans = yield* WeekPlans;
112
+ const configWrites = yield* Semaphore.make(1);
113
+ return handlers
114
+ .handle("agent", () => config.get.pipe(Effect.map((settings) => settings.sessionAgent)))
115
+ .handle("saveAgent", ({ payload }) => configWrites.withPermits(1)(Effect.gen(function* () {
116
+ yield* plans.invalidateDescriptions;
117
+ yield* config.set({ sessionAgent: payload }).pipe(Effect.ensuring(plans.invalidateDescriptions));
118
+ const stored = (yield* config.get).sessionAgent;
119
+ if (stored.provider !== payload.provider || stored.model !== payload.model || stored.effort !== payload.effort) {
120
+ return yield* failed("Agent settings could not be saved. Retry the save.");
121
+ }
122
+ return stored;
123
+ })))
124
+ .handle("standing", ({ payload }) => configWrites.withPermits(1)(Effect.gen(function* () {
125
+ const current = yield* config.get;
126
+ const next = { ...current.sessionTicketMap, [payload.cwd]: payload.ticketKey };
127
+ yield* config.set({ sessionTicketMap: next });
128
+ // Read back rather than echo: the config service swallows a failed write, so echoing the map
129
+ // we meant to store would report a save that never happened.
130
+ const stored = (yield* config.get).sessionTicketMap;
131
+ if (stored[payload.cwd] !== payload.ticketKey) {
132
+ return yield* failed("The Standing Attribution could not be saved to ~/.jcf/config.json");
133
+ }
134
+ return { sessionTicketMap: stored };
135
+ })))
136
+ .handle("mine", ({ payload }) => configWrites.withPermits(1)(Effect.gen(function* () {
137
+ const current = yield* config.get;
138
+ const next = current.sessionOwnershipOverrides.includes(payload.ticketKey)
139
+ ? current.sessionOwnershipOverrides
140
+ : [...current.sessionOwnershipOverrides, payload.ticketKey].sort();
141
+ yield* config.set({ sessionOwnershipOverrides: next });
142
+ // Read back rather than echo: the config service swallows a failed write, so echoing the
143
+ // list we meant to store would report a decision that did not stick.
144
+ const stored = (yield* config.get).sessionOwnershipOverrides;
145
+ if (!stored.includes(payload.ticketKey)) {
146
+ return yield* failed("That ticket could not be saved to ~/.jcf/config.json");
147
+ }
148
+ return { ownershipOverrides: stored };
149
+ })));
150
+ }));
151
+ export const EntriesLive = HttpApiBuilder.group(JcfWebApi, "entries", (handlers) => Effect.gen(function* () {
152
+ const plans = yield* WeekPlans;
153
+ return handlers
154
+ .handle("update", ({ payload }) => plans.updateSaved(payload))
155
+ .handle("describe", ({ payload }) => plans.describeSaved(payload));
156
+ }));
157
+ //# sourceMappingURL=Handlers.js.map