@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.
- package/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/client/assets/AgentSettingsPanel-NhTzwiq6.js +1 -0
- package/dist/client/assets/Schema-DUFTUil2.js +2 -0
- package/dist/client/assets/UrlParams-ZN2s8OM_.js +6 -0
- package/dist/client/assets/agentSettings-nw9lCgBQ.js +1 -0
- package/dist/client/assets/api-BQCWhiKV.js +4 -0
- package/dist/client/assets/decoding-CebF86HD.js +1 -0
- package/dist/client/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/client/assets/index-Ch0W9DhA.css +1 -0
- package/dist/client/assets/index-N2vhoKYl.js +51 -0
- package/dist/client/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/assets/transport-AcByYfLh.js +1 -0
- package/dist/client/index.html +19 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +32 -0
- package/dist/main.js.map +1 -0
- package/dist/server/Api.d.ts +789 -0
- package/dist/server/Api.js +98 -0
- package/dist/server/Api.js.map +1 -0
- package/dist/server/Confirm.d.ts +94 -0
- package/dist/server/Confirm.js +243 -0
- package/dist/server/Confirm.js.map +1 -0
- package/dist/server/Handlers.d.ts +14 -0
- package/dist/server/Handlers.js +157 -0
- package/dist/server/Handlers.js.map +1 -0
- package/dist/server/HttpApplication.d.ts +7 -0
- package/dist/server/HttpApplication.js +62 -0
- package/dist/server/HttpApplication.js.map +1 -0
- package/dist/server/OwnerSession.d.ts +62 -0
- package/dist/server/OwnerSession.js +219 -0
- package/dist/server/OwnerSession.js.map +1 -0
- package/dist/server/RowDescriptions.d.ts +19 -0
- package/dist/server/RowDescriptions.js +42 -0
- package/dist/server/RowDescriptions.js.map +1 -0
- package/dist/server/SavedEntryOperations.d.ts +27 -0
- package/dist/server/SavedEntryOperations.js +171 -0
- package/dist/server/SavedEntryOperations.js.map +1 -0
- package/dist/server/Server.d.ts +13 -0
- package/dist/server/Server.js +29 -0
- package/dist/server/Server.js.map +1 -0
- package/dist/server/WeekPlan.d.ts +110 -0
- package/dist/server/WeekPlan.js +258 -0
- package/dist/server/WeekPlan.js.map +1 -0
- package/dist/server/WeekPlans.d.ts +30 -0
- package/dist/server/WeekPlans.js +190 -0
- package/dist/server/WeekPlans.js.map +1 -0
- package/dist/server/WeekRead.d.ts +435 -0
- package/dist/server/WeekRead.js +189 -0
- package/dist/server/WeekRead.js.map +1 -0
- package/dist/shared/contracts.d.ts +901 -0
- package/dist/shared/contracts.js +365 -0
- package/dist/shared/contracts.js.map +1 -0
- package/dist/shared/writePlanning.d.ts +6 -0
- package/dist/shared/writePlanning.js +19 -0
- package/dist/shared/writePlanning.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- 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":"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"}
|
|
@@ -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
|