@goodandready/dsh-cron 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The "silent rule" (#44): a plain-language condition that decides whether a
3
+ * successful run is worth alerting about.
4
+ *
5
+ * A script that runs every 30 minutes usually produces nothing interesting, and
6
+ * `onlyOnFailure` cannot tell "the disk is at 12%" from "the disk is at 96%".
7
+ * The rule is written by the user in words; a cheap model call turns it into a
8
+ * yes/no verdict with a reason.
9
+ *
10
+ * Fail-open by design: no rule, no model, a broken call or an unusable answer all
11
+ * mean "deliver the report". Silence is only ever produced by an explicit
12
+ * verdict.
13
+ */
14
+
15
+ import { askModel, parseJsonAnswer, resolveAskTarget } from './llm-ask.js';
16
+
17
+ /** Task types whose runs produce output a rule can judge. */
18
+ export const SILENT_RULE_TASK_TYPES = ['script', 'node', 'python', 'http'];
19
+
20
+ export function supportsSilentRule(task) {
21
+ return SILENT_RULE_TASK_TYPES.includes(String(task && task.type ? task.type : 'script'));
22
+ }
23
+
24
+ const SYSTEM_PROMPT = [
25
+ 'You decide whether a monitoring job should notify its owner.',
26
+ 'Answer with a single JSON object and nothing else:',
27
+ '{"notify": true|false, "reason": "short explanation"}',
28
+ 'Be conservative: when the output shows a problem, a warning, an error or anything uncertain, notify.',
29
+ 'Only stay silent when the output clearly matches the condition for silence.',
30
+ ].join(' ');
31
+
32
+ export function buildSilentRulePrompt(task, runInfo) {
33
+ const output = String((runInfo && runInfo.output) || '').slice(0, 4000);
34
+ return [
35
+ `Task: ${task.title || 'scheduled task'} (type: ${task.type || 'script'})`,
36
+ `Schedule: ${task.scheduleText || task.schedule || ''}`,
37
+ `Exit status: ${(runInfo && runInfo.status) || 'success'}`,
38
+ '',
39
+ 'Condition for staying silent:',
40
+ String(task.silentRule || '').trim(),
41
+ '',
42
+ 'Output of the run:',
43
+ '```',
44
+ output || '(no output)',
45
+ '```',
46
+ ].join('\n');
47
+ }
48
+
49
+ /** Turn a model answer into a verdict; anything unusable means "notify". */
50
+ export function readVerdict(text) {
51
+ const parsed = parseJsonAnswer(text);
52
+ if (!parsed || typeof parsed.notify !== 'boolean') {
53
+ return { ok: false, error: 'the model did not answer with a notify verdict' };
54
+ }
55
+ return { ok: true, notify: parsed.notify, reason: String(parsed.reason || '').slice(0, 500) };
56
+ }
57
+
58
+ /**
59
+ * Decide whether a finished run should be delivered.
60
+ * Returns `{ skipped, reason, verdictError? }`; `skipped` is true only after an
61
+ * explicit "stay silent" verdict.
62
+ */
63
+ export async function applySilentRule({ task, runInfo, ask, preferredModel, timeoutMs }) {
64
+ if (!task || !task.silentRule || String(task.silentRule).trim() === '') {
65
+ return { skipped: false, reason: '' };
66
+ }
67
+ if ((runInfo && runInfo.status) !== 'success') {
68
+ return { skipped: false, reason: '', note: 'the rule only judges successful runs' };
69
+ }
70
+ if (typeof ask !== 'function') {
71
+ return { skipped: false, reason: '', verdictError: 'no model is available for the silent rule' };
72
+ }
73
+ try {
74
+ const result = await ask({
75
+ task,
76
+ prompt: buildSilentRulePrompt(task, runInfo),
77
+ system: SYSTEM_PROMPT,
78
+ preferredModel,
79
+ maxTokens: 200,
80
+ timeoutMs,
81
+ });
82
+ if (!result || !result.ok) {
83
+ return { skipped: false, reason: '', verdictError: (result && result.error) || 'the model call failed' };
84
+ }
85
+ const verdict = readVerdict(result.text);
86
+ if (!verdict.ok) return { skipped: false, reason: '', verdictError: verdict.error };
87
+ return { skipped: verdict.notify === false, reason: verdict.reason };
88
+ } catch (err) {
89
+ // The rule never decides by failing: a thrown model call means "deliver".
90
+ return { skipped: false, reason: '', verdictError: (err && err.message) || String(err) };
91
+ }
92
+ }
93
+
94
+ /** Ask helper bound to a context, used by the scheduler. */
95
+ export function makeAsk(ctx) {
96
+ return async ({ prompt, system, preferredModel, maxTokens, timeoutMs, task }) => {
97
+ const target = resolveAskTarget(ctx, task || {}, preferredModel);
98
+ return askModel(ctx, { ...target, prompt, system, maxTokens, timeoutMs });
99
+ };
100
+ }
package/lib/store.js CHANGED
@@ -8,8 +8,8 @@ import { getDshDefaultTelegramCredentials } from './telegram.js';
8
8
  * DSH_DATA_DIR wins when the harness sets it. Otherwise the profile home
9
9
  * (DSH_HOME) is authoritative: falling back to ~/.dsh would make an isolated
10
10
  * profile — a test contour, a second profile — write its tasks into another
11
- * home's data directory, which is how the MiniPC test cycle leaked a task into
12
- * the non-test profile.
11
+ * home's data directory, which is how an isolated test cycle once leaked a
12
+ * task into the wrong profile.
13
13
  */
14
14
  export function getDefaultStorePath() {
15
15
  const base = process.env.DSH_DATA_DIR
@@ -139,7 +139,7 @@ export class TaskStore {
139
139
  * fields (botTokenRef, ntfyTokenRef, …) take precedence over them.
140
140
  */
141
141
  static get MASKED_SETTING_KEYS() {
142
- return ['botToken', 'discordWebhookUrl', 'slackWebhookUrl', 'barkKey'];
142
+ return ['botToken', 'discordWebhookUrl', 'slackWebhookUrl', 'barkKey', 'apiToken'];
143
143
  }
144
144
 
145
145
  saveSettings(newSettings = {}) {
@@ -283,6 +283,17 @@ export class TaskStore {
283
283
  usage,
284
284
  costUsd,
285
285
  sessionId: runInfo.sessionId || null,
286
+ // #45: which model produced the run, and whether it only finished thanks
287
+ // to the configured fallback.
288
+ model: runInfo.model || '',
289
+ fallback: Boolean(runInfo.fallback),
290
+ // #44: this run was suppressed by its silent rule, with the reason.
291
+ silentSkip: Boolean(runInfo.silentSkip),
292
+ silentReason: runInfo.silentSkip ? String(runInfo.silentReason || '').slice(0, 500) : '',
293
+ // #43: the failure diagnosis and the prompt change it proposes.
294
+ diagnosis: runInfo.diagnosis ? String(runInfo.diagnosis).slice(0, 1000) : '',
295
+ suggestion: runInfo.suggestion ? String(runInfo.suggestion).slice(0, 2000) : '',
296
+ confidence: runInfo.confidence || '',
286
297
  });
287
298
  if (runs.length > 50) runs.length = 50;
288
299
  this.history.set(id, runs);
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Partial task updates, shared by the HTTP PATCH route and the cron_update_task
3
+ * tool (#49).
4
+ *
5
+ * One implementation keeps the whitelist, the type validation, the schedule
6
+ * re-parse and the re-scheduling identical on both surfaces; the HTTP route adds
7
+ * its own cross-origin and confirmation checks on top, while an agent calling the
8
+ * tool acts on the user's explicit instruction.
9
+ */
10
+
11
+ import { parseScheduleExpression } from './scheduler.js';
12
+ import { normalizeTaskType, CODE_EXECUTING_TYPES } from './runtimes.js';
13
+ import { PATCHABLE_TASK_FIELDS, validateTaskType } from './task-transfer.js';
14
+ import { pickPatchableFields } from './http-utils.js';
15
+ import { unknownChannelIds } from './channels.js';
16
+ import { isConfigOwned, configOwnedMessage } from './config-jobs.js';
17
+
18
+ /** True when a patch changes a task into a type that executes code. */
19
+ export function isCodeTypeSwitch(current, nextType) {
20
+ if (!current || nextType === undefined) return false;
21
+ const target = normalizeTaskType(nextType);
22
+ return CODE_EXECUTING_TYPES.includes(target) && !CODE_EXECUTING_TYPES.includes(current.type);
23
+ }
24
+
25
+ /**
26
+ * Normalise a patch: whitelist the fields, resolve the type, re-parse the
27
+ * schedule so cron patterns and one-shot markers stay consistent.
28
+ * Returns `{ ok: false, error }` when the merged task would be invalid.
29
+ */
30
+ export function buildTaskPatch(current, body) {
31
+ const patch = pickPatchableFields(body || {}, PATCHABLE_TASK_FIELDS);
32
+ if (patch.type !== undefined) {
33
+ patch.type = normalizeTaskType(patch.type);
34
+ const typeError = validateTaskType(patch.type, { ...current, ...patch });
35
+ if (typeError) return { ok: false, error: typeError };
36
+ }
37
+ if (patch.channels !== undefined) {
38
+ // #121: a typo in a channel id used to be dropped silently, so the client
39
+ // got ok: true and a task that never notified anyone. The refusal lives
40
+ // here so the HTTP route and the tool behave identically.
41
+ const unknown = unknownChannelIds(patch.channels);
42
+ if (unknown.length) {
43
+ return { ok: false, error: 'Unknown channel ids: ' + unknown.join(', '), unknownChannels: unknown };
44
+ }
45
+ }
46
+ if (patch.schedule !== undefined) {
47
+ // An empty schedule would leave an armed task that can never fire.
48
+ if (!String(patch.schedule).trim()) return { ok: false, error: 'Schedule cannot be empty' };
49
+ const parsed = parseScheduleExpression(patch.schedule);
50
+ patch.schedule = parsed.cronPattern || patch.schedule;
51
+ patch.scheduleText = (body && body.scheduleText) || parsed.humanText;
52
+ patch.oneShot = Boolean(parsed.isOneShot || (body && body.oneShot));
53
+ }
54
+ return { ok: true, patch };
55
+ }
56
+
57
+ /**
58
+ * Apply a patch to a stored task and re-arm or pause it.
59
+ *
60
+ * Switching a task into a code-executing type is the one change that turns a
61
+ * harmless schedule into code execution, so it needs explicit confirmation on
62
+ * EVERY surface: the HTTP route passes the result of its confirmation header,
63
+ * and the tool exposes its own switch. A prose request in a tool description is
64
+ * not a gate, so the refusal lives here instead.
65
+ *
66
+ * Returns `{ ok: true, task }`, `{ ok: false, error }`, or
67
+ * `{ ok: false, needsConfirmation: true, error }` for that switch.
68
+ */
69
+ export function applyTaskPatch({ store, scheduler, id, body, allowCodeSwitch = false }) {
70
+ const current = store.get(id);
71
+ if (!current) return { ok: false, notFound: true, error: 'Task not found' };
72
+ // A task declared in the profile config (#50) is owned by that file: any
73
+ // change made here disappears at the next start, so the refusal lives with
74
+ // the patch logic and covers both the HTTP route and the agent tool.
75
+ if (isConfigOwned(current)) {
76
+ return { ok: false, configOwned: true, error: configOwnedMessage(id) };
77
+ }
78
+ if (isCodeTypeSwitch(current, body && body.type) && allowCodeSwitch !== true) {
79
+ return {
80
+ ok: false,
81
+ needsConfirmation: true,
82
+ error: `Switching this task to ${normalizeTaskType(body.type)} makes it execute code; confirm that explicitly and retry`,
83
+ };
84
+ }
85
+ const built = buildTaskPatch(current, body);
86
+ if (!built.ok) return built;
87
+ const task = store.set({ ...current, ...built.patch, id });
88
+ if (task.status === 'active') scheduler.scheduleTask(task);
89
+ else scheduler.pauseTask(id);
90
+ return { ok: true, task, patch: built.patch };
91
+ }
92
+
93
+ /** Human-readable summary of what changed, for tool output and logs. */
94
+ export function describeTaskPatch(task, patch) {
95
+ const keys = Object.keys(patch || {}).filter((k) => k !== 'scheduleText');
96
+ if (!keys.length) return 'no changes';
97
+ return keys.map((key) => `${key}=${JSON.stringify(patch[key])}`).join(', ');
98
+ }
@@ -21,6 +21,10 @@ export const PATCHABLE_TASK_FIELDS = [
21
21
  'delivery',
22
22
  'provider',
23
23
  'model',
24
+ 'fallbackProvider',
25
+ 'fallbackModel',
26
+ 'silentRule',
27
+ 'inspectOnFailure',
24
28
  'timezone',
25
29
  'misfirePolicy',
26
30
  'maxRetries',
package/lib/templates.js CHANGED
@@ -17,6 +17,8 @@ export const TEMPLATE_VARIABLES = [
17
17
  'time',
18
18
  'tokens',
19
19
  'cost',
20
+ 'model',
21
+ 'diagnosis',
20
22
  ];
21
23
 
22
24
  export const DEFAULT_PLAIN_TEMPLATE = '⏰ {title}\nStatus: {status}\nSchedule: {schedule}\nDuration: {duration}\n{output}';
@@ -60,6 +62,8 @@ export function buildTemplateVars(task, runInfo = {}) {
60
62
  time: new Date(runInfo.at || Date.now()).toISOString(),
61
63
  tokens: String(tokens),
62
64
  cost: `$${Number(runInfo.costUsd || 0).toFixed(4)}`,
65
+ model: runInfo.model || '',
66
+ diagnosis: truncateText(runInfo.diagnosis || ''),
63
67
  };
64
68
  }
65
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-cron",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Scheduled cron tasks, background automation and agent execution for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -14,7 +14,8 @@
14
14
  "lib/",
15
15
  "cordis.patch.yml",
16
16
  "README.md",
17
- "docs/",
17
+ "docs/README.ru.md",
18
+ "docs/README.zh.md",
18
19
  "LICENSE"
19
20
  ],
20
21
  "scripts": {
@@ -1,81 +0,0 @@
1
- # DESIGN.md — dsh-cron
2
-
3
- ## Product / Purpose
4
- - Назначение: Интегрированный планировщик cron-задач и фоновой автоматизации для DeepSeek Harness. Позволяет запускать агентские сессии по расписанию, выполнять автоматические проверки и предоставлять визуальный интерфейс управления задачами.
5
- - Аудитория: Пользователи и операторы DeepSeek Harness, автоматизирующие периодические процессы (утренние сводки, проверка тикетов, мониторинг серверов).
6
- - Статус: Active, публичный npm-пакет @goodandready/dsh-cron (линия 0.2.x).
7
-
8
- ## User Surfaces
9
- - Web/UI:
10
- - Экран «Запланированные задачи» (полноэкранный оверлей в центральной колонке интерфейса DSH, аналогично dsh-kanban).
11
- - Раздел «Активные задачи» в сайдбаре под кнопкой плагина: сворачиваемый (свёрнут по умолчанию, состояние запоминается), до пяти строк с названием и временем следующего запуска или живым статусом, строка «и ещё N»; клик открывает панель и подсвечивает задачу (#34).
12
- - Панель фильтров в списке задач: тип, модель, канал; фильтрация мгновенная на клиенте, есть сброс, счётчик «показано N из M» и понятное пустое состояние (#40).
13
- - Кнопка вызова в боковой панели (sidebar-entry) рядом с новой сессией + иконка в шапке сессии (utilities slot).
14
- - Кнопка «Создать ⌄» с дропдауном:
15
- - 💬 «Создать с DSH» (запуск интерактивного диалога постановки задачи агенту).
16
- - ✏️ «Настроить вручную» (модальная форма: тип и параметры рантайма, расписание, таймаут, overlap, промпт, модель, каналы доставки, шаблон сообщения).
17
- - Табы фильтрации: «Все», «Активные», «На паузе», «Завершённые».
18
- - Поисковая строка; сводная статистика (активные задачи, запуски, токены, стоимость).
19
- - Карточки задач: статус-переключатель, название, расписание (человекочитаемое + raw cron), действия (запуск, редактирование, удаление).
20
- - Блок «Рекомендуемые задачи»: готовые шаблоны (Daily digest, Weekly review, Follow-up monitor) в один клик.
21
- - Карточка настроек в слоте settings.plugin.item, key = namespace `dsh-cron`; отдельный раздел настроек не используется (#102).
22
- - LLM Tools: cron_create_task (+ alias cron_schedule_task), cron_list_tasks, cron_pause_task, cron_resume_task, cron_delete_task, cron_run_task. Параметры задачи включают рантайм (`type`), `channels` (список каналов доставки) и `template` (шаблон сообщения).
23
- - API: HTTP эндпоинты /dsh-cron/* (tasks, models, chat/start, settings, telegram/test, kanban/test, tasks/:id/actions, legacy action/:id/:action). Мутирующие эндпоинты отклоняют cross-origin запросы; script-задачи по HTTP требуют заголовок x-dsh-cron-confirm. Настройки доставки принимаются как плоские ключи (webhook URL, топики, base URL) и `channelTemplates` — карта шаблонов по каналам.
24
- - Chat / Slash Commands: отсутствуют (ранее заявленные /cron-команды не были реализованы и удалены из документации; решение 2026-09-09).
25
-
26
- ## Visual Direction
27
- - Атмосфера: Строгий утилитарный интерфейс в нативном стиле DeepSeek Harness.
28
- - Использование нативных токенов темы DSH: --dsw-alias-bg-base, --dsw-alias-bg-layer-*, --dsw-alias-border-l1/l2, --dsw-alias-label-primary/secondary/tertiary, --dsw-alias-interactive-bg-hover.
29
- - Семантические статусы объявляются один раз через plugin-переменные --dsh-cron-success/danger/info/accent/warning (токен ядра при наличии, иначе фолбэк); рассыпанных hex в инлайн-стилях нет.
30
- - Иерархия: читаемые карточки с чётким акцентом на статусе и времени следующего запуска.
31
-
32
- ## Foundations
33
- - Семантические цвета статусов:
34
- - Зелёный/активный: рабочее расписание, успех.
35
- - Серый/приостановленный: на паузе.
36
- - Синий/информационный: шаблоны, Shell-тип, Telegram-акценты.
37
- - Красный: ошибка/таймаут последнего запуска.
38
- - Фиолетовый: разовая (one-shot) задача; жёлтый: skipped/missed.
39
- - Доступность: aria-label у иконок и действий, роль button + управление с клавиатуры (Enter/Space) для строк списков, Escape закрывает модалки, autofocus первого поля формы.
40
- - Язык: канонические строки — английские (словарь STRINGS.en, namespace dsh-cron, регистрация через ctx.locale.register); русский и другие языки предоставляет translation-плагин в рантайме.
41
-
42
- ## Components And States
43
- - Components:
44
- - CronSidebarButton: кнопка в левом сайдбаре DSH.
45
- - CronScreen: основной оверлей со списком, табами, статистикой и рекомендациями.
46
- - CreateDropdown: всплывающее меню выбора способа создания.
47
- - ManualTaskModal: модальная форма создания/редактирования (вкладки «Параметры» / «История запусков»); блок каналов доставки — сетка чекбоксов (Telegram, dsh-kanban, Discord, Slack, ntfy, Bark, PushPlus, Voice, Gitea) и поле шаблона сообщения с подсказкой по переменным.
48
- - SettingsModal: настройки доставки с тестами; три сворачиваемые секции — «Credentials (references)», «Delivery channels», «Message templates» (шапка-кнопка, aria-expanded, шеврон).
49
- - DeliverySettingsForm: общая форма настроек доставки, одна реализация для SettingsModal и CronSettingsCard; секреты вводятся только по имени credential-ссылки.
50
- - TaskItem: строка задачи с переключателем состояния и действиями (запуск, редактирование, дублирование, удаление); адресуется атрибутом data-task-id для подсветки из сайдбара.
51
- - ImportModal: сводка по файлу (сколько добавится, заменится, пропустится) и выбор стратегии add/replace/skip до применения (#42).
52
- - RecommendationCard: плашка с готовым шаблоном.
53
- - CronSettingsCard: карточка параметров плагина в настройках; свёрнута по умолчанию, шапка-кнопка с шевроном разворачивает тело (aria-expanded), кнопка «Открыть панель задач» — внутри раскрытого тела (решение 2026-09-10, #100).
54
- - States:
55
- - Loading: индикатор загрузки списка/истории.
56
- - Empty: дружелюбный пустой экран со списком рекомендаций.
57
- - Error: баннер с ошибкой и кнопкой повтора (в панели), статус-баннеры в модалках.
58
- - Success: статус-баннеры тестов доставки, «✓ Сохранено».
59
- - Опасные действия: удаление задачи — через confirm(); переключение типа на script по HTTP требует confirm-заголовок.
60
-
61
- ## User Flows
62
- 1. Создание через DSH-чат: «напоминай каждый день в 9 утра...» → «Создать с DSH» → агент уточняет тип (LLM/NO-LLM), расписание, модель, Silent Rule → после подтверждения вызывает cron_create_task → задача появляется на экране.
63
- 2. Создание вручную: кнопка сайдбара → «Создать ⌄» → «Настроить вручную» → форма → сохранение.
64
- 3. Выполнение по расписанию: croner/таймер one-shot → запуск по выбранному рантайму (агентская сессия, shell, node, python, http, ssh, docker) → запись в историю → доставка отчёта в выбранные каналы (Telegram, Kanban, Discord, Slack, ntfy, Bark, PushPlus, Voice, Gitea) с учётом `onlyOnFailure`.
65
- 4. Разбор инцидента: история запусков в карточке задачи → статус, длительность, вывод/ошибка.
66
-
67
- ## Locked Design Decisions
68
- - 2026-09-11 — Доставка вынесена в отдельный слой: сообщение рендерится шаблоном `{var}` (#25), транспорт — адаптеры каналов с чистым builder'ом payload и инжектируемым fetch, маршрутизатор собирает ошибки каналов и не роняет запуск (#26, #20–#23, #28, #47). Явно выбранные каналы задачи перекрывают legacy-флаги `notifyTelegram`/`kanbanMode`.
69
- - 2026-09-11 — Доставка не может заблокировать планировщик: каждый канал ограничен таймаутом (`deliveryTimeoutMs`, по умолчанию 15 с), каналы отправляются параллельно, а ошибки (включая таймаут) собираются в `failures`. Причина: `protect: true` в croner пропускал бы следующие тики, пока висит незавершённая доставка (находка независимого review PR #114).
70
- - 2026-09-11 — Блок A (0.2.5): список активных задач в сайдбаре (#34), фильтры по типу/модели/каналу (#40), адаптив до 375 px без скрытия информации (#39), дублирование задачи серверным маршрутом с принудительной паузой и сбросом состояния (#41), экспорт/импорт конфигурации задач в JSON со стратегиями и dry-run (#42), нормализация и нижняя граница таймаута доставки (#115). Экспорт — только JSON: YAML-парсера в проекте нет, а зависимость ради формата не добавляется.
71
- - 2026-09-11 — Значения с секретом внутри (webhook-URL Discord/Slack, ключ Bark) маскируются при отдаче в браузер, а замаскированное значение, вернувшееся от UI, не перезаписывает сохранённое; сырые credential-ключи отклоняются и в store, и на входе `/dsh-cron/settings`.
72
- - 2026-09-11 — Секреты доставки хранятся только как credential-ссылки (#51): настройки содержат имя credential, значение резолвится в момент отправки через DSH credentials-сервис с фолбэком на ENV; store отказывается сохранять сырые secret-ключи.
73
- - 2026-09-11 — Каталог данных плагина: `DSH_DATA_DIR` → `DSH_HOME/data` → `~/.dsh/data`. Причина: изолированный профиль не должен писать в чужой домашний каталог (issue #112, найдено на приёмке в тест-контуре).
74
- - 2026-09-09 — Пакет надёжности ядра (v0.1.24): буфер shell-задач 10МБ, атомарное сохранение с PID, аудит пропущенных запусков при рестарте (missed), фоновый поллинг UI (8с).
75
- - 2026-09-03 — Публичный скоуп @goodandready/dsh-cron; оверлей через mountSidebarEntry/mountScreen аналогично dsh-kanban; двойная кнопка «Создать ⌄».
76
- - 2026-09-09 — Слот карточки настроек: settings.plugin.item с key/namespace `dsh-cron` (совпадение с серверной регистрацией). Причина: контракт слота настроек (#85). Changed 2026-09-10 (#102): settings.section fallback удалён — карточка только во вкладке плагинов.
77
- - 2026-09-10 — Значения настроек пишутся через зарегистрированный settings-скоуп (`scope.set` → `watch` → store); REST /dsh-cron/settings остаётся транспортом для карточки и headless-сценариев; карточка проверяет статус снапшота и не рисует поля до его получения (#102).
78
- - 2026-09-09 — Английский — канонический язык строк; словарь STRINGS.en регистрируется в ctx.locale; русский — через translation-плагин. Причина: стандарт DSH-плагинов (#87).
79
- - 2026-09-09 — Same-origin проверка мутирующих эндпоинтов, лимит тела 1 МБ, confirm-заголовок для script-задач. Причина: закрытие CSRF→RCE поверхности (#86).
80
- - 2026-09-09 — Стилевая изоляция: динамические <style> с data-dsh-plugin="dsh-cron"; цвета только через токены темы + единый блок plugin-переменных. Причина: защита от очистки стилей соседями и поддержка светлой темы (#91).
81
- - 2026-09-09 — /cron slash-команды удалены из документации (не были реализованы); при появлении продукта — отдельная feature-issue и согласование. Причина: документация = истина (#84).
@@ -1,50 +0,0 @@
1
- # План: блок A — пользовательские поверхности (0.2.5)
2
-
3
- Живой план блока. Обновляется по факту работы; источник истины по задачам —
4
- Gitea (`goodandready/dsh-cron`), milestone `0.2.5`.
5
-
6
- ## Состав блока (согласован владельцем)
7
-
8
- | Issue | Задача | Сложность |
9
- |:--|:--|:--|
10
- | #34 | Сворачиваемый раздел активных задач в сайдбаре | H |
11
- | #40 | Поиск и фильтры списка задач | M |
12
- | #39 | Адаптив под узкие экраны | M |
13
- | #41 | Клонирование задачи | L |
14
- | #42 | Экспорт/импорт задач (JSON/YAML) | L |
15
- | #115 | Поле «Delivery timeout»: сброс и нижняя граница | L |
16
-
17
- Дальше по согласованию: блок B (0.2.6) — #45, #44, #43, #49, #48;
18
- блок C (0.2.7) — #54, #50, #53, #97, #121.
19
-
20
- ## Границы и инварианты
21
-
22
- - Один блок → одна ветка `feat/0.2.5-ui-block` → один PR → один релиз 0.2.5.
23
- - В релиз 0.2.5 также входит уже смёрженное удаление email-канала (#23, PR #120).
24
- - Клиентская половина остаётся single-file (`lib/client.js`) — требование загрузчика DSH.
25
- - Новых зависимостей блок не добавляет; экспорт/импорт использует существующие средства разбора.
26
- - Публичные экспорты `lib/index.js` и контракты маршрутов не ломаются: новые поля — только additive.
27
- - Секреты в экспортируемом файле отсутствуют: переносятся лишь имена credential-ссылок.
28
-
29
- ## Вне scope
30
-
31
- - Серверная авторизация и внешний API (блок C).
32
- - Изменения в путях исполнения и работе с моделью (блок B).
33
- - `#117` (мажор croner), `#95` (scope токена), `#113` (инфраструктура тест-контура) — вне блоков.
34
-
35
- ## План проверки блока
36
-
37
- - `npm test` + новые тесты на каждый пункт (контракт разметки, фильтрация, сериализация, клонирование, клампинг таймаута).
38
- - `bash deploy.sh check` — тесты, гейт размера пакета, сборка кандидата.
39
- - Приёмка на изолированном MiniPC: установка кандидата, проверка списка/фильтров/дублей/экспорта-импорта и геометрии на 375/768/1280 в браузере.
40
- - Независимое ревью PR перед merge.
41
-
42
- ## Журнал
43
-
44
- - 2026-09-11 — блок согласован (A=0.2.5, B=0.2.6, C=0.2.7), созданы milestones и ТЗ по каждой задаче.
45
- - 2026-09-11 — реализовано: #115 (клампинг таймаута), #41 (дублирование серверным маршрутом), #42 (экспорт/импорт JSON), #40 (фильтры), #39 (адаптив), #34 (аккордеон в сайдбаре). 123/123 тестов, гейт размера 23 файла.
46
- - Приёмка на MiniPC (изолированный профиль, кандидат из ветки): дубликат — копия `disk-check (copy)` в статусе paused, настройки скопированы, история и счётчики пусты; экспорт — 4 задачи, поля только конфигурации (нет status/totalTokens/lastRunAt/nextRunAt); dry-run импорта — `{add:0, replace:0, skip:4}`, импорт со стратегией add — 4 задачи, все на паузе; битый документ → 400 без изменений в хранилище. В браузере: секция «Active jobs» свёрнута по умолчанию, раскрывается, показывает 3 активные задачи с временем следующего запуска, состояние сохраняется; клик по задаче открывает панель и подсвечивает её; фильтры — 8 → 6 (type=script) → 2 (+channel=ntfy) с подписью «Showing N of 8» и сбросом; дублирование из строки — 9 задач и подтверждение «copy created and paused»; поле таймаута доставки: ввод 4000 сохраняется, очистка возвращает 15000.
47
- - Ограничение приёмки: смена ширины окна в текущем браузерном транспорте недоступна, поэтому адаптив (#39) проверен наличием и разбором media-query-правил и тестами, а не визуально на 375 px. Это единственный пункт блока без живой визуальной проверки.
48
- - 2026-09-11 — после независимого ревью (FAIL): импорт переведён на жёсткий whitelist (статус из файла игнорируется, лишние ключи отбрасываются), добавлен confirm-заголовок для документов с code-задачами, зарезервированы id `export`/`import` на создании и импорте, добавлен откат при частичном сбое записи, сайдбар и панель разведены разными атрибутами, добавлен 16px для полей на телефоне, `mountSidebarJobs` разбит на мелкие функции. Проверено живьём: без заголовка 403, с заголовком задача приходит paused с обнулёнными счётчиками и отброшенными лишними ключами; подсветка из сайдбара указывает на строку панели (`.dsh-cron-container .dsh-cron-task-highlight`). 127/127 тестов.
49
- - 2026-09-11 — релиз: bump 0.2.4 → 0.2.5, тег v0.2.5, npm publish, GitHub Release, установка точной версии в production web-профиль MiniAI и production-проверки. Закрыты #34, #39, #40, #41, #42, #115.
50
- - Отклонения от первоначального ТЗ (зафиксированы в issue): #41 — сделан отдельный маршрут вместо клиентской сборки payload (сервер знает, что конфигурация, а что состояние запуска, и это тестируемо); #42 — только JSON вместо JSON/YAML (парсера YAML в проекте нет, зависимость ради формата не добавляется); состояние фильтров не пишется в URL-хеш, чтобы не конфликтовать с роутингом SPA.