Let a conversation's menu show the routines that run in it

The header menu had Tasks but nothing for the schedules whose results
land in that conversation. It now has Schedules, right under Tasks,
opened in a modal that lists only what runs there: a bot's own chat holds
the routines that run nowhere, a workspace only that bot's routines in
it (`routineScopeSql`, tested against SQLite).

The list loads when the modal opens, through
`api/admin/bots/list-routines`, which checks the user chats in that
conversation first, so the page props stay small. `grabCronJobs` gained
an optional `workspace_id`: left out it still means everywhere.

The routines page's row, form and edit modal moved to
`src/components/general/`, and its subtitle to `src/utils/`, since both
the page and the modal use them.
This commit is contained in:
2026-10-23 07:23:15 +01:00
parent 796c53716f
commit 5beb5f3de4
17 changed files with 566 additions and 18 deletions
+81
View File
@@ -0,0 +1,81 @@
import { useState } from "react";
import Form from "@moduletrace/twui/form/Form";
import Input from "@moduletrace/twui/form/Input";
import CodeEditor from "@/src/components/general/code-editor";
import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api";
import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions";
import type { CronJobWithNames } from "@/src/types";
type Props = {
cron_job: CronJobWithNames;
onCancel: () => void;
onSaved: (params: { cron_job: CronJobWithNames }) => void;
};
/**
* A routine's name, schedule (a cron expression in UTC) and brief. The
* routines page and the conversation's routines modal both edit with it.
*/
export default function RoutineForm({ cron_job, onCancel, onSaved }: Props) {
const [title, setTitle] = useState(cron_job.title || "");
const [schedule, setSchedule] = useState(cron_job.schedule || "");
const [instructions, setInstructions] = useState(cron_job.instructions || "");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const submit = async () => {
if (loading) return;
if (!window.confirm(`Save ${title || "this routine"}?`)) return;
setLoading(true);
setError(undefined);
const res = await postAdminApi<CronJobWithNames>({
path: "routines/update-routine",
body: { cron_job_id: cron_job.id, title, schedule, instructions },
});
setLoading(false);
if (!res.success || !res.singleRes) {
setError(res.msg || "Couldn't save the routine.");
return;
}
onSaved({ cron_job: res.singleRes });
};
return (
<Form className="w-full gap-4! items-stretch pt-4" onSubmit={submit}>
<Input
id="routine-title"
title="Name"
label="Name"
showLabel
autoComplete="off"
defaultValue={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Input
id="routine-schedule"
title="Schedule"
label="Schedule"
showLabel
autoComplete="off"
placeholder="0 9 * * MON-FRI"
defaultValue={schedule}
info="Cron, in UTC: minute hour day month weekday, or @hourly, @daily, @weekly."
onChange={(e) => setSchedule(e.target.value)}
/>
<CodeEditor
mode="markdown"
className="h-40"
title="Brief"
content={instructions}
info="What the bot does at every run, and how it knows it's done."
onChange={(value) => setInstructions(value)}
/>
<AddFormActions submit_label="Save" loading={loading} error={error} onCancel={onCancel} />
</Form>
);
}