i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -1,11 +1,14 @@
|
||||
// ClawdDotNet — Aufgaben (Taskboard)
|
||||
//
|
||||
// Seit #594 die einzige Stelle für geplante Arbeit. Der frühere Reiter „Geplante Läufe (Jobs)“
|
||||
// bearbeitete die Alt-Konfiguration scheduler/toolJobs; nach der ersten Übernahme ins Taskboard
|
||||
// wirkte dort keine Änderung mehr. Termin, nächster und letzter Lauf, Pausieren und Sofortlauf
|
||||
// stehen jetzt an der Aufgabe selbst.
|
||||
// Central taskboard view for scheduled and manual tasks.
|
||||
//
|
||||
// Since #594 the only place for scheduled work. The former tab "Scheduled runs (Jobs)"
|
||||
// edited the legacy config scheduler/toolJobs; after the first migration into the taskboard
|
||||
// no change made there had any effect. Schedule, next and last run, pause and run-now
|
||||
// now live on the task itself.
|
||||
|
||||
import { api } from "../modules/api.js";
|
||||
import { t } from "../modules/i18n.js";
|
||||
import {
|
||||
erstelleElement,
|
||||
erstelleIcon,
|
||||
@@ -18,16 +21,19 @@ import {
|
||||
zeigeMeldung
|
||||
} from "../modules/dom.js";
|
||||
|
||||
const STATUS_TEXT = {
|
||||
Backlog: "Pausiert",
|
||||
Todo: "Bereit",
|
||||
InProgress: "Läuft",
|
||||
InReview: "Im Review",
|
||||
Done: "Erledigt",
|
||||
Canceled: "Verworfen",
|
||||
Blocked: "Blockiert",
|
||||
Archived: "Archiviert"
|
||||
};
|
||||
function holeStatusText(status) {
|
||||
switch (status) {
|
||||
case "Backlog": return t("status.paused");
|
||||
case "Todo": return t("status.ready");
|
||||
case "InProgress": return t("status.running");
|
||||
case "InReview": return t("status.inReview");
|
||||
case "Done": return t("status.done");
|
||||
case "Canceled": return t("status.canceled");
|
||||
case "Blocked": return t("status.blocked");
|
||||
case "Archived": return t("status.archived");
|
||||
default: return status || t("common.unknown");
|
||||
}
|
||||
}
|
||||
|
||||
const STATUS_ART = {
|
||||
Backlog: "warnung",
|
||||
@@ -37,7 +43,6 @@ const STATUS_ART = {
|
||||
Blocked: "warnung"
|
||||
};
|
||||
|
||||
// Laufstatus aus AgentRunStatus (#577/#581 brachten EmptyResponse, Incomplete, RepeatedFailure).
|
||||
const LAUF_ART = {
|
||||
Completed: "erfolg",
|
||||
Truncated: "warnung",
|
||||
@@ -49,7 +54,6 @@ const LAUF_ART = {
|
||||
Failed: "gefahr"
|
||||
};
|
||||
|
||||
const TERMIN_ART = { cron: "Cron", every: "Intervall", at: "Zeitpunkt" };
|
||||
const ABGESCHLOSSEN = ["Done", "Canceled", "Archived"];
|
||||
|
||||
let zeigeAbgeschlossene = false;
|
||||
@@ -73,12 +77,12 @@ export async function rendereAufgaben() {
|
||||
ladeTaskboard();
|
||||
}
|
||||
}),
|
||||
erstelleElement("span", { text: "Erledigte zeigen" })
|
||||
erstelleElement("span", { text: t("tasks.showCompleted") })
|
||||
);
|
||||
|
||||
const neuladen = erstelleElement(
|
||||
"button",
|
||||
{ klasse: "schaltflaeche flach", title: "Neu laden", onclick: () => ladeTaskboard() },
|
||||
{ klasse: "schaltflaeche flach", title: t("common.refresh"), onclick: () => ladeTaskboard() },
|
||||
erstelleIcon(PFADE.aktualisieren, 16)
|
||||
);
|
||||
|
||||
@@ -86,11 +90,11 @@ export async function rendereAufgaben() {
|
||||
"button",
|
||||
{ klasse: "schaltflaeche primaer", onclick: () => oeffneAufgabenDialog(null) },
|
||||
erstelleIcon(PFADE.plus, 16),
|
||||
erstelleElement("span", { text: " Neue Aufgabe" })
|
||||
erstelleElement("span", { text: " " + t("tasks.newTask") })
|
||||
);
|
||||
|
||||
kopf.append(
|
||||
erstelleElement("h1", { text: "Aufgaben" }),
|
||||
erstelleElement("h1", { text: t("tasks.title") }),
|
||||
erstelleElement("div", { klasse: "gruppe-horizontal" }, abgeschlossenSchalter, neuladen, neu)
|
||||
);
|
||||
|
||||
@@ -105,7 +109,7 @@ async function ladeTaskboard() {
|
||||
if (!container) return;
|
||||
|
||||
container.replaceChildren(
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: "Lade Taskboard…" })
|
||||
erstelleElement("div", { klasse: "hinweis-zentriert", text: t("common.loading") })
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -115,10 +119,10 @@ async function ladeTaskboard() {
|
||||
|
||||
if (tasks.length === 0) {
|
||||
container.append(
|
||||
erstelleKarte("Keine Aufgaben", [
|
||||
erstelleKarte(t("tasks.empty"), [
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft",
|
||||
text: "Keine offenen Aufgaben. Wiederkehrende Arbeit legst du über „Neue Aufgabe“ mit einem Termin an."
|
||||
text: "..."
|
||||
})
|
||||
])
|
||||
);
|
||||
@@ -130,7 +134,7 @@ async function ladeTaskboard() {
|
||||
container.append(raster);
|
||||
} catch (err) {
|
||||
container.replaceChildren(
|
||||
erstelleKarte("Taskboard nicht verfügbar", [
|
||||
erstelleKarte("Taskboard Error", [
|
||||
erstelleElement("p", { klasse: "gedaempft", text: err.message })
|
||||
])
|
||||
);
|
||||
@@ -138,8 +142,8 @@ async function ladeTaskboard() {
|
||||
}
|
||||
|
||||
function terminText(task) {
|
||||
if (!task.when) return "einmalig";
|
||||
const art = TERMIN_ART[task.when.kind] || task.when.kind;
|
||||
if (!task.when) return t("tasks.scheduleNone");
|
||||
const art = task.when.kind;
|
||||
return `${art} ${task.when.value} (${task.when.timeZone || "UTC"})`;
|
||||
}
|
||||
|
||||
@@ -150,25 +154,25 @@ function erstelleAufgabenKarte(task) {
|
||||
erstelleElement(
|
||||
"div",
|
||||
{ klasse: "task-status-zeile gruppe-horizontal" },
|
||||
erstellePlakette(STATUS_TEXT[task.status] || task.status, STATUS_ART[task.status] || "neutral"),
|
||||
erstellePlakette(holeStatusText(task.status), STATUS_ART[task.status] || "neutral"),
|
||||
erstelleElement("span", { klasse: "gedaempft text-klein", text: `${task.id} · ${task.assignee || "—"}` })
|
||||
)
|
||||
);
|
||||
|
||||
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `Termin: ${terminText(task)}` }));
|
||||
zeilen.push(erstelleElement("p", { klasse: "gedaempft text-klein", text: `${t("tasks.schedule")}: ${terminText(task)}` }));
|
||||
zeilen.push(
|
||||
erstelleElement("p", {
|
||||
klasse: "gedaempft text-klein",
|
||||
text: `Nächster Lauf: ${task.nextRunUtc ? formatiereDatumZeit(task.nextRunUtc) : "—"}`
|
||||
text: `Next: ${task.nextRunUtc ? formatiereDatumZeit(task.nextRunUtc) : "—"}`
|
||||
})
|
||||
);
|
||||
|
||||
const letzter = erstelleElement("p", { klasse: "gedaempft text-klein" }, "Letzter Lauf: ");
|
||||
const letzter = erstelleElement("p", { klasse: "gedaempft text-klein" }, "Last: ");
|
||||
if (task.lastRun) {
|
||||
letzter.append(
|
||||
`${formatiereDatumZeit(task.lastRun.occurredAt)} `,
|
||||
erstellePlakette(task.lastRun.status, LAUF_ART[task.lastRun.status] || "neutral"),
|
||||
` ${task.lastRun.costIsKnown ? formatiereKosten(task.lastRun.costUsd) : "Kosten unbekannt"}`
|
||||
` ${task.lastRun.costIsKnown ? formatiereKosten(task.lastRun.costUsd) : "—"}`
|
||||
);
|
||||
if (task.lastRun.resultRef) letzter.title = task.lastRun.resultRef;
|
||||
} else {
|
||||
@@ -189,9 +193,9 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement(
|
||||
"button",
|
||||
{ klasse: "schaltflaeche flach klein", title: "Jetzt ausführen", onclick: () => fuehreAus(task) },
|
||||
{ klasse: "schaltflaeche flach klein", title: t("tasks.runNow"), onclick: () => fuehreAus(task) },
|
||||
erstelleIcon(PFADE.abspielen, 14),
|
||||
erstelleElement("span", { text: " Jetzt" })
|
||||
erstelleElement("span", { text: " " + t("tasks.runNow") })
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -200,7 +204,7 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: "Pausieren",
|
||||
text: t("tasks.pause"),
|
||||
onclick: () => setzeAktiv(task, false)
|
||||
})
|
||||
);
|
||||
@@ -208,7 +212,7 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: "Aktivieren",
|
||||
text: t("tasks.resume"),
|
||||
onclick: () => setzeAktiv(task, true)
|
||||
})
|
||||
);
|
||||
@@ -217,32 +221,32 @@ function erstelleAufgabenKarte(task) {
|
||||
aktionen.append(
|
||||
erstelleElement("button", {
|
||||
klasse: "schaltflaeche flach klein",
|
||||
text: "Bearbeiten",
|
||||
text: t("common.edit"),
|
||||
onclick: () => oeffneAufgabenDialog(task)
|
||||
})
|
||||
);
|
||||
zeilen.push(aktionen);
|
||||
|
||||
return erstelleKarte(task.title || `Aufgabe ${task.id}`, zeilen);
|
||||
return erstelleKarte(task.title || `Task ${task.id}`, zeilen);
|
||||
}
|
||||
|
||||
async function fuehreAus(task) {
|
||||
try {
|
||||
const res = await api(`/tasks/${encodeURIComponent(task.id)}/run`, { method: "POST" });
|
||||
zeigeMeldung(res?.message || "Lauf angestoßen.", "erfolg");
|
||||
zeigeMeldung(res?.message || t("tasks.runTriggered"), "erfolg");
|
||||
setTimeout(ladeTaskboard, 1500);
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Nicht gestartet: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
async function setzeAktiv(task, aktiv) {
|
||||
try {
|
||||
const res = await api(`/tasks/${encodeURIComponent(task.id)}/enabled?enabled=${aktiv}`, { method: "PUT" });
|
||||
zeigeMeldung(res?.message || (aktiv ? "Aktiviert." : "Pausiert."), "info");
|
||||
zeigeMeldung(res?.message || (aktiv ? t("tasks.resume") : t("tasks.pause")), "info");
|
||||
await ladeTaskboard();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Fehler: ${err.message}`, "gefahr");
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,25 +255,24 @@ async function oeffneAufgabenDialog(task) {
|
||||
try {
|
||||
agenten = (await api("/agents")) || [];
|
||||
} catch {
|
||||
// Ohne Agentenliste bleibt nur „Mensch“ und eine vorhandene Zuordnung.
|
||||
//
|
||||
}
|
||||
|
||||
const formular = erstelleElement("form", { klasse: "formular-raster" });
|
||||
|
||||
const titel = erstelleElement("input", { type: "text", required: true, value: task?.title || "" });
|
||||
|
||||
const beschreibung = erstelleElement("textarea", { rows: "5", placeholder: "Auftrag an den Agenten…" });
|
||||
const beschreibung = erstelleElement("textarea", { rows: "5", placeholder: "..." });
|
||||
beschreibung.value = task?.body || "";
|
||||
|
||||
// #598: Frischer Lauf zuerst und als Vorgabe - im Chatverlauf wächst der Kontext mit jedem Lauf.
|
||||
const zuordnung = erstelleElement("select", { required: true });
|
||||
for (const a of agenten) {
|
||||
zuordnung.append(
|
||||
erstelleElement("option", { value: `@new:${a.agentId}`, text: `${a.displayName} – frischer Lauf` }),
|
||||
erstelleElement("option", { value: `@${a.agentId}`, text: `${a.displayName} – im Chatverlauf` })
|
||||
erstelleElement("option", { value: `@new:${a.agentId}`, text: `${a.displayName} (New Run)` }),
|
||||
erstelleElement("option", { value: `@${a.agentId}`, text: `${a.displayName} (Chat History)` })
|
||||
);
|
||||
}
|
||||
zuordnung.append(erstelleElement("option", { value: "@human", text: "Mensch (kein Agentenlauf)" }));
|
||||
zuordnung.append(erstelleElement("option", { value: "@human", text: "Human (No Agent)" }));
|
||||
if (task?.assignee && ![...zuordnung.options].some((o) => o.value === task.assignee)) {
|
||||
zuordnung.append(erstelleElement("option", { value: task.assignee, text: task.assignee }));
|
||||
}
|
||||
@@ -277,10 +280,10 @@ async function oeffneAufgabenDialog(task) {
|
||||
|
||||
const terminArt = erstelleElement("select");
|
||||
terminArt.append(
|
||||
erstelleElement("option", { value: "", text: "Einmalig" }),
|
||||
erstelleElement("option", { value: "cron", text: "Cron (z.B. stündlich)" }),
|
||||
erstelleElement("option", { value: "every", text: "Intervall" }),
|
||||
erstelleElement("option", { value: "at", text: "Zeitpunkt" })
|
||||
erstelleElement("option", { value: "", text: t("tasks.scheduleNone") }),
|
||||
erstelleElement("option", { value: "cron", text: "Cron" }),
|
||||
erstelleElement("option", { value: "every", text: t("tasks.scheduleInterval") }),
|
||||
erstelleElement("option", { value: "at", text: t("tasks.scheduleAt") })
|
||||
);
|
||||
terminArt.value = task?.when?.kind || "";
|
||||
|
||||
@@ -289,7 +292,7 @@ async function oeffneAufgabenDialog(task) {
|
||||
const standardZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "Europe/Berlin";
|
||||
const zeitzone = erstelleElement("input", { type: "text", value: task?.when?.timeZone || standardZone });
|
||||
|
||||
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "Woran das Ergebnis gemessen wird (optional)" });
|
||||
const abnahme = erstelleElement("textarea", { rows: "2", placeholder: "..." });
|
||||
abnahme.value = task?.acceptance || "";
|
||||
|
||||
const hinweis = erstelleElement("p", { klasse: "gedaempft text-klein" });
|
||||
@@ -298,45 +301,43 @@ async function oeffneAufgabenDialog(task) {
|
||||
const wiederkehrend = terminArt.value === "cron" || terminArt.value === "every";
|
||||
const imVerlauf = zuordnung.value.startsWith("@") && !zuordnung.value.startsWith("@new") && zuordnung.value !== "@human";
|
||||
hinweis.hidden = !(wiederkehrend && imVerlauf);
|
||||
hinweis.textContent =
|
||||
"⚠ Im Chatverlauf wächst der Kontext mit jedem Lauf – und mit ihm die Kosten je Lauf. " +
|
||||
"Für wiederkehrende Aufgaben ist „frischer Lauf“ meist die bessere Wahl.";
|
||||
hinweis.textContent = "⚠ Notice: Running recurring tasks in chat history increases context and costs.";
|
||||
|
||||
const ohneTermin = terminArt.value === "";
|
||||
terminWert.disabled = ohneTermin;
|
||||
zeitzone.disabled = ohneTermin;
|
||||
terminWert.placeholder =
|
||||
{ cron: "z.B. 0 7-22/3 * * *", every: "z.B. 30m, 2h, 1d", at: "z.B. 2026-09-15T08:00" }[terminArt.value] || "";
|
||||
{ cron: "e.g. 0 7-22/3 * * *", every: "e.g. 30m, 2h, 1d", at: "e.g. 2026-09-15T08:00" }[terminArt.value] || "";
|
||||
};
|
||||
zuordnung.addEventListener("change", aktualisiere);
|
||||
terminArt.addEventListener("change", aktualisiere);
|
||||
aktualisiere();
|
||||
|
||||
formular.append(
|
||||
erstelleElement("label", { text: "Titel:" }), titel,
|
||||
erstelleElement("label", { text: "Auftrag:" }), beschreibung,
|
||||
erstelleElement("label", { text: "Ausführen durch:" }), zuordnung,
|
||||
erstelleElement("label", { text: "Termin:" }), terminArt,
|
||||
erstelleElement("label", { text: "Wert:" }), terminWert,
|
||||
erstelleElement("label", { text: "Zeitzone:" }), zeitzone,
|
||||
erstelleElement("label", { text: "Abnahme:" }), abnahme,
|
||||
erstelleElement("label", { text: t("common.name") + ":" }), titel,
|
||||
erstelleElement("label", { text: t("tasks.taskMessage") + ":" }), beschreibung,
|
||||
erstelleElement("label", { text: t("tasks.agent") + ":" }), zuordnung,
|
||||
erstelleElement("label", { text: t("tasks.scheduleType") + ":" }), terminArt,
|
||||
erstelleElement("label", { text: "Value:" }), terminWert,
|
||||
erstelleElement("label", { text: "Timezone:" }), zeitzone,
|
||||
erstelleElement("label", { text: "Acceptance Criteria:" }), abnahme,
|
||||
erstelleElement("span"), hinweis
|
||||
);
|
||||
|
||||
const abbrechen = erstelleElement("button", {
|
||||
type: "button",
|
||||
klasse: "schaltflaeche flach",
|
||||
text: "Abbrechen",
|
||||
text: t("common.cancel"),
|
||||
onclick: () => modal.schliessen()
|
||||
});
|
||||
|
||||
const speichern = erstelleElement("button", {
|
||||
type: "submit",
|
||||
klasse: "schaltflaeche primaer",
|
||||
text: task ? "Speichern" : "Anlegen"
|
||||
text: task ? t("common.save") : t("tasks.newTask")
|
||||
});
|
||||
|
||||
const modal = erstelleModal(task ? "Aufgabe bearbeiten" : "Neue Aufgabe", formular, [abbrechen, speichern]);
|
||||
const modal = erstelleModal(task ? t("common.edit") : t("tasks.createTitle"), formular, [abbrechen, speichern]);
|
||||
|
||||
formular.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -359,11 +360,11 @@ async function oeffneAufgabenDialog(task) {
|
||||
body
|
||||
});
|
||||
modal.schliessen();
|
||||
const text = res?.message || "Gespeichert.";
|
||||
zeigeMeldung(text, text.includes("Hinweis:") ? "warnung" : "erfolg", text.includes("Hinweis:") ? 9000 : 4000);
|
||||
const text = res?.message || t("tasks.saved");
|
||||
zeigeMeldung(text, "erfolg");
|
||||
await ladeTaskboard();
|
||||
} catch (err) {
|
||||
zeigeMeldung(`Nicht gespeichert: ${err.message}`, "gefahr", 8000);
|
||||
zeigeMeldung(`Error: ${err.message}`, "gefahr");
|
||||
} finally {
|
||||
speichern.disabled = false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user