Scheduled: the server's real /api/v2 routes and shapes, edit, settings, results, models
Re-wired to api/v2/schedules.py and its contract fixtures: description and
next_run(s), pause/resume verbs, runs paged with `before`, a run's result
(GET runs/{id}, Markdown) unfolded under its row, run.model with the switch from
a busy one, waiting and its retry, timing (late, caught up, by hand). The form
serves new and edit: words -> compile -> title, instruction and next runs; the
rule set by hand (days and a time, read back by preview) when the words give
none; notify policy, fresh or continue, model from the scheduling models,
effort and team size. Models on the list: the person's narrowing (PUT
/schedules/models), each free or busy. Live from the `schedules` event.
i18n: 39 new sentences x 8 languages (312 in all); CHANGELOG [Unreleased].
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
6 files changed
+1179
-177
No files matched your search
+18
-7
@@ -47,13 +47,24 @@ All notable changes to LLeMbas Desktop. The format follows
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Scheduled tasks** (with a server that has them): the list with each task's next run, how its
|
- **Scheduled tasks** (with a server that has them, LLeMbas 2.0.0-beta.6 or later): the list
|
||||||
last run went and a switch to pause it; a task's page with what it does, when, how it tells you,
|
with each task's next run, how its last run went and a switch to pause it, kept up to date as
|
||||||
the next runs, its history (each run's status, time, model and its result to open) and **Run
|
the server runs them; a task's page with what it does, when (and in which time zone), its
|
||||||
now**; and a new task said in your own words -- the server reads them into a rule and an
|
model, effort, team and how it tells you, the next runs, and its history -- each run's status,
|
||||||
instruction and shows the next five runs before you save. While the app runs, a scheduled task's
|
time, whether it was late, caught up or run by hand, the model it ran on (and that it switched
|
||||||
result the server decides to tell of comes as a system notification (never a run with nothing
|
from a busy one), and its result to unfold in place, with its chat to open; earlier runs on
|
||||||
to report). A server without scheduled tasks says so.
|
request. **Run now**, **Edit**, pause and resume, and delete. A new task is said in your own
|
||||||
|
words: the server reads them into a rule, a title and an instruction and shows the next five
|
||||||
|
runs before you save; the title and instruction can be changed, and where the words give no
|
||||||
|
time the days and the time can be set by hand. Each task's settings: when to tell you
|
||||||
|
(*when it matters*, *always*, *never*), a new chat for each run or one that grows, its model
|
||||||
|
(or the first free one of the scheduling models), its effort and its team. **Models** on the
|
||||||
|
list chooses which models scheduled tasks may run on, each shown free or busy.
|
||||||
|
- **A scheduled task's result comes as a system notification** while the app runs, by the
|
||||||
|
task's own setting: a result, never a run with nothing to report unless the task says
|
||||||
|
*always*, nothing for *never*; a failure the first time, and on the third in a row the
|
||||||
|
question whether to pause it. The app follows the server's event stream for this rather than
|
||||||
|
asking every few seconds.
|
||||||
|
|
||||||
## [1.0.0-beta.3] — 2026-11-09
|
## [1.0.0-beta.3] — 2026-11-09
|
||||||
|
|
||||||
|
|||||||
+312
-9
@@ -312,13 +312,52 @@ KEYS = (
|
|||||||
"It has not run yet.",
|
"It has not run yet.",
|
||||||
"late",
|
"late",
|
||||||
"caught up",
|
"caught up",
|
||||||
"Open the result",
|
|
||||||
"Delete this scheduled task?",
|
"Delete this scheduled task?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list",
|
"Every weekday at 8:00, summarise the news on my reading list",
|
||||||
"What should happen, and when",
|
"What should happen, and when",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.",
|
||||||
"These words give no run in the future.",
|
"These words give no run in the future.",
|
||||||
|
"%(model)s was busy, so it ran on this one.",
|
||||||
|
"%(n)s missed",
|
||||||
|
"A busy model is waited for, then the next free one is used.",
|
||||||
|
"A result, but not a run with nothing to report.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.",
|
||||||
|
"A team of up to %(n)s",
|
||||||
|
"A team works on it together, its own agent included.",
|
||||||
|
"Choose a model to choose its effort.",
|
||||||
|
"Due %(when)s",
|
||||||
|
"Each run sees the ones before it.",
|
||||||
|
"Each run starts with nothing but the instruction.",
|
||||||
|
"Earlier runs",
|
||||||
|
"Edit %(title)s",
|
||||||
|
"Every result, even with nothing to report.",
|
||||||
|
"Models for scheduled tasks",
|
||||||
|
"No team",
|
||||||
|
"Only in its history.",
|
||||||
|
"Open the chat",
|
||||||
|
"Result",
|
||||||
|
"The first free scheduling model",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.",
|
||||||
|
"The last %(n)s runs failed: %(why)s",
|
||||||
|
"The result is no longer there.",
|
||||||
|
"Times in %(zone)s",
|
||||||
|
"Title",
|
||||||
|
"Written to %(path)s",
|
||||||
|
"busy",
|
||||||
|
"free",
|
||||||
|
"switched",
|
||||||
|
"tried again %(when)s",
|
||||||
|
"Set the time yourself",
|
||||||
|
"Days",
|
||||||
|
"Time",
|
||||||
|
"No day ticked: every day.",
|
||||||
|
"A scheduled task failed 3 times in a row. Pause it?",
|
||||||
|
"Edit",
|
||||||
|
"Models",
|
||||||
|
"Open its chat",
|
||||||
|
"The model's own",
|
||||||
|
"run by hand",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
|
# The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
|
||||||
@@ -482,13 +521,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Ještě neběžela.",
|
"It has not run yet.": "Ještě neběžela.",
|
||||||
"late": "pozdě",
|
"late": "pozdě",
|
||||||
"caught up": "dohnáno",
|
"caught up": "dohnáno",
|
||||||
"Open the result": "Otevřít výsledek",
|
|
||||||
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
|
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s“ se zastaví a její historie zmizí. Konverzace, které napsala, zůstanou.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s“ se zastaví a její historie zmizí. Konverzace, které napsala, zůstanou.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Každý pracovní den v 8:00 shrň zprávy z mého seznamu ke čtení",
|
"Every weekday at 8:00, summarise the news on my reading list": "Každý pracovní den v 8:00 shrň zprávy z mého seznamu ke čtení",
|
||||||
"What should happen, and when": "Co se má stát a kdy",
|
"What should happen, and when": "Co se má stát a kdy",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Vlastními slovy. Server z nich vytvoří pravidlo a pokyn a před uložením vám ukáže další spuštění.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Vlastními slovy. Server z nich vytvoří pravidlo a pokyn a před uložením vám ukáže další spuštění.",
|
||||||
"These words give no run in the future.": "Z těchto slov nevychází žádné budoucí spuštění.",
|
"These words give no run in the future.": "Z těchto slov nevychází žádné budoucí spuštění.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s byl zaneprázdněný, proto běžela na tomto.",
|
||||||
|
"%(n)s missed": "zmeškáno: %(n)s",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Na zaneprázdněný model se čeká, pak se použije další volný.",
|
||||||
|
"A result, but not a run with nothing to report.": "Výsledek, ale ne spuštění, které nemá co hlásit.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Úloha bez vlastního modelu běží na prvním volném z těchto, v tomto pořadí. Žádný nezaškrtnutý: všechny.",
|
||||||
|
"A team of up to %(n)s": "Tým: nejvýše %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Pracuje na ní tým společně, včetně jejího vlastního agenta.",
|
||||||
|
"Choose a model to choose its effort.": "Vyberte model, abyste mohli zvolit jeho náročnost.",
|
||||||
|
"Due %(when)s": "Naplánováno na %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Každé spuštění vidí ta předchozí.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Každé spuštění začíná jen s instrukcí.",
|
||||||
|
"Earlier runs": "Starší spuštění",
|
||||||
|
"Edit %(title)s": "Upravit %(title)s",
|
||||||
|
"Every result, even with nothing to report.": "Každý výsledek, i když není co hlásit.",
|
||||||
|
"Models for scheduled tasks": "Modely pro naplánované úlohy",
|
||||||
|
"No team": "Bez týmu",
|
||||||
|
"Only in its history.": "Jen v její historii.",
|
||||||
|
"Open the chat": "Otevřít konverzaci",
|
||||||
|
"Result": "Výsledek",
|
||||||
|
"The first free scheduling model": "První volný model pro plánování",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "Instrukce, kterou dostane každé spuštění samostatně: uveďte vše, co potřebuje.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Posledních %(n)s spuštění selhalo: %(why)s",
|
||||||
|
"The result is no longer there.": "Výsledek už tu není.",
|
||||||
|
"Times in %(zone)s": "Časové pásmo: %(zone)s",
|
||||||
|
"Title": "Název",
|
||||||
|
"Written to %(path)s": "Zapsáno do %(path)s",
|
||||||
|
"busy": "zaneprázdněný",
|
||||||
|
"free": "volný",
|
||||||
|
"switched": "přepnuto",
|
||||||
|
"tried again %(when)s": "znovu %(when)s",
|
||||||
|
"Set the time yourself": "Nastavit čas sami",
|
||||||
|
"Days": "Dny",
|
||||||
|
"Time": "Čas",
|
||||||
|
"No day ticked: every day.": "Žádný den nezaškrtnutý: každý den.",
|
||||||
},
|
},
|
||||||
"de": {
|
"de": {
|
||||||
"Menu": "Menü",
|
"Menu": "Menü",
|
||||||
@@ -647,13 +719,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Sie ist noch nicht gelaufen.",
|
"It has not run yet.": "Sie ist noch nicht gelaufen.",
|
||||||
"late": "verspätet",
|
"late": "verspätet",
|
||||||
"caught up": "nachgeholt",
|
"caught up": "nachgeholt",
|
||||||
"Open the result": "Ergebnis öffnen",
|
|
||||||
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
|
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s“ hört auf, und ihr Verlauf wird gelöscht. Die Chats, die sie geschrieben hat, bleiben.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s“ hört auf, und ihr Verlauf wird gelöscht. Die Chats, die sie geschrieben hat, bleiben.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Jeden Werktag um 8:00 die Nachrichten auf meiner Leseliste zusammenfassen",
|
"Every weekday at 8:00, summarise the news on my reading list": "Jeden Werktag um 8:00 die Nachrichten auf meiner Leseliste zusammenfassen",
|
||||||
"What should happen, and when": "Was soll wann geschehen?",
|
"What should happen, and when": "Was soll wann geschehen?",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "In Ihren eigenen Worten. Der Server macht daraus eine Regel und eine Anweisung und zeigt Ihnen die nächsten Läufe, bevor etwas gespeichert wird.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "In Ihren eigenen Worten. Der Server macht daraus eine Regel und eine Anweisung und zeigt Ihnen die nächsten Läufe, bevor etwas gespeichert wird.",
|
||||||
"These words give no run in the future.": "Diese Worte ergeben keinen künftigen Lauf.",
|
"These words give no run in the future.": "Diese Worte ergeben keinen künftigen Lauf.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s war belegt, daher lief sie auf diesem.",
|
||||||
|
"%(n)s missed": "%(n)s verpasst",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Auf ein belegtes Modell wird gewartet, dann wird das nächste freie genommen.",
|
||||||
|
"A result, but not a run with nothing to report.": "Ein Ergebnis, aber kein Lauf ohne Neuigkeiten.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Eine Aufgabe ohne eigenes Modell läuft auf dem ersten freien davon, in dieser Reihenfolge. Keines angehakt: alle.",
|
||||||
|
"A team of up to %(n)s": "Ein Team mit bis zu %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Ein Team arbeitet gemeinsam daran, ihr eigener Agent eingeschlossen.",
|
||||||
|
"Choose a model to choose its effort.": "Wählen Sie ein Modell, um seinen Aufwand zu wählen.",
|
||||||
|
"Due %(when)s": "Fällig %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Jeder Lauf beginnt nur mit der Anweisung.",
|
||||||
|
"Earlier runs": "Frühere Läufe",
|
||||||
|
"Edit %(title)s": "%(title)s bearbeiten",
|
||||||
|
"Every result, even with nothing to report.": "Jedes Ergebnis, auch ohne Neuigkeiten.",
|
||||||
|
"Models for scheduled tasks": "Modelle für geplante Aufgaben",
|
||||||
|
"No team": "Kein Team",
|
||||||
|
"Only in its history.": "Nur in ihrem Verlauf.",
|
||||||
|
"Open the chat": "Chat öffnen",
|
||||||
|
"Result": "Ergebnis",
|
||||||
|
"The first free scheduling model": "Das erste freie Planungsmodell",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "Die Anweisung, die jeder Lauf für sich erhält: Sagen Sie alles, was er braucht.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Die letzten %(n)s Läufe sind fehlgeschlagen: %(why)s",
|
||||||
|
"The result is no longer there.": "Das Ergebnis ist nicht mehr da.",
|
||||||
|
"Times in %(zone)s": "Zeitzone: %(zone)s",
|
||||||
|
"Title": "Titel",
|
||||||
|
"Written to %(path)s": "Geschrieben nach %(path)s",
|
||||||
|
"busy": "belegt",
|
||||||
|
"free": "frei",
|
||||||
|
"switched": "gewechselt",
|
||||||
|
"tried again %(when)s": "erneut %(when)s",
|
||||||
|
"Set the time yourself": "Zeit selbst festlegen",
|
||||||
|
"Days": "Tage",
|
||||||
|
"Time": "Uhrzeit",
|
||||||
|
"No day ticked: every day.": "Kein Tag angehakt: jeden Tag.",
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"Menu": "Menú",
|
"Menu": "Menú",
|
||||||
@@ -812,13 +917,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Aún no se ha ejecutado.",
|
"It has not run yet.": "Aún no se ha ejecutado.",
|
||||||
"late": "con retraso",
|
"late": "con retraso",
|
||||||
"caught up": "recuperada",
|
"caught up": "recuperada",
|
||||||
"Open the result": "Abrir el resultado",
|
|
||||||
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
|
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "«%(title)s» se detiene y su historial se borra. Los chats que escribió se conservan.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "«%(title)s» se detiene y su historial se borra. Los chats que escribió se conservan.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Cada día laborable a las 8:00, resume las noticias de mi lista de lectura",
|
"Every weekday at 8:00, summarise the news on my reading list": "Cada día laborable a las 8:00, resume las noticias de mi lista de lectura",
|
||||||
"What should happen, and when": "Qué debe ocurrir, y cuándo",
|
"What should happen, and when": "Qué debe ocurrir, y cuándo",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Con sus propias palabras. El servidor las convierte en una regla y una instrucción, y le muestra las próximas ejecuciones antes de guardar nada.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Con sus propias palabras. El servidor las convierte en una regla y una instrucción, y le muestra las próximas ejecuciones antes de guardar nada.",
|
||||||
"These words give no run in the future.": "Estas palabras no dan ninguna ejecución futura.",
|
"These words give no run in the future.": "Estas palabras no dan ninguna ejecución futura.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s estaba ocupado, así que se ejecutó en este.",
|
||||||
|
"%(n)s missed": "%(n)s perdidas",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Se espera a un modelo ocupado y luego se usa el siguiente libre.",
|
||||||
|
"A result, but not a run with nothing to report.": "Un resultado, pero no una ejecución sin nada que informar.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Una tarea sin modelo propio se ejecuta en el primero libre de estos, en este orden. Ninguno marcado: todos.",
|
||||||
|
"A team of up to %(n)s": "Un equipo de hasta %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Un equipo trabaja en ella, incluido su propio agente.",
|
||||||
|
"Choose a model to choose its effort.": "Elija un modelo para elegir su esfuerzo.",
|
||||||
|
"Due %(when)s": "Prevista %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Cada ejecución ve las anteriores.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Cada ejecución empieza solo con la instrucción.",
|
||||||
|
"Earlier runs": "Ejecuciones anteriores",
|
||||||
|
"Edit %(title)s": "Editar %(title)s",
|
||||||
|
"Every result, even with nothing to report.": "Cada resultado, incluso sin nada que informar.",
|
||||||
|
"Models for scheduled tasks": "Modelos para tareas programadas",
|
||||||
|
"No team": "Sin equipo",
|
||||||
|
"Only in its history.": "Solo en su historial.",
|
||||||
|
"Open the chat": "Abrir el chat",
|
||||||
|
"Result": "Resultado",
|
||||||
|
"The first free scheduling model": "El primer modelo de programación libre",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "La instrucción que recibe cada ejecución por sí sola: diga todo lo que necesita.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Las últimas %(n)s ejecuciones fallaron: %(why)s",
|
||||||
|
"The result is no longer there.": "El resultado ya no está.",
|
||||||
|
"Times in %(zone)s": "Zona horaria: %(zone)s",
|
||||||
|
"Title": "Título",
|
||||||
|
"Written to %(path)s": "Escrito en %(path)s",
|
||||||
|
"busy": "ocupado",
|
||||||
|
"free": "libre",
|
||||||
|
"switched": "cambiado",
|
||||||
|
"tried again %(when)s": "de nuevo %(when)s",
|
||||||
|
"Set the time yourself": "Fijar la hora usted mismo",
|
||||||
|
"Days": "Días",
|
||||||
|
"Time": "Hora",
|
||||||
|
"No day ticked: every day.": "Ningún día marcado: todos los días.",
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
@@ -977,13 +1115,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Elle n’a pas encore été exécutée.",
|
"It has not run yet.": "Elle n’a pas encore été exécutée.",
|
||||||
"late": "en retard",
|
"late": "en retard",
|
||||||
"caught up": "rattrapée",
|
"caught up": "rattrapée",
|
||||||
"Open the result": "Ouvrir le résultat",
|
|
||||||
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
|
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "« %(title)s » s’arrête et son historique disparaît. Les conversations qu’elle a écrites restent.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "« %(title)s » s’arrête et son historique disparaît. Les conversations qu’elle a écrites restent.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Chaque jour ouvré à 8 h, résume les nouvelles de ma liste de lecture",
|
"Every weekday at 8:00, summarise the news on my reading list": "Chaque jour ouvré à 8 h, résume les nouvelles de ma liste de lecture",
|
||||||
"What should happen, and when": "Ce qui doit se passer, et quand",
|
"What should happen, and when": "Ce qui doit se passer, et quand",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Avec vos propres mots. Le serveur en fait une règle et une instruction, et vous montre les prochaines exécutions avant que rien ne soit enregistré.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Avec vos propres mots. Le serveur en fait une règle et une instruction, et vous montre les prochaines exécutions avant que rien ne soit enregistré.",
|
||||||
"These words give no run in the future.": "Ces mots ne donnent aucune exécution à venir.",
|
"These words give no run in the future.": "Ces mots ne donnent aucune exécution à venir.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s était occupé, elle a donc tourné sur celui-ci.",
|
||||||
|
"%(n)s missed": "%(n)s manquées",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Un modèle occupé est attendu, puis le suivant libre est utilisé.",
|
||||||
|
"A result, but not a run with nothing to report.": "Un résultat, mais pas une exécution sans rien à signaler.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Une tâche sans modèle à elle tourne sur le premier libre de ceux-ci, dans cet ordre. Aucun coché : tous.",
|
||||||
|
"A team of up to %(n)s": "Une équipe jusqu'à %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Une équipe y travaille ensemble, son propre agent compris.",
|
||||||
|
"Choose a model to choose its effort.": "Choisissez un modèle pour choisir son effort.",
|
||||||
|
"Due %(when)s": "Prévue %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Chaque exécution voit les précédentes.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Chaque exécution part de la seule instruction.",
|
||||||
|
"Earlier runs": "Exécutions précédentes",
|
||||||
|
"Edit %(title)s": "Modifier %(title)s",
|
||||||
|
"Every result, even with nothing to report.": "Chaque résultat, même sans rien à signaler.",
|
||||||
|
"Models for scheduled tasks": "Modèles pour les tâches planifiées",
|
||||||
|
"No team": "Pas d'équipe",
|
||||||
|
"Only in its history.": "Seulement dans son historique.",
|
||||||
|
"Open the chat": "Ouvrir la conversation",
|
||||||
|
"Result": "Résultat",
|
||||||
|
"The first free scheduling model": "Le premier modèle de planification libre",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "L'instruction donnée à chaque exécution, seule : dites tout ce dont elle a besoin.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Les %(n)s dernières exécutions ont échoué : %(why)s",
|
||||||
|
"The result is no longer there.": "Le résultat n'est plus là.",
|
||||||
|
"Times in %(zone)s": "Fuseau horaire : %(zone)s",
|
||||||
|
"Title": "Titre",
|
||||||
|
"Written to %(path)s": "Écrit dans %(path)s",
|
||||||
|
"busy": "occupé",
|
||||||
|
"free": "libre",
|
||||||
|
"switched": "changé",
|
||||||
|
"tried again %(when)s": "de nouveau %(when)s",
|
||||||
|
"Set the time yourself": "Régler l'heure vous-même",
|
||||||
|
"Days": "Jours",
|
||||||
|
"Time": "Heure",
|
||||||
|
"No day ticked: every day.": "Aucun jour coché : tous les jours.",
|
||||||
},
|
},
|
||||||
"hu": {
|
"hu": {
|
||||||
"Menu": "Menü",
|
"Menu": "Menü",
|
||||||
@@ -1142,13 +1313,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Még nem futott.",
|
"It has not run yet.": "Még nem futott.",
|
||||||
"late": "késve",
|
"late": "késve",
|
||||||
"caught up": "pótolva",
|
"caught up": "pótolva",
|
||||||
"Open the result": "Az eredmény megnyitása",
|
|
||||||
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
|
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s” leáll, és az előzményei törlődnek. Az általa írt beszélgetések megmaradnak.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s” leáll, és az előzményei törlődnek. Az általa írt beszélgetések megmaradnak.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Minden hétköznap 8:00-kor foglald össze az olvasólistám híreit",
|
"Every weekday at 8:00, summarise the news on my reading list": "Minden hétköznap 8:00-kor foglald össze az olvasólistám híreit",
|
||||||
"What should happen, and when": "Mi történjen, és mikor",
|
"What should happen, and when": "Mi történjen, és mikor",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "A saját szavaival. A szerver szabályt és utasítást készít belőlük, és mentés előtt megmutatja a következő futásokat.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "A saját szavaival. A szerver szabályt és utasítást készít belőlük, és mentés előtt megmutatja a következő futásokat.",
|
||||||
"These words give no run in the future.": "Ezekből a szavakból nem következik jövőbeli futás.",
|
"These words give no run in the future.": "Ezekből a szavakból nem következik jövőbeli futás.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s foglalt volt, ezért ezen futott.",
|
||||||
|
"%(n)s missed": "%(n)s kimaradt",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "A foglalt modellre vár, majd a következő szabadot használja.",
|
||||||
|
"A result, but not a run with nothing to report.": "Eredmény, de nem olyan futás, amelynek nincs mit jelentenie.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "A saját modell nélküli feladat ezek közül az első szabadon fut, ebben a sorrendben. Ha egy sincs bejelölve: mindegyiken.",
|
||||||
|
"A team of up to %(n)s": "Legfeljebb %(n)s fős csapat",
|
||||||
|
"A team works on it together, its own agent included.": "Egy csapat dolgozik rajta együtt, a saját ügynökét is beleértve.",
|
||||||
|
"Choose a model to choose its effort.": "Válasszon modellt az erőfeszítés megadásához.",
|
||||||
|
"Due %(when)s": "Esedékes: %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Minden futás látja az előzőeket.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Minden futás csak az utasítással indul.",
|
||||||
|
"Earlier runs": "Korábbi futások",
|
||||||
|
"Edit %(title)s": "%(title)s szerkesztése",
|
||||||
|
"Every result, even with nothing to report.": "Minden eredmény, akkor is, ha nincs mit jelenteni.",
|
||||||
|
"Models for scheduled tasks": "Modellek az ütemezett feladatokhoz",
|
||||||
|
"No team": "Nincs csapat",
|
||||||
|
"Only in its history.": "Csak az előzményeiben.",
|
||||||
|
"Open the chat": "A csevegés megnyitása",
|
||||||
|
"Result": "Eredmény",
|
||||||
|
"The first free scheduling model": "Az első szabad ütemezési modell",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "Az utasítás, amelyet minden futás önmagában kap: mondjon el mindent, amire szüksége van.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Az utolsó %(n)s futás sikertelen: %(why)s",
|
||||||
|
"The result is no longer there.": "Az eredmény már nincs meg.",
|
||||||
|
"Times in %(zone)s": "Időzóna: %(zone)s",
|
||||||
|
"Title": "Cím",
|
||||||
|
"Written to %(path)s": "Kiírva ide: %(path)s",
|
||||||
|
"busy": "foglalt",
|
||||||
|
"free": "szabad",
|
||||||
|
"switched": "váltva",
|
||||||
|
"tried again %(when)s": "újra %(when)s",
|
||||||
|
"Set the time yourself": "Az időpont megadása kézzel",
|
||||||
|
"Days": "Napok",
|
||||||
|
"Time": "Időpont",
|
||||||
|
"No day ticked: every day.": "Ha egy nap sincs bejelölve: minden nap.",
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
@@ -1307,13 +1511,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Non è ancora stata eseguita.",
|
"It has not run yet.": "Non è ancora stata eseguita.",
|
||||||
"late": "in ritardo",
|
"late": "in ritardo",
|
||||||
"caught up": "recuperata",
|
"caught up": "recuperata",
|
||||||
"Open the result": "Apri il risultato",
|
|
||||||
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
|
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "«%(title)s» si ferma e la sua cronologia viene eliminata. Le chat che ha scritto restano.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "«%(title)s» si ferma e la sua cronologia viene eliminata. Le chat che ha scritto restano.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Ogni giorno feriale alle 8:00, riassumi le notizie della mia lista di lettura",
|
"Every weekday at 8:00, summarise the news on my reading list": "Ogni giorno feriale alle 8:00, riassumi le notizie della mia lista di lettura",
|
||||||
"What should happen, and when": "Cosa deve succedere, e quando",
|
"What should happen, and when": "Cosa deve succedere, e quando",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Con parole tue. Il server ne ricava una regola e un’istruzione e ti mostra le prossime esecuzioni prima di salvare.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Con parole tue. Il server ne ricava una regola e un’istruzione e ti mostra le prossime esecuzioni prima di salvare.",
|
||||||
"These words give no run in the future.": "Queste parole non danno nessuna esecuzione futura.",
|
"These words give no run in the future.": "Queste parole non danno nessuna esecuzione futura.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s era occupato, quindi è stata eseguita su questo.",
|
||||||
|
"%(n)s missed": "%(n)s perse",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Si attende un modello occupato, poi si usa il successivo libero.",
|
||||||
|
"A result, but not a run with nothing to report.": "Un risultato, ma non un’esecuzione senza nulla da segnalare.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Un’attività senza un modello proprio gira sul primo libero di questi, in quest’ordine. Nessuno spuntato: tutti.",
|
||||||
|
"A team of up to %(n)s": "Un team fino a %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Un team ci lavora insieme, incluso il suo agente.",
|
||||||
|
"Choose a model to choose its effort.": "Scegli un modello per sceglierne l’impegno.",
|
||||||
|
"Due %(when)s": "Prevista %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Ogni esecuzione parte solo dall’istruzione.",
|
||||||
|
"Earlier runs": "Esecuzioni precedenti",
|
||||||
|
"Edit %(title)s": "Modifica %(title)s",
|
||||||
|
"Every result, even with nothing to report.": "Ogni risultato, anche senza nulla da segnalare.",
|
||||||
|
"Models for scheduled tasks": "Modelli per le attività pianificate",
|
||||||
|
"No team": "Nessun team",
|
||||||
|
"Only in its history.": "Solo nella sua cronologia.",
|
||||||
|
"Open the chat": "Apri la chat",
|
||||||
|
"Result": "Risultato",
|
||||||
|
"The first free scheduling model": "Il primo modello di pianificazione libero",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "L’istruzione che ogni esecuzione riceve da sola: di’ tutto ciò che le serve.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Le ultime %(n)s esecuzioni non sono riuscite: %(why)s",
|
||||||
|
"The result is no longer there.": "Il risultato non c’è più.",
|
||||||
|
"Times in %(zone)s": "Fuso orario: %(zone)s",
|
||||||
|
"Title": "Titolo",
|
||||||
|
"Written to %(path)s": "Scritto in %(path)s",
|
||||||
|
"busy": "occupato",
|
||||||
|
"free": "libero",
|
||||||
|
"switched": "cambiato",
|
||||||
|
"tried again %(when)s": "di nuovo %(when)s",
|
||||||
|
"Set the time yourself": "Imposta tu l’orario",
|
||||||
|
"Days": "Giorni",
|
||||||
|
"Time": "Ora",
|
||||||
|
"No day ticked: every day.": "Nessun giorno spuntato: ogni giorno.",
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
@@ -1472,13 +1709,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Jeszcze nie było uruchomione.",
|
"It has not run yet.": "Jeszcze nie było uruchomione.",
|
||||||
"late": "z opóźnieniem",
|
"late": "z opóźnieniem",
|
||||||
"caught up": "nadrobione",
|
"caught up": "nadrobione",
|
||||||
"Open the result": "Otwórz wynik",
|
|
||||||
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
|
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s” zatrzyma się, a jego historia zniknie. Czaty, które napisało, zostaną.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s” zatrzyma się, a jego historia zniknie. Czaty, które napisało, zostaną.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "W każdy dzień roboczy o 8:00 streść wiadomości z mojej listy do czytania",
|
"Every weekday at 8:00, summarise the news on my reading list": "W każdy dzień roboczy o 8:00 streść wiadomości z mojej listy do czytania",
|
||||||
"What should happen, and when": "Co ma się stać i kiedy",
|
"What should happen, and when": "Co ma się stać i kiedy",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Własnymi słowami. Serwer zamieni je w regułę i polecenie i pokaże następne uruchomienia, zanim cokolwiek zostanie zapisane.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Własnymi słowami. Serwer zamieni je w regułę i polecenie i pokaże następne uruchomienia, zanim cokolwiek zostanie zapisane.",
|
||||||
"These words give no run in the future.": "Z tych słów nie wynika żadne przyszłe uruchomienie.",
|
"These words give no run in the future.": "Z tych słów nie wynika żadne przyszłe uruchomienie.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s był zajęty, więc uruchomiono je na tym.",
|
||||||
|
"%(n)s missed": "pominięte: %(n)s",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Na zajęty model się czeka, potem używany jest następny wolny.",
|
||||||
|
"A result, but not a run with nothing to report.": "Wynik, ale nie uruchomienie bez niczego do zgłoszenia.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Zadanie bez własnego modelu działa na pierwszym wolnym z tych, w tej kolejności. Żaden niezaznaczony: wszystkie.",
|
||||||
|
"A team of up to %(n)s": "Zespół do %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Pracuje nad nim zespół, wraz z jego własnym agentem.",
|
||||||
|
"Choose a model to choose its effort.": "Wybierz model, aby wybrać jego wysiłek.",
|
||||||
|
"Due %(when)s": "Zaplanowane na %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Każde uruchomienie zaczyna tylko od instrukcji.",
|
||||||
|
"Earlier runs": "Wcześniejsze uruchomienia",
|
||||||
|
"Edit %(title)s": "Edytuj %(title)s",
|
||||||
|
"Every result, even with nothing to report.": "Każdy wynik, nawet bez niczego do zgłoszenia.",
|
||||||
|
"Models for scheduled tasks": "Modele dla zaplanowanych zadań",
|
||||||
|
"No team": "Bez zespołu",
|
||||||
|
"Only in its history.": "Tylko w jego historii.",
|
||||||
|
"Open the chat": "Otwórz czat",
|
||||||
|
"Result": "Wynik",
|
||||||
|
"The first free scheduling model": "Pierwszy wolny model do planowania",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "Instrukcja, którą każde uruchomienie dostaje samodzielnie: podaj wszystko, czego potrzebuje.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Ostatnie uruchomienia nie powiodły się (%(n)s): %(why)s",
|
||||||
|
"The result is no longer there.": "Wyniku już nie ma.",
|
||||||
|
"Times in %(zone)s": "Strefa czasowa: %(zone)s",
|
||||||
|
"Title": "Tytuł",
|
||||||
|
"Written to %(path)s": "Zapisano do %(path)s",
|
||||||
|
"busy": "zajęty",
|
||||||
|
"free": "wolny",
|
||||||
|
"switched": "zmieniony",
|
||||||
|
"tried again %(when)s": "ponownie %(when)s",
|
||||||
|
"Set the time yourself": "Ustaw czas samodzielnie",
|
||||||
|
"Days": "Dni",
|
||||||
|
"Time": "Godzina",
|
||||||
|
"No day ticked: every day.": "Żaden dzień niezaznaczony: codziennie.",
|
||||||
},
|
},
|
||||||
"sk": {
|
"sk": {
|
||||||
"Menu": "Ponuka",
|
"Menu": "Ponuka",
|
||||||
@@ -1637,13 +1907,46 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"It has not run yet.": "Ešte nebežala.",
|
"It has not run yet.": "Ešte nebežala.",
|
||||||
"late": "neskoro",
|
"late": "neskoro",
|
||||||
"caught up": "dobehnuté",
|
"caught up": "dobehnuté",
|
||||||
"Open the result": "Otvoriť výsledok",
|
|
||||||
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
|
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
|
||||||
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s“ sa zastaví a jej história zmizne. Konverzácie, ktoré napísala, zostanú.",
|
"“%(title)s” stops, and its history goes. The chats it wrote stay.": "„%(title)s“ sa zastaví a jej história zmizne. Konverzácie, ktoré napísala, zostanú.",
|
||||||
"Every weekday at 8:00, summarise the news on my reading list": "Každý pracovný deň o 8:00 zhrň správy z môjho zoznamu na čítanie",
|
"Every weekday at 8:00, summarise the news on my reading list": "Každý pracovný deň o 8:00 zhrň správy z môjho zoznamu na čítanie",
|
||||||
"What should happen, and when": "Čo sa má stať a kedy",
|
"What should happen, and when": "Čo sa má stať a kedy",
|
||||||
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Vlastnými slovami. Server z nich vytvorí pravidlo a pokyn a pred uložením vám ukáže ďalšie spustenia.",
|
"In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.": "Vlastnými slovami. Server z nich vytvorí pravidlo a pokyn a pred uložením vám ukáže ďalšie spustenia.",
|
||||||
"These words give no run in the future.": "Z týchto slov nevychádza žiadne budúce spustenie.",
|
"These words give no run in the future.": "Z týchto slov nevychádza žiadne budúce spustenie.",
|
||||||
|
"%(model)s was busy, so it ran on this one.": "%(model)s bol zaneprázdnený, preto bežala na tomto.",
|
||||||
|
"%(n)s missed": "zmeškané: %(n)s",
|
||||||
|
"A busy model is waited for, then the next free one is used.": "Na zaneprázdnený model sa čaká, potom sa použije ďalší voľný.",
|
||||||
|
"A result, but not a run with nothing to report.": "Výsledok, ale nie spustenie, ktoré nemá čo hlásiť.",
|
||||||
|
"A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.": "Úloha bez vlastného modelu beží na prvom voľnom z týchto, v tomto poradí. Žiadny nezaškrtnutý: všetky.",
|
||||||
|
"A team of up to %(n)s": "Tím: najviac %(n)s",
|
||||||
|
"A team works on it together, its own agent included.": "Pracuje na nej tím spoločne, vrátane jej vlastného agenta.",
|
||||||
|
"Choose a model to choose its effort.": "Vyberte model, aby ste mohli zvoliť jeho náročnosť.",
|
||||||
|
"Due %(when)s": "Naplánované na %(when)s",
|
||||||
|
"Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.",
|
||||||
|
"Each run starts with nothing but the instruction.": "Každé spustenie začína len s inštrukciou.",
|
||||||
|
"Earlier runs": "Staršie spustenia",
|
||||||
|
"Edit %(title)s": "Upraviť %(title)s",
|
||||||
|
"Every result, even with nothing to report.": "Každý výsledok, aj keď nie je čo hlásiť.",
|
||||||
|
"Models for scheduled tasks": "Modely pre naplánované úlohy",
|
||||||
|
"No team": "Bez tímu",
|
||||||
|
"Only in its history.": "Len v jej histórii.",
|
||||||
|
"Open the chat": "Otvoriť konverzáciu",
|
||||||
|
"Result": "Výsledok",
|
||||||
|
"The first free scheduling model": "Prvý voľný model na plánovanie",
|
||||||
|
"The instruction each run is given, on its own: say everything it needs.": "Inštrukcia, ktorú dostane každé spustenie samostatne: uveďte všetko, čo potrebuje.",
|
||||||
|
"The last %(n)s runs failed: %(why)s": "Posledné spustenia zlyhali (%(n)s): %(why)s",
|
||||||
|
"The result is no longer there.": "Výsledok už tu nie je.",
|
||||||
|
"Times in %(zone)s": "Časové pásmo: %(zone)s",
|
||||||
|
"Title": "Názov",
|
||||||
|
"Written to %(path)s": "Zapísané do %(path)s",
|
||||||
|
"busy": "zaneprázdnený",
|
||||||
|
"free": "voľný",
|
||||||
|
"switched": "prepnuté",
|
||||||
|
"tried again %(when)s": "znova %(when)s",
|
||||||
|
"Set the time yourself": "Nastaviť čas sami",
|
||||||
|
"Days": "Dni",
|
||||||
|
"Time": "Čas",
|
||||||
|
"No day ticked: every day.": "Žiadny deň nezaškrtnutý: každý deň.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-4
@@ -108,7 +108,7 @@ bdi { unicode-bidi: isolate; }
|
|||||||
.tool:hover, .pane__btn:hover { color: var(--ink); background: var(--surface-hover); }
|
.tool:hover, .pane__btn:hover { color: var(--ink); background: var(--surface-hover); }
|
||||||
.tool:disabled { opacity: var(--opacity-dim); cursor: default; }
|
.tool:disabled { opacity: var(--opacity-dim); cursor: default; }
|
||||||
|
|
||||||
input[type="text"], input[type="search"], input[type="number"], select, textarea {
|
input[type="text"], input[type="search"], input[type="number"], input[type="time"], select, textarea {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font: var(--text-sm) var(--font-body);
|
font: var(--text-sm) var(--font-body);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
@@ -116,7 +116,7 @@ input[type="text"], input[type="search"], input[type="number"], select, textarea
|
|||||||
border: var(--border-w) solid var(--border-strong);
|
border: var(--border-w) solid var(--border-strong);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
input[type="text"], input[type="search"], input[type="number"], select { height: var(--control-h); padding: 0 var(--control-px-sm); }
|
input[type="text"], input[type="search"], input[type="number"], input[type="time"], select { height: var(--control-h); padding: 0 var(--control-px-sm); }
|
||||||
textarea { padding: var(--sp-2) var(--control-px-sm); resize: none; line-height: var(--leading-normal); }
|
textarea { padding: var(--sp-2) var(--control-px-sm); resize: none; line-height: var(--leading-normal); }
|
||||||
:focus-visible { outline: var(--outline-w) solid var(--accent); outline-offset: var(--outline-offset); }
|
:focus-visible { outline: var(--outline-w) solid var(--accent); outline-offset: var(--outline-offset); }
|
||||||
|
|
||||||
@@ -1380,16 +1380,47 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
|
|||||||
font-size: var(--text-sm);
|
font-size: var(--text-sm);
|
||||||
border-bottom: var(--border-w) solid var(--border);
|
border-bottom: var(--border-w) solid var(--border);
|
||||||
}
|
}
|
||||||
.run__when { display: flex; gap: var(--sp-2); align-items: center; min-width: 0; }
|
.run__when { display: flex; gap: var(--sp-2); align-items: center; min-width: 0; white-space: nowrap; overflow: hidden; }
|
||||||
.run__late { font: var(--text-xs) var(--font-ui); color: var(--warning); }
|
.run__late { font: var(--text-xs) var(--font-ui); color: var(--warning); }
|
||||||
.run__model { display: inline-grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-2); align-items: center; min-width: 0; color: var(--ink-muted); }
|
.run__model { display: inline-grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--sp-2); align-items: center; min-width: 0; color: var(--ink-muted); }
|
||||||
.run__model > span:last-child { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
.run__model > span:last-child { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||||
.run__error { max-width: var(--w-field); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--warning); font-size: var(--text-xs); }
|
.run__error { max-width: var(--w-field-sm); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: var(--warning); font-size: var(--text-xs); }
|
||||||
.sched--new { gap: var(--sp-3); }
|
.sched--new { gap: var(--sp-3); }
|
||||||
.sched__words { width: 100%; font-size: var(--text-base); border-radius: var(--radius-soft); padding: var(--sp-3) var(--sp-4); }
|
.sched__words { width: 100%; font-size: var(--text-base); border-radius: var(--radius-soft); padding: var(--sp-3) var(--sp-4); }
|
||||||
.sched__preview { margin-top: var(--sp-3); }
|
.sched__preview { margin-top: var(--sp-3); }
|
||||||
.sched__card { display: grid; gap: var(--sp-4); padding: var(--sp-4); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius-soft); }
|
.sched__card { display: grid; gap: var(--sp-4); padding: var(--sp-4); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius-soft); }
|
||||||
.sched__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-body); }
|
.sched__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-body); }
|
||||||
|
/* A run's row opens its result under it, across every track. */
|
||||||
|
.run__open { justify-self: end; }
|
||||||
|
.run__switched { font: var(--text-xs) var(--font-ui); color: var(--ink-faint); }
|
||||||
|
.run__detail { grid-column: 1 / -1; display: grid; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-2); background: var(--surface); border: var(--border-w) solid var(--border); border-radius: var(--radius-soft); }
|
||||||
|
.run__detail[hidden] { display: none; }
|
||||||
|
.run__report { font: var(--text-xs) var(--font-ui); color: var(--ink-muted); overflow-wrap: anywhere; }
|
||||||
|
.sched__more { justify-self: start; }
|
||||||
|
.sched + .sched__history { margin-top: var(--sp-6); }
|
||||||
|
.sched__trouble { padding: var(--sp-3) var(--sp-4); color: var(--warning); background: var(--warning-soft); border-radius: var(--radius-soft); overflow-wrap: anywhere; }
|
||||||
|
.fact__hint { font: var(--text-xs) var(--font-ui); color: var(--ink-faint); }
|
||||||
|
/* The form: the When field's words on the control's track, one line, the time zone as its hint. */
|
||||||
|
.sched__desc { align-self: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: var(--text-sm); }
|
||||||
|
.sched__desc.is-error { color: var(--warning); }
|
||||||
|
/* The rule by hand: seven day toggles on equal tracks, and a time. */
|
||||||
|
.sched__manual[hidden] { display: none; }
|
||||||
|
.sdays { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--sp-1); }
|
||||||
|
.sday { position: relative; display: grid; place-items: center; min-width: 0; height: var(--control-h); border: var(--border-w) solid var(--border); border-radius: var(--radius-lg); cursor: pointer; font: var(--text-xs) var(--font-ui); color: var(--ink-muted); overflow: hidden; }
|
||||||
|
.sday input { position: absolute; opacity: 0; pointer-events: none; }
|
||||||
|
.sday:has(input:checked) { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); }
|
||||||
|
.sday:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
.sched__manual-open { justify-self: start; }
|
||||||
|
.sched__manual-open[hidden] { display: none; }
|
||||||
|
.sched__settings { padding-top: var(--sp-3); border-top: var(--border-w) solid var(--border); }
|
||||||
|
/* The scheduling models: a tick, the avatar, the name, free or busy -- on fixed tracks. */
|
||||||
|
.smodels { width: 100%; }
|
||||||
|
.smodels__hint { font-size: var(--text-xs); color: var(--ink-muted); }
|
||||||
|
.smodels__list { list-style: none; margin: 0; padding: 0; display: grid; }
|
||||||
|
.smodel { display: grid; grid-template-columns: auto auto minmax(0, 1fr) auto; gap: var(--sp-2); align-items: center; min-height: var(--control-h); color: var(--ink); cursor: pointer; }
|
||||||
|
.smodel__name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: var(--text-sm); }
|
||||||
|
.smodel__state { font: var(--text-xs) var(--font-ui); color: var(--warning); }
|
||||||
|
.smodel__state.is-free { color: var(--success); }
|
||||||
/* A page's way back, before its title: "Scheduled ›". */
|
/* A page's way back, before its title: "Scheduled ›". */
|
||||||
.crumb { display: inline-flex; gap: var(--sp-1); align-items: center; padding: 0; font: inherit; color: var(--ink-muted); background: none; border: 0; cursor: pointer; }
|
.crumb { display: inline-flex; gap: var(--sp-1); align-items: center; padding: 0; font: inherit; color: var(--ink-muted); background: none; border: 0; cursor: pointer; }
|
||||||
.crumb:hover { color: var(--ink); }
|
.crumb:hover { color: var(--ink); }
|
||||||
|
|||||||
+347
-35
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -332,7 +332,7 @@ async function boot() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
invoke("set_words", { words: { reply: t("The reply is ready."), agent: t("The agent has finished."), scheduled: t("A scheduled task has a result."), scheduled_failed: t("A scheduled task failed.") } })
|
invoke("set_words", { words: { reply: t("The reply is ready."), agent: t("The agent has finished."), scheduled: t("A scheduled task has a result."), scheduled_failed: t("A scheduled task failed."), scheduled_pause: t("A scheduled task failed 3 times in a row. Pause it?") } })
|
||||||
listen("status", (e) => {
|
listen("status", (e) => {
|
||||||
const before = status
|
const before = status
|
||||||
status = e.payload
|
status = e.payload
|
||||||
|
|||||||
+466
-121
@@ -1,32 +1,47 @@
|
|||||||
// Scheduled tasks: standing instructions the server runs on its own -- "every weekday at 8:00,
|
// Scheduled tasks: standing instructions the server runs on its own -- "every weekday at 8:00,
|
||||||
// summarise …". The list (each task's next run, how its last run went, a switch to pause it), a
|
// summarise …". The list (each task's next run, how its last run went, a switch to pause it), a
|
||||||
// task's page (what it does, when, how it tells, its history, Run now), and a new one said in
|
// task's page (what it does, when, its settings, the next runs, its history with each run's
|
||||||
// words: the server turns them into a rule and a standalone instruction, the next five runs are
|
// result, Run now, edit, pause, delete), and a task said in words: the server reads them into a
|
||||||
// shown, and only then is it saved. The server runs them; this page only reads and asks.
|
// rule and an instruction (compile), the next runs are shown, the settings chosen, and only then
|
||||||
|
// is it saved. The server runs them; this page only reads and asks.
|
||||||
//
|
//
|
||||||
// Its routes are the server's /api/v2 for scheduling:
|
// The server's /api/v2 for scheduling (LLeMbas `api/v2/schedules.py`; the shapes are its contract
|
||||||
|
// fixtures `schedules.json`, `schedule*.json`, `event_schedules.json`):
|
||||||
//
|
//
|
||||||
// GET /schedules { schedules: [Schedule] }
|
// GET /schedules { schedules: [schedule] }, newest first
|
||||||
// GET /schedules/{id} { schedule, runs: [Run], more }
|
// POST /schedules { title, instruction, request, rule, model, notify,
|
||||||
// POST /schedules/compile { text } -> { title, instruction, summary, rule, next: [stamp] }
|
// mode, effort, agents } -> 201 { schedule }
|
||||||
// POST /schedules { request, title, instruction, rule, model?, notify?, mode? } -> { schedule }
|
// GET /schedules/{id} { schedule }
|
||||||
// PATCH /schedules/{id} { enabled?, notify?, mode?, model? } -> { schedule }
|
// PATCH /schedules/{id} any of the create fields -> { schedule }
|
||||||
// POST /schedules/{id}/run -> { run }
|
// DELETE /schedules/{id}?keep_chat=true 204
|
||||||
// DELETE /schedules/{id}
|
// POST /schedules/{id}/pause | /resume { schedule }
|
||||||
|
// POST /schedules/{id}/run 202 { run }: now, not counted against the rule
|
||||||
|
// GET /schedules/{id}/runs?before=&limit= { runs: [run], more }, newest first
|
||||||
|
// GET /schedules/{id}/runs/{run} { run, text }: the result, Markdown (untrusted)
|
||||||
|
// POST /schedules/compile { text } -> { ok, reason, title, instruction,
|
||||||
|
// target, rule, description, next_runs, timezone }
|
||||||
|
// POST /schedules/preview { rule } -> { rule, description, next_runs, timezone }
|
||||||
|
// GET /schedules/models { models: [model + free, busy, chosen], instance, mine }
|
||||||
|
// PUT /schedules/models { models: [id] }: the person's narrowing
|
||||||
|
// GET /events `schedules` frames, { version, schedules }: followed
|
||||||
|
// by the app (Rust, watch.rs), handed here as the
|
||||||
|
// `schedules` event
|
||||||
//
|
//
|
||||||
// A server without them answers 404: the page says this server has no scheduled tasks yet.
|
// A server without them answers 404: the page says this server has no scheduled tasks yet.
|
||||||
// Everything a task says (its title, instruction, a run's error) is the person's or a model's
|
// Everything a task says (its title, instruction, a run's result or error) is the person's or a
|
||||||
// words, set as text.
|
// model's words, set as text; a result's Markdown goes through markdown.js.
|
||||||
|
|
||||||
import { $, h, clear, icon, ICONS, when } from "./dom.js"
|
import { $, h, clear, icon, ICONS, when } from "./dom.js"
|
||||||
import { get, post, patch, del, why } from "./bridge.js"
|
import { api, get, post, patch, del, why, listen } from "./bridge.js"
|
||||||
import { ask, menu } from "./popup.js"
|
import { ask, menu, popover } from "./popup.js"
|
||||||
import { avatar } from "./models.js"
|
import { avatar, levelsOf, levelWord } from "./models.js"
|
||||||
|
import { markdown } from "./markdown.js"
|
||||||
|
|
||||||
let ctx = null // { status(), models(), me(), openChat(id), show() }
|
let ctx = null // { status(), models(), me(), openChat(id), show() }
|
||||||
let view = { name: "list" } // { name: "list" } | { name: "task", id } | { name: "new" }
|
let view = { name: "list" } // { name: "list" } | { name: "task", id } | { name: "form", task? }
|
||||||
let tasks = []
|
let tasks = []
|
||||||
let missing = false // the server has no scheduling routes
|
let missing = false // the server has no scheduling routes
|
||||||
|
let shown = null // the task whose page is open: { id, last, running }
|
||||||
|
|
||||||
const STATUS = {
|
const STATUS = {
|
||||||
ok: () => t("Done"),
|
ok: () => t("Done"),
|
||||||
@@ -39,36 +54,56 @@ const STATUS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NOTIFY = {
|
const NOTIFY = {
|
||||||
always: () => t("Always"),
|
|
||||||
matters: () => t("When it matters"),
|
matters: () => t("When it matters"),
|
||||||
|
always: () => t("Always"),
|
||||||
never: () => t("Never"),
|
never: () => t("Never"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NOTIFY_HINT = {
|
||||||
|
matters: () => t("A result, but not a run with nothing to report."),
|
||||||
|
always: () => t("Every result, even with nothing to report."),
|
||||||
|
never: () => t("Only in its history."),
|
||||||
|
}
|
||||||
|
|
||||||
const MODE = {
|
const MODE = {
|
||||||
fresh: () => t("A new chat for each run"),
|
fresh: () => t("A new chat for each run"),
|
||||||
continue: () => t("One chat that grows"),
|
continue: () => t("One chat that grows"),
|
||||||
}
|
}
|
||||||
|
|
||||||
const enc = encodeURIComponent
|
const TIMING = {
|
||||||
|
late: () => t("late"),
|
||||||
|
catch_up: () => t("caught up"),
|
||||||
|
manual: () => t("run by hand"),
|
||||||
|
}
|
||||||
|
|
||||||
function statusChip(run) {
|
const MAX_AGENTS = 8 // services/schedules.py MAX_AGENTS
|
||||||
|
const enc = encodeURIComponent
|
||||||
|
const RUNS_PAGE = 20
|
||||||
|
|
||||||
|
function statusChip(run, running) {
|
||||||
|
if (running) return h("span", { class: "schip is-running", text: STATUS.running() })
|
||||||
if (!run) return h("span", { class: "schip", text: t("Not run yet") })
|
if (!run) return h("span", { class: "schip", text: t("Not run yet") })
|
||||||
const word = STATUS[run.status]?.() || run.status
|
const word = STATUS[run.status]?.() || run.status
|
||||||
const n = run.status === "missed" && run.missed > 1 ? ` ×${run.missed}` : ""
|
// A missed run counts the slots that passed unrun; any other run says how many it followed.
|
||||||
return h("span", { class: `schip is-${run.status}`, text: `${word}${n}` })
|
const n = run.status === "missed" ? (run.missed > 1 ? ` ×${run.missed}` : "") : run.missed > 0 ? ` · ${t("%(n)s missed", { n: run.missed })}` : ""
|
||||||
|
return h("span", { class: `schip is-${run.status}`, title: `${word}${n}`, text: `${word}${n}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A stamp as a date and time in the person's language: "Mon 10 Nov, 08:00". */
|
/** A stamp as a date and time in the person's language: "Mon 10 Nov, 08:00". */
|
||||||
function stamp(value) {
|
function stamp(value) {
|
||||||
const d = new Date(value)
|
const d = new Date(value)
|
||||||
if (Number.isNaN(d.getTime())) return ""
|
if (!value || Number.isNaN(d.getTime())) return ""
|
||||||
return d.toLocaleString(document.documentElement.lang || undefined, { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })
|
return d.toLocaleString(document.documentElement.lang || undefined, { weekday: "short", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" })
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextWords(task) {
|
function nextWords(task) {
|
||||||
if (!task.enabled) return t("Paused")
|
if (!task.enabled) return t("Paused")
|
||||||
if (!task.next_run_at) return t("No more runs")
|
if (!task.next_run) return t("No more runs")
|
||||||
return `${when(task.next_run_at)} · ${stamp(task.next_run_at)}`
|
return `${when(task.next_run)} · ${stamp(task.next_run)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPage() {
|
||||||
|
return !$("page-scheduled").hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Reading ------------------------------------------------------------------------------------
|
// --- Reading ------------------------------------------------------------------------------------
|
||||||
@@ -84,6 +119,15 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The scheduling models, in the order they are tried; null when the server has none to say. */
|
||||||
|
async function schedulingModels() {
|
||||||
|
try {
|
||||||
|
return await get("/schedules/models")
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Show the page: the list, or one task's page (`id`). */
|
/** Show the page: the list, or one task's page (`id`). */
|
||||||
export async function showScheduled(id) {
|
export async function showScheduled(id) {
|
||||||
view = id ? { name: "task", id } : { name: "list" }
|
view = id ? { name: "task", id } : { name: "list" }
|
||||||
@@ -94,10 +138,11 @@ async function draw() {
|
|||||||
const body = clear($("scheduled-body"))
|
const body = clear($("scheduled-body"))
|
||||||
clear($("scheduled-tools"))
|
clear($("scheduled-tools"))
|
||||||
clear($("scheduled-place"))
|
clear($("scheduled-place"))
|
||||||
|
shown = null
|
||||||
body.append(h("p", { class: "muted page__loading", text: t("Loading…") }))
|
body.append(h("p", { class: "muted page__loading", text: t("Loading…") }))
|
||||||
try {
|
try {
|
||||||
if (view.name === "task") await drawTask(view.id)
|
if (view.name === "task") await drawTask(view.id)
|
||||||
else if (view.name === "new") drawNew()
|
else if (view.name === "form") await drawForm(view.task)
|
||||||
else {
|
else {
|
||||||
await load()
|
await load()
|
||||||
drawList()
|
drawList()
|
||||||
@@ -114,6 +159,11 @@ function header(title, back) {
|
|||||||
else h1.textContent = title
|
else h1.textContent = title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function go(next) {
|
||||||
|
view = next
|
||||||
|
draw()
|
||||||
|
}
|
||||||
|
|
||||||
// --- The list -------------------------------------------------------------------------------------
|
// --- The list -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
function drawList() {
|
function drawList() {
|
||||||
@@ -124,11 +174,14 @@ function drawList() {
|
|||||||
body.append(empty(t("This server has no scheduled tasks yet."), t("Scheduled tasks need a newer LLeMbas server. Ask its administrator to update it.")))
|
body.append(empty(t("This server has no scheduled tasks yet."), t("Scheduled tasks need a newer LLeMbas server. Ask its administrator to update it.")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tools.append(h("button", { type: "button", class: "btn btn--sm btn--primary", onclick: () => go({ name: "new" }) }, icon(ICONS.plus), h("span", { text: t("New scheduled task") })))
|
tools.append(
|
||||||
|
h("button", { type: "button", class: "btn btn--sm", id: "sched-models", "aria-haspopup": "dialog", onclick: (e) => modelsPanel(e.currentTarget) }, h("span", { text: t("Models") })),
|
||||||
|
h("button", { type: "button", class: "btn btn--sm btn--primary", id: "sched-new", onclick: () => go({ name: "form" }) }, icon(ICONS.plus), h("span", { text: t("New scheduled task") })),
|
||||||
|
)
|
||||||
if (!tasks.length) {
|
if (!tasks.length) {
|
||||||
body.append(
|
body.append(
|
||||||
empty(t("Nothing is scheduled."), t("Say what should happen and when — “every weekday at 8:00, summarise the news on my reading list” — and the server runs it for you.")),
|
empty(t("Nothing is scheduled."), t("Say what should happen and when — “every weekday at 8:00, summarise the news on my reading list” — and the server runs it for you.")),
|
||||||
h("div", { class: "empty__actions" }, h("button", { type: "button", class: "btn btn--primary", onclick: () => go({ name: "new" }) }, icon(ICONS.plus), h("span", { text: t("New scheduled task") }))),
|
h("div", { class: "empty__actions" }, h("button", { type: "button", class: "btn btn--primary", onclick: () => go({ name: "form" }) }, icon(ICONS.plus), h("span", { text: t("New scheduled task") }))),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -152,30 +205,30 @@ function taskRow(task) {
|
|||||||
onclick: (e) => e.stopPropagation(),
|
onclick: (e) => e.stopPropagation(),
|
||||||
onchange: (e) => setEnabled(task, e.target.checked),
|
onchange: (e) => setEnabled(task, e.target.checked),
|
||||||
})
|
})
|
||||||
const row = h(
|
return h(
|
||||||
"li",
|
"li",
|
||||||
{ class: `task${task.enabled ? "" : " is-paused"}` },
|
{ class: `task${task.enabled ? "" : " is-paused"}`, dataset: { id: task.id } },
|
||||||
h(
|
h(
|
||||||
"button",
|
"button",
|
||||||
{ type: "button", class: "task__open", onclick: () => go({ name: "task", id: task.id }) },
|
{ type: "button", class: "task__open", onclick: () => go({ name: "task", id: task.id }) },
|
||||||
h("span", { class: "task__icon" }, icon(ICONS.clock)),
|
h("span", { class: "task__icon" }, icon(ICONS.clock)),
|
||||||
h("span", { class: "task__what" }, h("span", { class: "task__title", text: task.title || task.request || "" }), h("span", { class: "task__rule", text: task.summary || "" })),
|
h("span", { class: "task__what" }, h("span", { class: "task__title", text: task.title || task.request || "" }), h("span", { class: "task__rule", text: task.description || "" })),
|
||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ class: "task__next", title: task.enabled && task.next_run_at ? stamp(task.next_run_at) : "" },
|
{ class: "task__next", title: task.enabled && task.next_run ? stamp(task.next_run) : "" },
|
||||||
h("span", { class: "task__label", text: t("Next run") }),
|
h("span", { class: "task__label", text: t("Next run") }),
|
||||||
h("span", { class: "task__value", text: task.enabled && task.next_run_at ? when(task.next_run_at) : nextWords(task) }),
|
h("span", { class: "task__value", text: task.enabled && task.next_run ? when(task.next_run) : nextWords(task) }),
|
||||||
),
|
),
|
||||||
h("span", { class: "task__last" }, statusChip(task.last_run)),
|
h("span", { class: "task__last" }, statusChip(task.last_run, task.running)),
|
||||||
),
|
),
|
||||||
h("label", { class: "toggle task__toggle" }, toggle),
|
h("label", { class: "toggle task__toggle" }, toggle),
|
||||||
)
|
)
|
||||||
return row
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pause or resume: the server's own verbs, which answer the schedule as it now is. */
|
||||||
async function setEnabled(task, on) {
|
async function setEnabled(task, on) {
|
||||||
try {
|
try {
|
||||||
const r = await patch(`/schedules/${enc(task.id)}`, { enabled: on })
|
const r = await post(`/schedules/${enc(task.id)}/${on ? "resume" : "pause"}`)
|
||||||
Object.assign(task, r.schedule || { enabled: on })
|
Object.assign(task, r.schedule || { enabled: on })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await ask({ title: t("Scheduled"), text: why(err), ok: t("Close"), cancel: t("Cancel") })
|
await ask({ title: t("Scheduled"), text: why(err), ok: t("Close"), cancel: t("Cancel") })
|
||||||
@@ -184,22 +237,80 @@ async function setEnabled(task, on) {
|
|||||||
else draw()
|
else draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
function go(next) {
|
/**
|
||||||
view = next
|
* The scheduling models: the ones scheduled work may run on, in the order they are tried, each
|
||||||
draw()
|
* free now or busy (and why). A task without a model of its own runs on the first free one of
|
||||||
|
* those ticked; ticking none means all of them.
|
||||||
|
*/
|
||||||
|
async function modelsPanel(anchor) {
|
||||||
|
const data = await schedulingModels()
|
||||||
|
if (!data) return
|
||||||
|
let mine = new Set(data.mine || [])
|
||||||
|
const said = h("p", { class: "status is-error", hidden: true })
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
const r = await api("PUT", "/schedules/models", { models: data.models.filter((m) => mine.has(m.id)).map((m) => m.id) })
|
||||||
|
Object.assign(data, r)
|
||||||
|
mine = new Set(r.mine || [])
|
||||||
|
said.hidden = true
|
||||||
|
} catch (err) {
|
||||||
|
said.textContent = why(err)
|
||||||
|
said.hidden = false
|
||||||
|
}
|
||||||
|
pop.redraw()
|
||||||
|
}
|
||||||
|
const content = () =>
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{ class: "pop__body smodels" },
|
||||||
|
h("p", { class: "pop__title", text: t("Models for scheduled tasks") }),
|
||||||
|
h("p", { class: "smodels__hint", text: t("A task without a model of its own runs on the first free one of these, in this order. None ticked: all of them.") }),
|
||||||
|
h(
|
||||||
|
"ul",
|
||||||
|
{ class: "smodels__list", role: "list" },
|
||||||
|
data.models.map((m) =>
|
||||||
|
h(
|
||||||
|
"li",
|
||||||
|
null,
|
||||||
|
h(
|
||||||
|
"label",
|
||||||
|
{ class: "smodel" },
|
||||||
|
h("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: mine.has(m.id),
|
||||||
|
onchange: (e) => {
|
||||||
|
if (e.target.checked) mine.add(m.id)
|
||||||
|
else mine.delete(m.id)
|
||||||
|
save()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
avatar(m),
|
||||||
|
h("span", { class: "smodel__name", text: m.name }),
|
||||||
|
h("span", { class: `smodel__state${m.free ? " is-free" : ""}`, title: m.busy || "", text: m.free ? t("free") : t("busy") }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data.models.length ? null : h("p", { class: "muted", text: t("No model is available to this account.") }),
|
||||||
|
said,
|
||||||
|
)
|
||||||
|
const pop = popover(anchor, content, { align: "end", label: t("Models for scheduled tasks") })
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- One task -----------------------------------------------------------------------------------
|
// --- One task -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
async function drawTask(id) {
|
async function drawTask(id) {
|
||||||
const r = await get(`/schedules/${enc(id)}`)
|
const [r, page] = await Promise.all([get(`/schedules/${enc(id)}`), get(`/schedules/${enc(id)}/runs?limit=${RUNS_PAGE}`)])
|
||||||
const task = r.schedule
|
const task = r.schedule
|
||||||
const runs = r.runs || []
|
shown = { id: task.id, last: task.last_run?.id || null, status: task.last_run?.status || null, running: task.running }
|
||||||
header(task.title || t("Scheduled"), () => go({ name: "list" }))
|
header(task.title || t("Scheduled"), () => go({ name: "list" }))
|
||||||
const tools = clear($("scheduled-tools"))
|
const tools = clear($("scheduled-tools"))
|
||||||
tools.append(
|
tools.append(
|
||||||
h("button", { type: "button", class: "btn btn--sm btn--primary", onclick: (e) => runNow(task, e.currentTarget) }, icon(ICONS.play), h("span", { text: t("Run now") })),
|
h("button", { type: "button", class: "btn btn--sm btn--primary", id: "sched-run", disabled: !!task.running, onclick: (e) => runNow(task, e.currentTarget) }, icon(ICONS.play), h("span", { text: t("Run now") })),
|
||||||
h("button", {
|
h("button", { type: "button", class: "btn btn--sm", id: "sched-edit", onclick: () => go({ name: "form", task }) }, icon(ICONS.edit), h("span", { text: t("Edit") })),
|
||||||
|
h(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
class: "tool",
|
class: "tool",
|
||||||
"aria-label": t("More"),
|
"aria-label": t("More"),
|
||||||
@@ -207,37 +318,48 @@ async function drawTask(id) {
|
|||||||
onclick: (e) =>
|
onclick: (e) =>
|
||||||
menu(e.currentTarget, [
|
menu(e.currentTarget, [
|
||||||
{ label: task.enabled ? t("Pause") : t("Resume"), run: () => setEnabled(task, !task.enabled) },
|
{ label: task.enabled ? t("Pause") : t("Resume"), run: () => setEnabled(task, !task.enabled) },
|
||||||
|
...(task.chat_id ? [{ label: t("Open its chat"), run: () => ctx.openChat(task.chat_id) }] : []),
|
||||||
null,
|
null,
|
||||||
{ label: t("Delete"), danger: true, run: () => remove(task) },
|
{ label: t("Delete"), danger: true, run: () => remove(task) },
|
||||||
]),
|
]),
|
||||||
}, icon(ICONS.more)),
|
},
|
||||||
|
icon(ICONS.more),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const models = ctx.models()
|
const models = (await schedulingModels())?.models || ctx.models()
|
||||||
const model = models.find((m) => m.id === task.model)
|
|
||||||
const body = clear($("scheduled-body"))
|
const body = clear($("scheduled-body"))
|
||||||
body.append(
|
body.append(
|
||||||
h(
|
h(
|
||||||
"section",
|
"section",
|
||||||
{ class: "sched" },
|
{ class: "sched" },
|
||||||
|
task.failure_streak > 0 && task.last_error ? h("p", { class: "status is-error sched__trouble", text: t("The last %(n)s runs failed: %(why)s", { n: task.failure_streak, why: task.last_error }) }) : null,
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "sched__facts" },
|
{ class: "sched__facts" },
|
||||||
fact(t("When"), h("span", { text: task.summary || "" })),
|
fact(t("When"), h("span", { text: task.description || "" }), task.timezone ? t("Times in %(zone)s", { zone: task.timezone }) : ""),
|
||||||
fact(t("Next run"), h("span", { text: nextWords(task) })),
|
fact(t("Next run"), h("span", { text: nextWords(task) })),
|
||||||
fact(t("Last run"), statusChip(task.last_run)),
|
fact(t("Last run"), statusChip(task.last_run, task.running)),
|
||||||
fact(t("Model"), model ? h("span", { class: "sched__model" }, avatar(model), h("span", { text: model.name })) : h("span", { text: task.model || t("default") })),
|
fact(t("Model"), modelWords(models, task.model)),
|
||||||
|
fact(t("Reasoning effort"), h("span", { text: task.effort ? levelWord(task.effort) : t("The model's own") })),
|
||||||
|
fact(t("Agents"), h("span", { text: task.agents ? t("A team of up to %(n)s", { n: task.agents }) : t("No team") })),
|
||||||
fact(t("Tell me"), h("span", { text: NOTIFY[task.notify]?.() || NOTIFY.matters() })),
|
fact(t("Tell me"), h("span", { text: NOTIFY[task.notify]?.() || NOTIFY.matters() })),
|
||||||
fact(t("Runs"), h("span", { text: MODE[task.mode]?.() || MODE.fresh() })),
|
fact(t("Runs"), h("span", { text: MODE[task.mode]?.() || MODE.fresh() })),
|
||||||
),
|
),
|
||||||
h("div", { class: "sched__what" }, h("h2", { class: "sched__h", text: t("What it does") }), h("p", { class: "sched__instruction", text: task.instruction || task.request || "" }), task.request && task.request !== task.instruction ? h("p", { class: "sched__asked", text: t("You asked: “%(text)s”", { text: task.request }) }) : null),
|
h("div", { class: "sched__what" }, h("h2", { class: "sched__h", text: t("What it does") }), h("p", { class: "sched__instruction", text: task.instruction || task.request || "" }), task.request && task.request !== task.instruction ? h("p", { class: "sched__asked", text: t("You asked: “%(text)s”", { text: task.request }) }) : null),
|
||||||
upcoming(task.next || []),
|
upcoming(task.next_runs || []),
|
||||||
),
|
),
|
||||||
history(runs),
|
history(task, page.runs || [], page.more),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fact(label, value) {
|
function fact(label, value, hint) {
|
||||||
return h("div", { class: "fact" }, h("span", { class: "fact__label", text: label }), h("span", { class: "fact__value" }, value))
|
return h("div", { class: "fact" }, h("span", { class: "fact__label", text: label }), h("span", { class: "fact__value" }, value), hint ? h("span", { class: "fact__hint", text: hint }) : null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelWords(models, id) {
|
||||||
|
if (!id) return h("span", { text: t("The first free scheduling model") })
|
||||||
|
const m = models.find((x) => x.id === id)
|
||||||
|
return m ? h("span", { class: "sched__model" }, avatar(m), h("span", { text: m.name })) : h("span", { text: id })
|
||||||
}
|
}
|
||||||
|
|
||||||
function upcoming(next) {
|
function upcoming(next) {
|
||||||
@@ -245,34 +367,93 @@ function upcoming(next) {
|
|||||||
return h("div", { class: "sched__next" }, h("h2", { class: "sched__h", text: t("The next runs") }), h("ol", { class: "runs-next" }, next.slice(0, 5).map((s) => h("li", null, h("span", { text: stamp(s) }), h("span", { class: "muted", text: when(s) })))))
|
return h("div", { class: "sched__next" }, h("h2", { class: "sched__h", text: t("The next runs") }), h("ol", { class: "runs-next" }, next.slice(0, 5).map((s) => h("li", null, h("span", { text: stamp(s) }), h("span", { class: "muted", text: when(s) })))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The history: one row per run on fixed tracks -- status, when, model, and its result. */
|
/** The history: one row per run on fixed tracks -- status, when, model, and a way to its result. */
|
||||||
function history(runs) {
|
function history(task, runs, more) {
|
||||||
const box = h("section", { class: "sched__history" }, h("h2", { class: "sched__h", text: t("History") }))
|
const box = h("section", { class: "sched__history" }, h("h2", { class: "sched__h", text: t("History") }))
|
||||||
if (!runs.length) {
|
if (!runs.length) {
|
||||||
box.append(h("p", { class: "muted", text: t("It has not run yet.") }))
|
box.append(h("p", { class: "muted", text: t("It has not run yet.") }))
|
||||||
return box
|
return box
|
||||||
}
|
}
|
||||||
const models = ctx.models()
|
|
||||||
const list = h("ul", { class: "runs" })
|
const list = h("ul", { class: "runs" })
|
||||||
for (const run of runs) {
|
const add = (page) => {
|
||||||
const m = models.find((x) => x.id === run.model)
|
for (const run of page) list.append(runRow(task, run))
|
||||||
list.append(
|
|
||||||
h(
|
|
||||||
"li",
|
|
||||||
{ class: "run" },
|
|
||||||
h("span", { class: "run__status" }, statusChip(run)),
|
|
||||||
h("span", { class: "run__when", title: run.started_at || run.due_at || "" }, h("span", { text: stamp(run.started_at || run.due_at) }), run.timing === "late" || run.timing === "catch_up" ? h("span", { class: "run__late", text: run.timing === "late" ? t("late") : t("caught up") }) : null),
|
|
||||||
h("span", { class: "run__model", title: run.model_reason || "" }, m ? avatar(m) : null, h("span", { text: m?.name || run.model || "" })),
|
|
||||||
run.chat_id
|
|
||||||
? h("button", { type: "button", class: "btn btn--sm", text: t("Open the result"), onclick: () => ctx.openChat(run.chat_id) })
|
|
||||||
: h("span", { class: "run__error", title: run.error || "", text: run.error || "" }),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
add(runs)
|
||||||
box.append(list)
|
box.append(list)
|
||||||
|
if (more) {
|
||||||
|
const earlier = h("button", { type: "button", class: "btn btn--sm sched__more", text: t("Earlier runs") })
|
||||||
|
earlier.addEventListener("click", async () => {
|
||||||
|
earlier.disabled = true
|
||||||
|
try {
|
||||||
|
const last = list.lastElementChild?.dataset.id || ""
|
||||||
|
const r = await get(`/schedules/${enc(task.id)}/runs?limit=${RUNS_PAGE}&before=${enc(last)}`)
|
||||||
|
add(r.runs || [])
|
||||||
|
if (!r.more) earlier.remove()
|
||||||
|
else earlier.disabled = false
|
||||||
|
} catch (err) {
|
||||||
|
earlier.disabled = false
|
||||||
|
await ask({ title: t("History"), text: why(err), ok: t("Close"), cancel: t("Cancel") })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
box.append(earlier)
|
||||||
|
}
|
||||||
return box
|
return box
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What a run that did not finish well says: waiting and until when, or its error. */
|
||||||
|
function runNote(run) {
|
||||||
|
if (run.status === "waiting") {
|
||||||
|
const again = run.retry_at ? t("tried again %(when)s", { when: when(run.retry_at) }) : ""
|
||||||
|
return [run.error?.text || STATUS.waiting(), again].filter(Boolean).join(" · ")
|
||||||
|
}
|
||||||
|
return run.error?.text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function runRow(task, run) {
|
||||||
|
const model = run.model || {}
|
||||||
|
const switched = model.reason === "switched" && model.switched_from ? t("%(model)s was busy, so it ran on this one.", { model: model.switched_from }) : ""
|
||||||
|
const hasResult = run.message_id || run.report_path || run.preview
|
||||||
|
const detail = h("div", { class: "run__detail", hidden: true })
|
||||||
|
const open = hasResult
|
||||||
|
? h("button", { type: "button", class: "btn btn--sm run__open", "aria-expanded": "false", text: t("Result") })
|
||||||
|
: null
|
||||||
|
open?.addEventListener("click", async () => {
|
||||||
|
const opening = detail.hidden
|
||||||
|
open.setAttribute("aria-expanded", String(opening))
|
||||||
|
if (!opening) {
|
||||||
|
detail.hidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
detail.hidden = false
|
||||||
|
detail.replaceChildren(h("p", { class: "muted", text: t("Loading…") }))
|
||||||
|
try {
|
||||||
|
const r = await get(`/schedules/${enc(task.id)}/runs/${enc(run.id)}`)
|
||||||
|
detail.replaceChildren(
|
||||||
|
r.text ? h("div", { class: "md run__text" }, markdown(r.text)) : h("p", { class: "muted", text: run.preview || t("The result is no longer there.") }),
|
||||||
|
r.run?.report_path ? h("p", { class: "run__report", text: t("Written to %(path)s", { path: r.run.report_path }) }) : null,
|
||||||
|
r.run?.chat_id ? h("div", { class: "set__buttons" }, h("button", { type: "button", class: "btn btn--sm", text: t("Open the chat"), onclick: () => ctx.openChat(r.run.chat_id) })) : null,
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
detail.replaceChildren(h("p", { class: "status is-error", text: why(err) }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const note = runNote(run)
|
||||||
|
return h(
|
||||||
|
"li",
|
||||||
|
{ class: "run", dataset: { id: run.id } },
|
||||||
|
h("span", { class: "run__status" }, statusChip(run)),
|
||||||
|
h(
|
||||||
|
"span",
|
||||||
|
{ class: "run__when", title: run.due_at ? t("Due %(when)s", { when: stamp(run.due_at) }) : "" },
|
||||||
|
h("span", { text: stamp(run.started_at || run.due_at) }),
|
||||||
|
TIMING[run.timing] ? h("span", { class: "run__late", text: TIMING[run.timing]() }) : null,
|
||||||
|
),
|
||||||
|
h("span", { class: "run__model", title: switched }, h("span", { text: model.name || "" }), switched ? h("span", { class: "run__switched", text: t("switched") }) : null),
|
||||||
|
open || h("span", { class: "run__error", title: note, text: note }),
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function runNow(task, btn) {
|
async function runNow(task, btn) {
|
||||||
btn.disabled = true
|
btn.disabled = true
|
||||||
try {
|
try {
|
||||||
@@ -288,95 +469,258 @@ async function remove(task) {
|
|||||||
const yes = await ask({ title: t("Delete this scheduled task?"), text: t("“%(title)s” stops, and its history goes. The chats it wrote stay."), ok: t("Delete"), cancel: t("Cancel"), danger: true })
|
const yes = await ask({ title: t("Delete this scheduled task?"), text: t("“%(title)s” stops, and its history goes. The chats it wrote stay."), ok: t("Delete"), cancel: t("Cancel"), danger: true })
|
||||||
if (!yes) return
|
if (!yes) return
|
||||||
try {
|
try {
|
||||||
await del(`/schedules/${enc(task.id)}`)
|
await del(`/schedules/${enc(task.id)}?keep_chat=true`)
|
||||||
go({ name: "list" })
|
go({ name: "list" })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await ask({ title: t("Delete"), text: why(err), ok: t("Close"), cancel: t("Cancel") })
|
await ask({ title: t("Delete"), text: why(err), ok: t("Close"), cancel: t("Cancel") })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- A new task, said in words ------------------------------------------------------------------
|
// --- A task said in words: new, or an existing one edited ---------------------------------------
|
||||||
|
|
||||||
let compiled = null // the server's reading of the words: { title, instruction, summary, rule, next }
|
/**
|
||||||
|
* The form. The words first: Read them asks the server (compile) for the rule, its title and
|
||||||
function drawNew() {
|
* instruction, and the next runs; the title and instruction can then be changed. Below, the
|
||||||
header(t("New scheduled task"), () => go({ name: "list" }))
|
* settings the server keeps per task. An existing task starts from what it is: its words, title,
|
||||||
|
* instruction, and its rule read back (preview) -- a paused task's next runs too.
|
||||||
|
*/
|
||||||
|
async function drawForm(task) {
|
||||||
|
const editing = !!task
|
||||||
|
header(editing ? t("Edit %(title)s", { title: task.title }) : t("New scheduled task"), () => go(editing ? { name: "task", id: task.id } : { name: "list" }))
|
||||||
clear($("scheduled-tools"))
|
clear($("scheduled-tools"))
|
||||||
compiled = null
|
const smodels = (await schedulingModels())?.models || []
|
||||||
const words = h("textarea", { id: "sched-words", class: "sched__words", rows: "4", placeholder: t("Every weekday at 8:00, summarise the news on my reading list"), "aria-label": t("What should happen, and when") })
|
// What will be saved: the rule only from the server's reading of words (or the task's own).
|
||||||
|
const draft = {
|
||||||
|
request: task?.request || "",
|
||||||
|
title: task?.title || "",
|
||||||
|
instruction: task?.instruction || "",
|
||||||
|
rule: task?.rule || null,
|
||||||
|
target: task?.target || "chat",
|
||||||
|
model: task?.model || "",
|
||||||
|
effort: task?.effort || "",
|
||||||
|
agents: task?.agents || null,
|
||||||
|
notify: task?.notify || "matters",
|
||||||
|
mode: task?.mode || "fresh",
|
||||||
|
}
|
||||||
|
let reading = editing ? await post("/schedules/preview", { rule: task.rule }).catch(() => null) : null
|
||||||
|
|
||||||
|
const words = h("textarea", { id: "sched-words", class: "sched__words", rows: "3", placeholder: t("Every weekday at 8:00, summarise the news on my reading list"), "aria-label": t("What should happen, and when"), value: draft.request })
|
||||||
|
const readBtn = h("button", { type: "submit", class: "btn", text: editing ? t("Read again") : t("Preview") })
|
||||||
const said = h("p", { class: "status is-error", hidden: true })
|
const said = h("p", { class: "status is-error", hidden: true })
|
||||||
const preview = h("div", { class: "sched__preview", hidden: true })
|
const when_ = h("span", { class: "sched__desc", id: "sched-when" })
|
||||||
const read = h("button", { type: "submit", class: "btn btn--primary", text: t("Preview") })
|
const zone = h("p", { class: "hint" })
|
||||||
|
const title = h("input", { type: "text", id: "sched-title", value: draft.title, "aria-label": t("Title") })
|
||||||
|
const instruction = h("textarea", { id: "sched-instruction", class: "sched__words", rows: "3", "aria-label": t("What it does"), value: draft.instruction })
|
||||||
|
const settings = h("div", { class: "fields sched__settings" })
|
||||||
|
const saveBtn = h("button", { type: "button", class: "btn btn--primary", id: "sched-save", text: t("Save") })
|
||||||
|
const saveSaid = h("p", { class: "status is-error", hidden: true })
|
||||||
|
const nextBox = h("div", { class: "sched__nextbox" })
|
||||||
|
const manual = manualRule((rule) => {
|
||||||
|
post("/schedules/preview", { rule })
|
||||||
|
.then((r) => {
|
||||||
|
reading = r
|
||||||
|
draft.rule = r.rule || {}
|
||||||
|
drawWhen()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
saveSaid.textContent = why(err)
|
||||||
|
saveSaid.hidden = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const manualBtn = h("button", { type: "button", class: "btn btn--sm sched__manual-open", text: t("Set the time yourself"), onclick: () => ((manual.hidden = false), (manualBtn.hidden = true), manual.querySelector("input")?.focus()) })
|
||||||
|
const rest = h(
|
||||||
|
"div",
|
||||||
|
{ class: "sched__card", hidden: !editing },
|
||||||
|
h("div", { class: "fields" }, field(t("Title"), title, "", "sched-title"), h("div", { class: "field" }, h("span", { class: "label", text: t("When") }), when_, zone)),
|
||||||
|
manualBtn,
|
||||||
|
manual,
|
||||||
|
nextBox,
|
||||||
|
h("label", { class: "sched__h", for: "sched-instruction", text: t("What it does") }),
|
||||||
|
instruction,
|
||||||
|
h("p", { class: "hint", text: t("The instruction each run is given, on its own: say everything it needs.") }),
|
||||||
|
settings,
|
||||||
|
saveSaid,
|
||||||
|
h("div", { class: "set__buttons" }, saveBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
const drawWhen = () => {
|
||||||
|
const r = reading
|
||||||
|
const next = r?.next_runs || []
|
||||||
|
const runs = !!(draft.rule && Object.keys(draft.rule).length)
|
||||||
|
when_.textContent = (runs && r?.description) || t("These words give no run in the future.")
|
||||||
|
when_.title = when_.textContent
|
||||||
|
when_.classList.toggle("is-error", !runs)
|
||||||
|
zone.textContent = runs && r?.timezone ? t("Times in %(zone)s", { zone: r.timezone }) : ""
|
||||||
|
nextBox.replaceChildren(upcoming(next) || "")
|
||||||
|
saveBtn.disabled = !(draft.rule && Object.keys(draft.rule).length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawSettings = () => {
|
||||||
|
const model = smodels.find((m) => m.id === draft.model) || ctx.models().find((m) => m.id === draft.model)
|
||||||
|
const select = (id, table, key, onChange) =>
|
||||||
|
h(
|
||||||
|
"select",
|
||||||
|
{ id, onchange: (e) => ((draft[key] = e.target.value), onChange?.()) },
|
||||||
|
Object.entries(table).map(([v, word]) => h("option", { value: v, text: word(), selected: draft[key] === v })),
|
||||||
|
)
|
||||||
|
const modelSelect = h(
|
||||||
|
"select",
|
||||||
|
{ id: "sched-model", onchange: (e) => ((draft.model = e.target.value), (draft.effort = ""), drawSettings()) },
|
||||||
|
h("option", { value: "", text: t("The first free scheduling model"), selected: !draft.model }),
|
||||||
|
smodels.map((m) => h("option", { value: m.id, text: m.free ? m.name : `${m.name} · ${t("busy")}`, selected: draft.model === m.id })),
|
||||||
|
draft.model && !smodels.some((m) => m.id === draft.model) ? h("option", { value: draft.model, text: draft.model, selected: true }) : null,
|
||||||
|
)
|
||||||
|
const levels = model ? levelsOf(model).levels.filter((l) => l !== "default") : []
|
||||||
|
const effort = h(
|
||||||
|
"select",
|
||||||
|
{ id: "sched-effort", disabled: !levels.length && !draft.effort, onchange: (e) => (draft.effort = e.target.value) },
|
||||||
|
h("option", { value: "", text: t("The model's own"), selected: !draft.effort }),
|
||||||
|
levels.map((l) => h("option", { value: l, text: levelWord(l), selected: draft.effort === l })),
|
||||||
|
// A level the task has that its model no longer offers stays, as it is, until changed.
|
||||||
|
draft.effort && !levels.includes(draft.effort) ? h("option", { value: draft.effort, text: levelWord(draft.effort), selected: true }) : null,
|
||||||
|
)
|
||||||
|
const agents = h(
|
||||||
|
"select",
|
||||||
|
{ id: "sched-agents", onchange: (e) => (draft.agents = Number(e.target.value) || null) },
|
||||||
|
h("option", { value: "", text: t("No team"), selected: !draft.agents }),
|
||||||
|
Array.from({ length: MAX_AGENTS - 1 }, (_, i) => i + 2).map((n) => h("option", { value: String(n), text: t("A team of up to %(n)s", { n }), selected: draft.agents === n })),
|
||||||
|
)
|
||||||
|
settings.replaceChildren(
|
||||||
|
field(t("Tell me"), select("sched-notify", NOTIFY, "notify", () => (settings.querySelector("#sched-notify-hint").textContent = NOTIFY_HINT[draft.notify]())), NOTIFY_HINT[draft.notify]?.() || "", "sched-notify", "sched-notify-hint"),
|
||||||
|
field(t("Runs"), select("sched-mode", MODE, "mode"), draft.mode === "continue" ? t("Each run sees the ones before it.") : t("Each run starts with nothing but the instruction."), "sched-mode"),
|
||||||
|
field(t("Model"), modelSelect, t("A busy model is waited for, then the next free one is used."), "sched-model"),
|
||||||
|
field(t("Reasoning effort"), effort, levels.length ? "" : t("Choose a model to choose its effort."), "sched-effort"),
|
||||||
|
field(t("Agents"), agents, t("A team works on it together, its own agent included."), "sched-agents"),
|
||||||
|
)
|
||||||
|
settings.querySelector("#sched-mode").addEventListener("change", drawSettings)
|
||||||
|
}
|
||||||
|
|
||||||
const form = h(
|
const form = h(
|
||||||
"form",
|
"form",
|
||||||
{ class: "sched sched--new" },
|
{ class: "sched sched--new" },
|
||||||
h("label", { class: "sched__h", for: "sched-words", text: t("What should happen, and when") }),
|
h("label", { class: "sched__h", for: "sched-words", text: t("What should happen, and when") }),
|
||||||
words,
|
words,
|
||||||
h("p", { class: "hint", text: t("In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.") }),
|
h("p", { class: "hint", text: t("In your own words. The server reads them into a rule and an instruction, and shows you the next runs before anything is saved.") }),
|
||||||
h("div", { class: "set__buttons" }, read),
|
h("div", { class: "set__buttons" }, readBtn),
|
||||||
said,
|
said,
|
||||||
preview,
|
rest,
|
||||||
)
|
)
|
||||||
form.addEventListener("submit", async (e) => {
|
form.addEventListener("submit", async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const text = words.value.trim()
|
const text = words.value.trim()
|
||||||
if (!text) return words.focus()
|
if (!text) return words.focus()
|
||||||
read.disabled = true
|
readBtn.disabled = true
|
||||||
said.hidden = true
|
said.hidden = true
|
||||||
try {
|
try {
|
||||||
compiled = await post("/schedules/compile", { text })
|
const c = await post("/schedules/compile", { text })
|
||||||
drawPreview(preview, text)
|
draft.request = text
|
||||||
|
draft.rule = c.rule || {}
|
||||||
|
if (c.title || !editing) title.value = c.title || ""
|
||||||
|
if (c.instruction || !editing) instruction.value = c.instruction || text
|
||||||
|
if (c.target) draft.target = c.target
|
||||||
|
reading = c
|
||||||
|
if (!c.ok && c.reason) {
|
||||||
|
said.textContent = c.reason
|
||||||
|
said.hidden = false
|
||||||
|
}
|
||||||
|
// The words gave no rule: the form for one, rather than a task that cannot run.
|
||||||
|
if (!(c.rule && Object.keys(c.rule).length)) {
|
||||||
|
manual.hidden = false
|
||||||
|
manualBtn.hidden = true
|
||||||
|
}
|
||||||
|
rest.hidden = false
|
||||||
|
drawWhen()
|
||||||
|
;(c.ok ? saveBtn : instruction).focus()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
said.textContent = why(err)
|
said.textContent = why(err)
|
||||||
said.hidden = false
|
said.hidden = false
|
||||||
} finally {
|
} finally {
|
||||||
read.disabled = false
|
readBtn.disabled = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
words.addEventListener("keydown", (e) => {
|
words.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) form.requestSubmit()
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) form.requestSubmit()
|
||||||
})
|
})
|
||||||
|
saveBtn.addEventListener("click", async () => {
|
||||||
|
saveBtn.disabled = true
|
||||||
|
saveSaid.hidden = true
|
||||||
|
const body = {
|
||||||
|
request: draft.request || words.value.trim(),
|
||||||
|
title: title.value.trim(),
|
||||||
|
instruction: instruction.value.trim(),
|
||||||
|
rule: draft.rule,
|
||||||
|
target: draft.target,
|
||||||
|
model: draft.model,
|
||||||
|
effort: draft.effort,
|
||||||
|
agents: draft.agents,
|
||||||
|
notify: draft.notify,
|
||||||
|
mode: draft.mode,
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = editing ? await patch(`/schedules/${enc(task.id)}`, body) : await post("/schedules", body)
|
||||||
|
go({ name: "task", id: r.schedule.id })
|
||||||
|
} catch (err) {
|
||||||
|
saveSaid.textContent = why(err)
|
||||||
|
saveSaid.hidden = false
|
||||||
|
saveBtn.disabled = false
|
||||||
|
}
|
||||||
|
})
|
||||||
clear($("scheduled-body")).append(form)
|
clear($("scheduled-body")).append(form)
|
||||||
|
drawSettings()
|
||||||
|
drawWhen()
|
||||||
words.focus()
|
words.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the server made of the words: the rule in words, the instruction, the next five runs. */
|
/**
|
||||||
function drawPreview(box, text) {
|
* The rule set by hand, for words the server could not read into one: the days of the week (none
|
||||||
const c = compiled
|
* ticked: every day) and a time, as the rule's calendar (`{ at: { weekdays, times } }`, Monday 0).
|
||||||
box.hidden = false
|
* `onRule(rule)` on every change; the server reads it back (preview).
|
||||||
const choice = { notify: "matters", mode: "fresh" }
|
*/
|
||||||
const select = (key, table) =>
|
function manualRule(onRule) {
|
||||||
h(
|
const lang = document.documentElement.lang || undefined
|
||||||
"select",
|
const days = Array.from({ length: 7 }, (_, i) => {
|
||||||
{ "aria-label": key === "notify" ? t("Tell me") : t("Runs"), onchange: (e) => (choice[key] = e.target.value) },
|
const d = new Date(2024, 0, 1 + i) // 1 January 2024 was a Monday
|
||||||
Object.entries(table).map(([v, word]) => h("option", { value: v, text: word(), selected: choice[key] === v })),
|
return h("label", { class: "sday", title: d.toLocaleDateString(lang, { weekday: "long" }) }, h("input", { type: "checkbox", value: String(i) }), h("span", { text: d.toLocaleDateString(lang, { weekday: "short" }) }))
|
||||||
)
|
|
||||||
const save = h("button", { type: "button", class: "btn btn--primary", text: t("Save") })
|
|
||||||
const said = h("p", { class: "status is-error", hidden: true })
|
|
||||||
save.addEventListener("click", async () => {
|
|
||||||
save.disabled = true
|
|
||||||
try {
|
|
||||||
const r = await post("/schedules", { request: text, title: c.title, instruction: c.instruction, rule: c.rule, notify: choice.notify, mode: choice.mode })
|
|
||||||
go({ name: "task", id: r.schedule.id })
|
|
||||||
} catch (err) {
|
|
||||||
said.textContent = why(err)
|
|
||||||
said.hidden = false
|
|
||||||
save.disabled = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
box.replaceChildren(
|
const time = h("input", { type: "time", id: "sched-time", value: "08:00", required: true })
|
||||||
h(
|
const box = h(
|
||||||
"div",
|
"div",
|
||||||
{ class: "sched__card" },
|
{ class: "sched__manual", hidden: true },
|
||||||
h("p", { class: "sched__title", text: c.title || "" }),
|
h("div", { class: "fields" }, h("div", { class: "field" }, h("span", { class: "label", text: t("Days") }), h("div", { class: "sdays", role: "group", "aria-label": t("Days") }, days), h("p", { class: "hint", text: t("No day ticked: every day.") })), field(t("Time"), time, "", "sched-time")),
|
||||||
h("div", { class: "sched__facts" }, fact(t("When"), h("span", { text: c.summary || "" })), fact(t("What it does"), h("span", { text: c.instruction || "" }))),
|
|
||||||
upcoming(c.next || []) || h("p", { class: "status is-error", text: t("These words give no run in the future.") }),
|
|
||||||
h("div", { class: "sched__facts" }, fact(t("Tell me"), select("notify", NOTIFY)), fact(t("Runs"), select("mode", MODE))),
|
|
||||||
said,
|
|
||||||
h("div", { class: "set__buttons" }, save),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
save.focus()
|
const changed = () => {
|
||||||
|
if (!time.value) return
|
||||||
|
const weekdays = days.map((d) => d.querySelector("input")).filter((i) => i.checked).map((i) => Number(i.value))
|
||||||
|
onRule({ at: { ...(weekdays.length ? { weekdays } : {}), times: [time.value] } })
|
||||||
|
}
|
||||||
|
box.addEventListener("change", changed)
|
||||||
|
return box
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A field on the form's shared tracks: label, control, hint -- always all three. */
|
||||||
|
function field(label, control, hint, forId, hintId) {
|
||||||
|
return h("div", { class: "field" }, h("label", { for: forId || null, text: label }), control, h("p", { class: "hint", id: hintId || null, text: hint || "" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Live: the list as the server's event stream gives it ---------------------------------------
|
||||||
|
|
||||||
|
/** A `schedules` frame (`{ version, schedules }`), handed on by the app: redraw what it changes. */
|
||||||
|
function onSchedules(list) {
|
||||||
|
if (!Array.isArray(list?.schedules)) return
|
||||||
|
tasks = list.schedules
|
||||||
|
missing = false
|
||||||
|
if (!onPage() || popoverOpenHere()) return
|
||||||
|
if (view.name === "list") drawList()
|
||||||
|
else if (view.name === "task" && shown) {
|
||||||
|
const now = tasks.find((x) => x.id === shown.id)
|
||||||
|
if (!now) return go({ name: "list" })
|
||||||
|
const last = now.last_run || {}
|
||||||
|
if ((last.id || null) !== shown.last || (last.status || null) !== shown.status || now.running !== shown.running) draw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function popoverOpenHere() {
|
||||||
|
return !!document.querySelector(".pop .smodels")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The palette's commands for scheduled tasks. */
|
/** The palette's commands for scheduled tasks. */
|
||||||
@@ -386,7 +730,7 @@ export function scheduledCommands() {
|
|||||||
label: t("New scheduled task"),
|
label: t("New scheduled task"),
|
||||||
run: () => {
|
run: () => {
|
||||||
ctx.show()
|
ctx.show()
|
||||||
go({ name: "new" })
|
go({ name: "form" })
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -394,4 +738,5 @@ export function scheduledCommands() {
|
|||||||
|
|
||||||
export function initScheduled(context) {
|
export function initScheduled(context) {
|
||||||
ctx = context
|
ctx = context
|
||||||
|
listen("schedules", (e) => onSchedules(e.payload))
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user