1import Node from "../../db/models/node.js";
2import { logContribution } from "../../db/utils.js";
3import { useEnergy } from "../../core/tree/energy.js";
4
5async function updateSchedule({
6 nodeId,
7 versionIndex,
8 newSchedule,
9 reeffectTime,
10 userId,
11 wasAi = false,
12 aiChatId = null,
13 sessionId = null,
14}) {
15 if (!nodeId || versionIndex === undefined || reeffectTime === undefined) {
16 const error = new Error(
17 "nodeId, versionIndex, and reeffectTime are required.",
18 );
19 error.status = 400;
20 throw error;
21 }
22
23 if (reeffectTime > 1000000) {
24 const error = new Error("reeffect time must be below 1,000,000 hrs");
25 error.status = 400;
26 throw error;
27 }
28
29 const node = await Node.findById(nodeId);
30 if (!node) {
31 const error = new Error("Node not found.");
32 error.status = 404;
33 throw error;
34 }
35 if (node.isSystem) throw new Error("Cannot modify system nodes");
36
37 if (versionIndex < 0 || versionIndex >= node.versions.length) {
38 const error = new Error("Invalid version index.");
39 error.status = 400;
40 throw error;
41 }
42
43 let formattedDate = null;
44
45 if (newSchedule !== undefined && newSchedule !== "" && newSchedule !== null) {
46 formattedDate = new Date(newSchedule);
47 if (isNaN(formattedDate)) {
48 const error = new Error("Invalid schedule date.");
49 error.status = 400;
50 throw error;
51 }
52 }
53 const { energyUsed } = await useEnergy({
54 userId,
55 action: "editSchedule",
56 });
57 node.versions[versionIndex].schedule = formattedDate;
58
59 node.versions[versionIndex].schedule = formattedDate;
60
61 node.versions[versionIndex].reeffectTime = reeffectTime;
62
63 await node.save();
64
65 const scheduleEdited = { date: formattedDate, reeffectTime };
66
67 await logContribution({
68 userId,
69 nodeId,
70 wasAi,
71 aiChatId,
72 sessionId,
73 action: "editSchedule",
74 nodeVersion: versionIndex,
75 scheduleEdited,
76 energyUsed,
77 });
78
79 return {
80 message: "Schedule and re-effect time updated successfully.",
81 node,
82 };
83}
84
85async function getCalendar({ rootNodeId, startDate, endDate }) {
86 if (!rootNodeId) {
87 const error = new Error("rootNodeId is required");
88 error.status = 400;
89 throw error;
90 }
91
92 const start = startDate ? new Date(startDate) : null;
93 const end = endDate ? new Date(endDate) : null;
94
95 if (start && isNaN(start)) {
96 const error = new Error("Invalid startDate");
97 error.status = 400;
98 throw error;
99 }
100
101 if (end && isNaN(end)) {
102 const error = new Error("Invalid endDate");
103 error.status = 400;
104 throw error;
105 }
106
107 const results = [];
108 const visited = new Set();
109
110 async function walk(nodeId) {
111 if (!nodeId || visited.has(nodeId)) return;
112 visited.add(nodeId);
113
114 const node = await Node.findById(nodeId)
115 .select("name prestige versions children")
116 .lean();
117
118 if (!node) return;
119
120 const versionIndex = node.prestige;
121 const version = node.versions?.[versionIndex];
122
123 if (version?.schedule) {
124 const scheduleDate = new Date(version.schedule);
125
126 const inRange =
127 (!start || scheduleDate >= start) && (!end || scheduleDate <= end);
128
129 if (inRange) {
130 results.push({
131 nodeId: node._id.toString(),
132 name: node.name ?? "(Untitled)",
133 versionIndex,
134 schedule: scheduleDate,
135 reeffectTime: version.reeffectTime ?? null,
136 });
137 }
138 }
139
140 if (Array.isArray(node.children)) {
141 for (const childId of node.children) {
142 await walk(childId);
143 }
144 }
145 }
146
147 await walk(rootNodeId);
148
149 return results;
150}
151
152export { updateSchedule, getCalendar };
153
1import router from "./routes.js";
2
3export async function init(core) {
4 return { router };
5}
6
1export default {
2 name: "schedules",
3 version: "1.0.0",
4 description: "Cron and ISO date scheduling for node versions",
5
6 needs: {
7 models: ["Node"],
8 },
9
10 optional: {
11 services: ["energy"],
12 },
13
14 provides: {
15 models: {},
16 routes: "./routes.js",
17 tools: false,
18 jobs: false,
19 orchestrator: false,
20 energyActions: {
21 editSchedule: { cost: 1 },
22 },
23 sessionTypes: {},
24 cli: [
25 { command: "schedule <date>", description: "Set schedule on current node", method: "POST", endpoint: "/node/:nodeId/editSchedule" },
26 { command: "calendar", description: "Show scheduled nodes for current tree", method: "GET", endpoint: "/root/:rootId/calendar" },
27 ],
28 },
29};
30
1import express from "express";
2import authenticate from "../../middleware/authenticate.js";
3import { updateSchedule } from "./core.js";
4import Node from "../../db/models/node.js";
5
6const router = express.Router();
7
8async function useLatest(req, res, next) {
9 try {
10 const node = await Node.findById(req.params.nodeId).select("prestige").lean();
11 if (!node) return res.status(404).json({ error: "Node not found" });
12 req.params.version = String(node.prestige);
13 next();
14 } catch (err) {
15 res.status(500).json({ error: err.message });
16 }
17}
18
19const editScheduleHandler = async (req, res) => {
20 try {
21 const { nodeId, version } = req.params;
22 const userId = req.userId;
23
24 const newSchedule = req.body?.newSchedule || req.query?.newSchedule;
25 const reeffectTime = req.body?.reeffectTime ?? req.query?.reeffectTime;
26
27 if (reeffectTime === undefined) {
28 return res.status(400).json({
29 error: "reeffectTime is required",
30 });
31 }
32
33 const result = await updateSchedule({
34 nodeId,
35 versionIndex: Number(version),
36 newSchedule,
37 reeffectTime: Number(reeffectTime),
38 userId,
39 });
40
41 if ("html" in req.query) {
42 return res.redirect(
43 `/api/v1/node/${nodeId}/${version}?token=${req.query.token ?? ""}&html`,
44 );
45 }
46
47 res.json({ success: true, ...result });
48 } catch (err) {
49 console.error("editSchedule error:", err);
50 res.status(err.status || 400).json({ error: err.message });
51 }
52};
53
54router.post("/node/:nodeId/editSchedule", authenticate, useLatest, editScheduleHandler);
55router.post("/node/:nodeId/:version/editSchedule", authenticate, editScheduleHandler);
56
57export default router;
58