taskify-nostr 0.1.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/README.md +271 -0
- package/dist/aiClient.js +40 -0
- package/dist/completions.js +637 -0
- package/dist/config.js +39 -0
- package/dist/index.js +2074 -0
- package/dist/nostrRuntime.js +888 -0
- package/dist/onboarding.js +93 -0
- package/dist/render.js +207 -0
- package/dist/shared/agentDispatcher.js +595 -0
- package/dist/shared/agentIdempotency.js +50 -0
- package/dist/shared/agentRuntime.js +7 -0
- package/dist/shared/agentSecurity.js +161 -0
- package/dist/shared/boardUtils.js +441 -0
- package/dist/shared/dateUtils.js +123 -0
- package/dist/shared/nostr.js +70 -0
- package/dist/shared/settingsTypes.js +23 -0
- package/dist/shared/taskTypes.js +12 -0
- package/dist/shared/taskUtils.js +261 -0
- package/dist/taskCache.js +59 -0
- package/package.json +44 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// CLI-adapted version of agentSecurity.ts (browser storage removed)
|
|
2
|
+
export const AGENT_SECURITY_STORAGE_KEY = "taskify.agent.security.v1";
|
|
3
|
+
function nowISO() {
|
|
4
|
+
return new Date().toISOString();
|
|
5
|
+
}
|
|
6
|
+
export function defaultAgentSecurityConfig() {
|
|
7
|
+
return {
|
|
8
|
+
enabled: true,
|
|
9
|
+
mode: "moderate",
|
|
10
|
+
trustedNpubs: [],
|
|
11
|
+
updatedISO: nowISO(),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function normalizeMode(value) {
|
|
15
|
+
return value === "strict" || value === "off" ? value : "moderate";
|
|
16
|
+
}
|
|
17
|
+
function normalizeTrustedNpub(value) {
|
|
18
|
+
if (typeof value !== "string")
|
|
19
|
+
return null;
|
|
20
|
+
const trimmed = value.trim().toLowerCase();
|
|
21
|
+
if (!trimmed)
|
|
22
|
+
return null;
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
export function isLooselyValidTrustedNpub(value) {
|
|
26
|
+
return typeof value === "string" && value.trim().toLowerCase().startsWith("npub1") && value.trim().length > 10;
|
|
27
|
+
}
|
|
28
|
+
function normalizeTrustedNpubList(value) {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
return [];
|
|
31
|
+
const deduped = new Map();
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
const normalized = normalizeTrustedNpub(entry);
|
|
34
|
+
if (!normalized)
|
|
35
|
+
continue;
|
|
36
|
+
deduped.set(normalized, normalized);
|
|
37
|
+
}
|
|
38
|
+
return Array.from(deduped.values()).sort();
|
|
39
|
+
}
|
|
40
|
+
export function normalizeAgentSecurityConfig(raw) {
|
|
41
|
+
const fallback = defaultAgentSecurityConfig();
|
|
42
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
43
|
+
return fallback;
|
|
44
|
+
const candidate = raw;
|
|
45
|
+
const updatedISO = typeof candidate.updatedISO === "string" && !Number.isNaN(Date.parse(candidate.updatedISO))
|
|
46
|
+
? new Date(candidate.updatedISO).toISOString()
|
|
47
|
+
: fallback.updatedISO;
|
|
48
|
+
return {
|
|
49
|
+
enabled: candidate.enabled === true,
|
|
50
|
+
mode: normalizeMode(candidate.mode),
|
|
51
|
+
trustedNpubs: normalizeTrustedNpubList(candidate.trustedNpubs),
|
|
52
|
+
updatedISO,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// In-memory store for CLI use (config.ts provides persistence)
|
|
56
|
+
const inMemoryStore = { config: null };
|
|
57
|
+
const defaultStore = {
|
|
58
|
+
get() {
|
|
59
|
+
return inMemoryStore.config ?? defaultAgentSecurityConfig();
|
|
60
|
+
},
|
|
61
|
+
set(config) {
|
|
62
|
+
const normalized = normalizeAgentSecurityConfig(config);
|
|
63
|
+
inMemoryStore.config = normalized;
|
|
64
|
+
return normalized;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
let currentAgentSecurityStore = defaultStore;
|
|
68
|
+
export function getAgentSecurityStore() {
|
|
69
|
+
return currentAgentSecurityStore;
|
|
70
|
+
}
|
|
71
|
+
export function setAgentSecurityStore(store) {
|
|
72
|
+
currentAgentSecurityStore = store ?? defaultStore;
|
|
73
|
+
}
|
|
74
|
+
export function loadAgentSecurityConfig() {
|
|
75
|
+
return currentAgentSecurityStore.get();
|
|
76
|
+
}
|
|
77
|
+
export function saveAgentSecurityConfig(config) {
|
|
78
|
+
return currentAgentSecurityStore.set(config);
|
|
79
|
+
}
|
|
80
|
+
export function updateAgentSecurityConfig(updates) {
|
|
81
|
+
const current = loadAgentSecurityConfig();
|
|
82
|
+
return saveAgentSecurityConfig({
|
|
83
|
+
enabled: typeof updates.enabled === "boolean" ? updates.enabled : current.enabled,
|
|
84
|
+
mode: updates.mode ? normalizeMode(updates.mode) : current.mode,
|
|
85
|
+
trustedNpubs: updates.trustedNpubs !== undefined
|
|
86
|
+
? normalizeTrustedNpubList(updates.trustedNpubs)
|
|
87
|
+
: current.trustedNpubs,
|
|
88
|
+
updatedISO: nowISO(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function addTrustedNpub(config, npub) {
|
|
92
|
+
const normalized = normalizeTrustedNpub(npub);
|
|
93
|
+
if (!normalized)
|
|
94
|
+
return normalizeAgentSecurityConfig(config);
|
|
95
|
+
return normalizeAgentSecurityConfig({
|
|
96
|
+
...config,
|
|
97
|
+
trustedNpubs: Array.from(new Set([...config.trustedNpubs.map((entry) => entry.toLowerCase()), normalized])),
|
|
98
|
+
updatedISO: nowISO(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function removeTrustedNpub(config, npub) {
|
|
102
|
+
const normalized = normalizeTrustedNpub(npub);
|
|
103
|
+
return normalizeAgentSecurityConfig({
|
|
104
|
+
...config,
|
|
105
|
+
trustedNpubs: config.trustedNpubs.filter((entry) => entry.toLowerCase() !== normalized),
|
|
106
|
+
updatedISO: nowISO(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export function clearTrustedNpubs(config) {
|
|
110
|
+
return normalizeAgentSecurityConfig({
|
|
111
|
+
...config,
|
|
112
|
+
trustedNpubs: [],
|
|
113
|
+
updatedISO: nowISO(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function getEffectiveAgentSecurityMode(config) {
|
|
117
|
+
if (!config.enabled)
|
|
118
|
+
return "off";
|
|
119
|
+
return normalizeMode(config.mode);
|
|
120
|
+
}
|
|
121
|
+
function classifyProvenance(createdByNpub, lastEditedByNpub, config) {
|
|
122
|
+
const trustedNpubs = new Set(config.trustedNpubs.map((entry) => entry.toLowerCase()));
|
|
123
|
+
const normalizedLastEditedBy = normalizeTrustedNpub(lastEditedByNpub);
|
|
124
|
+
const normalizedCreatedBy = normalizeTrustedNpub(createdByNpub);
|
|
125
|
+
if (normalizedLastEditedBy && trustedNpubs.has(normalizedLastEditedBy)) {
|
|
126
|
+
return "trusted";
|
|
127
|
+
}
|
|
128
|
+
if (!normalizedLastEditedBy && !normalizedCreatedBy) {
|
|
129
|
+
return "unknown";
|
|
130
|
+
}
|
|
131
|
+
return "untrusted";
|
|
132
|
+
}
|
|
133
|
+
export function annotateTrust(item, config) {
|
|
134
|
+
const createdByNpub = normalizeTrustedNpub(item.createdByNpub) ?? null;
|
|
135
|
+
const lastEditedByNpub = normalizeTrustedNpub(item.lastEditedByNpub) ?? null;
|
|
136
|
+
const provenance = classifyProvenance(createdByNpub, lastEditedByNpub, config);
|
|
137
|
+
const trusted = provenance === "trusted";
|
|
138
|
+
return {
|
|
139
|
+
...item,
|
|
140
|
+
createdByNpub,
|
|
141
|
+
lastEditedByNpub,
|
|
142
|
+
provenance,
|
|
143
|
+
trusted,
|
|
144
|
+
agentSafe: trusted,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export function applyTrustFilter(items, config) {
|
|
148
|
+
const annotated = items.map((item) => annotateTrust(item, config));
|
|
149
|
+
if (getEffectiveAgentSecurityMode(config) === "strict") {
|
|
150
|
+
return annotated.filter((item) => item.trusted);
|
|
151
|
+
}
|
|
152
|
+
return annotated;
|
|
153
|
+
}
|
|
154
|
+
export function summarizeTrustCounts(allItems, returnedCount) {
|
|
155
|
+
return {
|
|
156
|
+
trusted: allItems.filter((item) => item.provenance === "trusted").length,
|
|
157
|
+
untrusted: allItems.filter((item) => item.provenance === "untrusted").length,
|
|
158
|
+
unknown: allItems.filter((item) => item.provenance === "unknown").length,
|
|
159
|
+
returned: returnedCount,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { isoDatePart, parseDateKey, startOfDay, isoTimePart, formatDateKeyFromParts, isoFromDateTime, normalizeTimeZone, } from "./dateUtils.js";
|
|
2
|
+
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
3
|
+
export function parseCompoundChildInput(raw) {
|
|
4
|
+
const trimmed = raw.trim();
|
|
5
|
+
if (!trimmed)
|
|
6
|
+
return { boardId: "", relays: [] };
|
|
7
|
+
let boardId = trimmed;
|
|
8
|
+
let relaySegment = "";
|
|
9
|
+
const atIndex = trimmed.indexOf("@");
|
|
10
|
+
if (atIndex >= 0) {
|
|
11
|
+
boardId = trimmed.slice(0, atIndex).trim();
|
|
12
|
+
relaySegment = trimmed.slice(atIndex + 1).trim();
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const spaceIndex = trimmed.search(/\s/);
|
|
16
|
+
if (spaceIndex >= 0) {
|
|
17
|
+
boardId = trimmed.slice(0, spaceIndex).trim();
|
|
18
|
+
relaySegment = trimmed.slice(spaceIndex + 1).trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const relays = relaySegment
|
|
22
|
+
? relaySegment.split(/[\s,]+/).map((relay) => relay.trim()).filter(Boolean)
|
|
23
|
+
: [];
|
|
24
|
+
return { boardId, relays };
|
|
25
|
+
}
|
|
26
|
+
export function boardScopeIds(board, boards) {
|
|
27
|
+
const ids = new Set();
|
|
28
|
+
const addId = (value) => {
|
|
29
|
+
if (typeof value === "string" && value)
|
|
30
|
+
ids.add(value);
|
|
31
|
+
};
|
|
32
|
+
const addBoard = (target) => {
|
|
33
|
+
if (!target)
|
|
34
|
+
return;
|
|
35
|
+
addId(target.id);
|
|
36
|
+
addId(target.nostr?.boardId);
|
|
37
|
+
};
|
|
38
|
+
addBoard(board);
|
|
39
|
+
if (board.kind === "compound") {
|
|
40
|
+
board.children.forEach((childId) => {
|
|
41
|
+
addId(childId);
|
|
42
|
+
addBoard(findBoardByCompoundChildId(boards, childId));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return Array.from(ids);
|
|
46
|
+
}
|
|
47
|
+
export function findBoardByCompoundChildId(boards, childId) {
|
|
48
|
+
return boards.find((board) => {
|
|
49
|
+
if (board.id === childId)
|
|
50
|
+
return true;
|
|
51
|
+
return !!board.nostr?.boardId && board.nostr.boardId === childId;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export function compoundChildMatchesBoard(childId, board) {
|
|
55
|
+
return childId === board.id || (!!board.nostr?.boardId && childId === board.nostr.boardId);
|
|
56
|
+
}
|
|
57
|
+
export function normalizeCompoundChildId(boards, childId) {
|
|
58
|
+
const match = findBoardByCompoundChildId(boards, childId);
|
|
59
|
+
return match ? match.id : childId;
|
|
60
|
+
}
|
|
61
|
+
// ---- Board migration ----
|
|
62
|
+
export function migrateBoards(stored) {
|
|
63
|
+
try {
|
|
64
|
+
const arr = stored;
|
|
65
|
+
if (!Array.isArray(arr))
|
|
66
|
+
return null;
|
|
67
|
+
return arr.map((b) => {
|
|
68
|
+
const archived = typeof b?.archived === "boolean"
|
|
69
|
+
? b.archived
|
|
70
|
+
: typeof b?.hidden === "boolean"
|
|
71
|
+
? b.hidden
|
|
72
|
+
: false;
|
|
73
|
+
const hidden = typeof b?.hidden === "boolean" && typeof b?.archived === "boolean"
|
|
74
|
+
? b.hidden
|
|
75
|
+
: false;
|
|
76
|
+
const clearCompletedDisabled = typeof b?.clearCompletedDisabled === "boolean" ? b.clearCompletedDisabled : false;
|
|
77
|
+
const indexCardEnabled = typeof b?.indexCardEnabled === "boolean" ? Boolean(b.indexCardEnabled) : false;
|
|
78
|
+
const hideChildBoardNames = typeof b?.hideChildBoardNames === "boolean"
|
|
79
|
+
? Boolean(b.hideChildBoardNames)
|
|
80
|
+
: false;
|
|
81
|
+
if (b?.kind === "week") {
|
|
82
|
+
return {
|
|
83
|
+
id: b.id,
|
|
84
|
+
name: b.name,
|
|
85
|
+
kind: "week",
|
|
86
|
+
nostr: b.nostr,
|
|
87
|
+
archived,
|
|
88
|
+
hidden,
|
|
89
|
+
clearCompletedDisabled,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (b?.kind === "lists" && Array.isArray(b.columns)) {
|
|
93
|
+
return {
|
|
94
|
+
id: b.id,
|
|
95
|
+
name: b.name,
|
|
96
|
+
kind: "lists",
|
|
97
|
+
columns: b.columns,
|
|
98
|
+
nostr: b.nostr,
|
|
99
|
+
archived,
|
|
100
|
+
hidden,
|
|
101
|
+
clearCompletedDisabled,
|
|
102
|
+
indexCardEnabled,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (b?.kind === "compound") {
|
|
106
|
+
const rawChildren = Array.isArray(b?.children) ? b.children : [];
|
|
107
|
+
const children = rawChildren
|
|
108
|
+
.filter((child) => typeof child === "string" && child && child !== b.id);
|
|
109
|
+
return {
|
|
110
|
+
id: b.id,
|
|
111
|
+
name: b.name,
|
|
112
|
+
kind: "compound",
|
|
113
|
+
children,
|
|
114
|
+
nostr: b.nostr,
|
|
115
|
+
archived,
|
|
116
|
+
hidden,
|
|
117
|
+
clearCompletedDisabled,
|
|
118
|
+
indexCardEnabled,
|
|
119
|
+
hideChildBoardNames,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (b?.kind === "bible") {
|
|
123
|
+
const name = typeof b?.name === "string" && b.name.trim() ? b.name : "Bible";
|
|
124
|
+
return {
|
|
125
|
+
id: b.id,
|
|
126
|
+
name,
|
|
127
|
+
kind: "bible",
|
|
128
|
+
archived,
|
|
129
|
+
hidden,
|
|
130
|
+
clearCompletedDisabled,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (b?.kind === "list") {
|
|
134
|
+
const colId = crypto.randomUUID();
|
|
135
|
+
return {
|
|
136
|
+
id: b.id,
|
|
137
|
+
name: b.name,
|
|
138
|
+
kind: "lists",
|
|
139
|
+
columns: [{ id: colId, name: "Items" }],
|
|
140
|
+
nostr: b?.nostr,
|
|
141
|
+
archived,
|
|
142
|
+
hidden,
|
|
143
|
+
clearCompletedDisabled,
|
|
144
|
+
indexCardEnabled,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const colId = crypto.randomUUID();
|
|
148
|
+
return {
|
|
149
|
+
id: b?.id || crypto.randomUUID(),
|
|
150
|
+
name: b?.name || "Board",
|
|
151
|
+
kind: "lists",
|
|
152
|
+
columns: [{ id: colId, name: "Items" }],
|
|
153
|
+
nostr: b?.nostr,
|
|
154
|
+
archived,
|
|
155
|
+
hidden,
|
|
156
|
+
clearCompletedDisabled,
|
|
157
|
+
indexCardEnabled,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ---- Startup board selection ----
|
|
166
|
+
export function pickStartupBoard(boards, overrides) {
|
|
167
|
+
const visible = boards.filter(b => !b.archived && !b.hidden);
|
|
168
|
+
const today = new Date().getDay();
|
|
169
|
+
const overrideId = overrides?.[today];
|
|
170
|
+
if (overrideId) {
|
|
171
|
+
const match = visible.find(b => b.id === overrideId) || boards.find(b => !b.archived && b.id === overrideId);
|
|
172
|
+
if (match)
|
|
173
|
+
return match.id;
|
|
174
|
+
}
|
|
175
|
+
if (visible.length)
|
|
176
|
+
return visible[0].id;
|
|
177
|
+
const firstUnarchived = boards.find(b => !b.archived);
|
|
178
|
+
if (firstUnarchived)
|
|
179
|
+
return firstUnarchived.id;
|
|
180
|
+
return boards[0]?.id || "";
|
|
181
|
+
}
|
|
182
|
+
// ---- Week helpers ----
|
|
183
|
+
export function startOfWeek(d, weekStart) {
|
|
184
|
+
const sd = startOfDay(d);
|
|
185
|
+
const current = sd.getDay();
|
|
186
|
+
const ws = (weekStart === 1 || weekStart === 6) ? weekStart : 0;
|
|
187
|
+
let diff = current - ws;
|
|
188
|
+
if (diff < 0)
|
|
189
|
+
diff += 7;
|
|
190
|
+
return new Date(sd.getTime() - diff * 86400000);
|
|
191
|
+
}
|
|
192
|
+
// ---- Recurrence helpers ----
|
|
193
|
+
export function nextOccurrence(currentISO, rule, keepTime = false, timeZone) {
|
|
194
|
+
const safeZone = normalizeTimeZone(timeZone);
|
|
195
|
+
if (safeZone) {
|
|
196
|
+
const dateKey = isoDatePart(currentISO, safeZone);
|
|
197
|
+
const dateParts = parseDateKey(dateKey);
|
|
198
|
+
if (dateParts) {
|
|
199
|
+
const baseTime = keepTime ? isoTimePart(currentISO, safeZone) : "";
|
|
200
|
+
const applyDate = (parts) => {
|
|
201
|
+
const nextDateKey = formatDateKeyFromParts(parts.year, parts.month, parts.day);
|
|
202
|
+
return isoFromDateTime(nextDateKey, baseTime || undefined, safeZone);
|
|
203
|
+
};
|
|
204
|
+
const addDays = (d) => {
|
|
205
|
+
const base = new Date(Date.UTC(dateParts.year, dateParts.month - 1, dateParts.day));
|
|
206
|
+
base.setUTCDate(base.getUTCDate() + d);
|
|
207
|
+
return {
|
|
208
|
+
year: base.getUTCFullYear(),
|
|
209
|
+
month: base.getUTCMonth() + 1,
|
|
210
|
+
day: base.getUTCDate(),
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
const weekdayForParts = (parts) => new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
|
|
214
|
+
let next = null;
|
|
215
|
+
switch (rule.type) {
|
|
216
|
+
case "none":
|
|
217
|
+
next = null;
|
|
218
|
+
break;
|
|
219
|
+
case "daily":
|
|
220
|
+
next = applyDate(addDays(1));
|
|
221
|
+
break;
|
|
222
|
+
case "weekly": {
|
|
223
|
+
if (!rule.days.length)
|
|
224
|
+
return null;
|
|
225
|
+
for (let i = 1; i <= 28; i++) {
|
|
226
|
+
const cand = addDays(i);
|
|
227
|
+
const wd = weekdayForParts(cand);
|
|
228
|
+
if (rule.days.includes(wd)) {
|
|
229
|
+
next = applyDate(cand);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "every": {
|
|
236
|
+
if (rule.unit === "hour") {
|
|
237
|
+
const current = new Date(currentISO);
|
|
238
|
+
const n = new Date(current.getTime() + rule.n * 3600000);
|
|
239
|
+
next = n.toISOString();
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const daysToAdd = rule.unit === "day" ? rule.n : rule.n * 7;
|
|
243
|
+
next = applyDate(addDays(daysToAdd));
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
case "monthlyDay": {
|
|
248
|
+
const interval = Math.max(1, rule.interval ?? 1);
|
|
249
|
+
const base = new Date(Date.UTC(dateParts.year, dateParts.month - 1 + interval, 1));
|
|
250
|
+
const n = {
|
|
251
|
+
year: base.getUTCFullYear(),
|
|
252
|
+
month: base.getUTCMonth() + 1,
|
|
253
|
+
day: Math.min(rule.day, 28),
|
|
254
|
+
};
|
|
255
|
+
next = applyDate(n);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (next && rule.untilISO) {
|
|
260
|
+
const limitKey = isoDatePart(rule.untilISO, safeZone);
|
|
261
|
+
const nextKey = isoDatePart(next, safeZone);
|
|
262
|
+
if (nextKey > limitKey)
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
return next;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const currentDate = new Date(currentISO);
|
|
269
|
+
const curDay = startOfDay(currentDate);
|
|
270
|
+
const timeOffset = currentDate.getTime() - curDay.getTime();
|
|
271
|
+
const baseTime = keepTime ? isoTimePart(currentISO) : "";
|
|
272
|
+
const applyTime = (day) => {
|
|
273
|
+
if (keepTime && baseTime) {
|
|
274
|
+
const datePart = isoDatePart(day.toISOString());
|
|
275
|
+
return isoFromDateTime(datePart, baseTime);
|
|
276
|
+
}
|
|
277
|
+
return new Date(day.getTime() + timeOffset).toISOString();
|
|
278
|
+
};
|
|
279
|
+
const addDays = (d) => {
|
|
280
|
+
const nextDay = startOfDay(new Date(curDay.getTime() + d * 86400000));
|
|
281
|
+
return applyTime(nextDay);
|
|
282
|
+
};
|
|
283
|
+
let next = null;
|
|
284
|
+
switch (rule.type) {
|
|
285
|
+
case "none":
|
|
286
|
+
next = null;
|
|
287
|
+
break;
|
|
288
|
+
case "daily":
|
|
289
|
+
next = addDays(1);
|
|
290
|
+
break;
|
|
291
|
+
case "weekly": {
|
|
292
|
+
if (!rule.days.length)
|
|
293
|
+
return null;
|
|
294
|
+
for (let i = 1; i <= 28; i++) {
|
|
295
|
+
const cand = addDays(i);
|
|
296
|
+
const wd = new Date(cand).getDay();
|
|
297
|
+
if (rule.days.includes(wd)) {
|
|
298
|
+
next = cand;
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case "every": {
|
|
305
|
+
if (rule.unit === "hour") {
|
|
306
|
+
const current = new Date(currentISO);
|
|
307
|
+
const n = new Date(current.getTime() + rule.n * 3600000);
|
|
308
|
+
next = n.toISOString();
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const daysToAdd = rule.unit === "day" ? rule.n : rule.n * 7;
|
|
312
|
+
next = addDays(daysToAdd);
|
|
313
|
+
}
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "monthlyDay": {
|
|
317
|
+
const y = curDay.getFullYear(), m = curDay.getMonth();
|
|
318
|
+
const interval = Math.max(1, rule.interval ?? 1);
|
|
319
|
+
const n = startOfDay(new Date(y, m + interval, Math.min(rule.day, 28)));
|
|
320
|
+
next = applyTime(n);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (next && rule.untilISO) {
|
|
325
|
+
const limit = startOfDay(new Date(rule.untilISO)).getTime();
|
|
326
|
+
const n = startOfDay(new Date(next)).getTime();
|
|
327
|
+
if (n > limit)
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return next;
|
|
331
|
+
}
|
|
332
|
+
// ---- Visibility helpers ----
|
|
333
|
+
export function revealsOnDueDate(rule) {
|
|
334
|
+
if (isFrequentRecurrence(rule))
|
|
335
|
+
return true;
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
export function isFrequentRecurrence(rule) {
|
|
339
|
+
if (!rule)
|
|
340
|
+
return false;
|
|
341
|
+
if (rule.type === "daily" || rule.type === "weekly")
|
|
342
|
+
return true;
|
|
343
|
+
if (rule.type === "every") {
|
|
344
|
+
return rule.unit === "day" || rule.unit === "week";
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
export function isVisibleNow(t, now = new Date()) {
|
|
349
|
+
if (!t.hiddenUntilISO)
|
|
350
|
+
return true;
|
|
351
|
+
const today = startOfDay(now).getTime();
|
|
352
|
+
if (t.recurrence && revealsOnDueDate(t.recurrence)) {
|
|
353
|
+
const dueReveal = startOfDay(new Date(t.dueISO)).getTime();
|
|
354
|
+
if (!Number.isNaN(dueReveal))
|
|
355
|
+
return today >= dueReveal;
|
|
356
|
+
}
|
|
357
|
+
const reveal = startOfDay(new Date(t.hiddenUntilISO)).getTime();
|
|
358
|
+
return today >= reveal;
|
|
359
|
+
}
|
|
360
|
+
export function hiddenUntilForNext(nextISO, rule, weekStart) {
|
|
361
|
+
const nextMidnight = startOfDay(new Date(nextISO));
|
|
362
|
+
if (revealsOnDueDate(rule)) {
|
|
363
|
+
return nextMidnight.toISOString();
|
|
364
|
+
}
|
|
365
|
+
const sow = startOfWeek(nextMidnight, weekStart);
|
|
366
|
+
return sow.toISOString();
|
|
367
|
+
}
|
|
368
|
+
export function hiddenUntilForBoard(dueISO, boardKind, weekStart) {
|
|
369
|
+
const dueDate = startOfDay(new Date(dueISO));
|
|
370
|
+
if (Number.isNaN(dueDate.getTime()))
|
|
371
|
+
return undefined;
|
|
372
|
+
const today = startOfDay(new Date());
|
|
373
|
+
if (boardKind === "lists" || boardKind === "compound") {
|
|
374
|
+
return dueDate.getTime() > today.getTime() ? dueDate.toISOString() : undefined;
|
|
375
|
+
}
|
|
376
|
+
const nowSow = startOfWeek(new Date(), weekStart);
|
|
377
|
+
const dueSow = startOfWeek(dueDate, weekStart);
|
|
378
|
+
return dueSow.getTime() > nowSow.getTime() ? dueSow.toISOString() : undefined;
|
|
379
|
+
}
|
|
380
|
+
export function applyHiddenForFuture(task, weekStart, boardKind) {
|
|
381
|
+
if (task.dueDateEnabled === false) {
|
|
382
|
+
task.hiddenUntilISO = undefined;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
task.hiddenUntilISO = hiddenUntilForBoard(task.dueISO, boardKind, weekStart);
|
|
386
|
+
}
|
|
387
|
+
// ---- Calendar event visibility helpers ----
|
|
388
|
+
function calendarEventDateKey(event) {
|
|
389
|
+
if (event.kind === "date") {
|
|
390
|
+
return ISO_DATE_PATTERN.test(event.startDate) ? event.startDate : null;
|
|
391
|
+
}
|
|
392
|
+
const key = isoDatePart(event.startISO, event.startTzid);
|
|
393
|
+
return ISO_DATE_PATTERN.test(key) ? key : null;
|
|
394
|
+
}
|
|
395
|
+
function hiddenUntilForCalendarEvent(event, boardKind, weekStart) {
|
|
396
|
+
if (boardKind !== "lists" && boardKind !== "compound")
|
|
397
|
+
return undefined;
|
|
398
|
+
const dateKey = calendarEventDateKey(event);
|
|
399
|
+
if (!dateKey)
|
|
400
|
+
return undefined;
|
|
401
|
+
const parsed = parseDateKey(dateKey);
|
|
402
|
+
if (!parsed)
|
|
403
|
+
return undefined;
|
|
404
|
+
const eventDate = new Date(parsed.year, parsed.month - 1, parsed.day);
|
|
405
|
+
if (Number.isNaN(eventDate.getTime()))
|
|
406
|
+
return undefined;
|
|
407
|
+
const eventWeekStart = startOfWeek(eventDate, weekStart);
|
|
408
|
+
const currentWeekStart = startOfWeek(new Date(), weekStart);
|
|
409
|
+
if (eventWeekStart.getTime() > currentWeekStart.getTime()) {
|
|
410
|
+
return eventWeekStart.toISOString();
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
export function applyHiddenForCalendarEvent(event, weekStart, boardKind) {
|
|
415
|
+
const hiddenUntilISO = hiddenUntilForCalendarEvent(event, boardKind, weekStart);
|
|
416
|
+
if (hiddenUntilISO) {
|
|
417
|
+
if (event.hiddenUntilISO === hiddenUntilISO)
|
|
418
|
+
return event;
|
|
419
|
+
return { ...event, hiddenUntilISO };
|
|
420
|
+
}
|
|
421
|
+
if (!event.hiddenUntilISO)
|
|
422
|
+
return event;
|
|
423
|
+
return { ...event, hiddenUntilISO: undefined };
|
|
424
|
+
}
|
|
425
|
+
// ---- Order helpers ----
|
|
426
|
+
export function nextOrderForBoard(boardId, tasks, newTaskPosition) {
|
|
427
|
+
const boardTasks = tasks.filter(task => task.boardId === boardId);
|
|
428
|
+
if (newTaskPosition === "top") {
|
|
429
|
+
const minOrder = boardTasks.reduce((min, task) => Math.min(min, task.order ?? 0), 0);
|
|
430
|
+
return minOrder - 1;
|
|
431
|
+
}
|
|
432
|
+
return boardTasks.reduce((max, task) => Math.max(max, task.order ?? -1), -1) + 1;
|
|
433
|
+
}
|
|
434
|
+
export function nextOrderForCalendarBoard(boardId, events, newItemPosition) {
|
|
435
|
+
const boardEvents = events.filter((event) => event.boardId === boardId && !event.external);
|
|
436
|
+
if (newItemPosition === "top") {
|
|
437
|
+
const minOrder = boardEvents.reduce((min, event) => Math.min(min, event.order ?? 0), 0);
|
|
438
|
+
return minOrder - 1;
|
|
439
|
+
}
|
|
440
|
+
return boardEvents.reduce((max, event) => Math.max(max, event.order ?? -1), -1) + 1;
|
|
441
|
+
}
|