Cookbook scheduler + serve: schedule via Tasks, Stop verifies kill, Ollama auto port-pick

- Schedule cookbook serves through the existing ScheduledTask system: the
  serve preset gets a ^ button next to Launch that opens a daily/hourly/
  weekly form mirroring the admin-switch style; the schedule action runs
  action_cookbook_serve, which delegates to /api/model/serve and stamps
  the resulting task with _scheduledStopAtMs. A background
  cookbook_serve_lifecycle loop ticks every 60s and kills any serve
  whose window has ended, also dropping the auto-registered endpoint
  so the model picker doesn't keep pointing at a dead server.
- Stop and remove on a Running serve now awaits the SSH/tmux kill,
  re-checks tmux has-session, and surfaces an error toast (leaving the
  row) when the kill failed. Previously fire-and-forget, so a failed
  SSH/tmux call silently left the live serve running while the row
  vanished from the UI.
- Cookbook tasks/status orphan-adoption sweep no longer requires the
  serve-/cookbook- session-id prefix; any tmux session whose pane is
  running a known model-server process gets auto-pulled into Running.
  Without this loosening, a cookbook-launched serve whose tmux id
  fell back to a bare number was invisible — you couldn't see it,
  let alone stop it.
- Ollama serve always launches a fresh process under cookbook's tmux
  (no more monitor-mode reattach to a systemd/Docker ollama Stop can't
  reach). The handler pre-picks a free port by probing the target
  host over SSH and mutates req.cmd's OLLAMA_HOST so the runner script
  AND the auto-registered endpoint agree on the same bind port.
- Auto-register uses host.docker.internal (when running inside Docker)
  instead of localhost, matching the URL /setup adds for Ollama by
  hand. Local cookbook serves now produce a chat-reachable endpoint
  on first launch.
- Cascade-delete: removing a scheduled cookbook task also deletes any
  linked calendar event (cookbook_task_id marker in the description).
- Tasks list groups cookbook_serve under a "Cookbook" category that
  sorts above the rest, so scheduler-launched serves are easy to find.
This commit is contained in:
pewdiepie-archdaemon
2026-06-05 14:41:43 +09:00
parent f8aaeab245
commit e2f449f4ef
12 changed files with 1434 additions and 67 deletions

View File

@@ -14059,8 +14059,28 @@ body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
.admin-model-form-row {
display: flex;
gap: 6px;
flex-wrap: wrap; /* let buttons drop to a new row on narrow widths
instead of overflowing the modal */
align-items: center;
}
.admin-model-form-row input {
flex: 1 1 180px; /* api-key input takes the long axis but allows
dropping below the buttons at small widths */
min-width: 0; /* don't refuse to shrink past content width */
}
.admin-model-form-row select {
flex: 0 0 auto;
}
.admin-model-form-row button {
flex: 0 0 auto;
white-space: nowrap;
}
/* On narrow screens, give buttons their own row + push the Add button
to the right so it remains the obvious primary action. */
@media (max-width: 540px) {
.admin-model-form-row input { flex-basis: 100%; }
.admin-model-form-row .admin-btn-add { margin-left: auto; }
}
.admin-model-form-row input { flex: 1; }
.adm-ep-inline-msg {
min-height: 16px;
margin-top: 5px;
@@ -18219,8 +18239,12 @@ body.gallery-selecting .gallery-dl-btn,
color: var(--fg-muted);
letter-spacing: 0.2px;
}
/* "running" pill on a Serve-tab card when the model has a live serve task. */
.cookbook-serve-running-pill {
/* Status pills shown inline in a Serve-tab card title (next to the model
name) when the model has a live serve / download task. Shared base
class so "running" and "downloading" sit on the same row with the
same chrome; only color varies. */
.cookbook-serve-running-pill,
.cookbook-serve-downloading-pill {
display: inline-block;
margin-left: 6px;
padding: 1px 7px;
@@ -18231,11 +18255,50 @@ body.gallery-selecting .gallery-dl-btn,
letter-spacing: 0.3px;
vertical-align: 2px;
position: relative;
top: -1px;
top: 1px; /* nudged down 2px from the old -1px so it sits
flush with the cap-height of the title text */
}
.cookbook-serve-running-pill {
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
}
.cookbook-serve-downloading-pill {
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
opacity: 0.85;
}
.cookbook-serve-downloading-pill.is-stalled {
/* Stalled downloads stay visible but read as warning, not progress. */
color: var(--fg-muted, #888);
background: color-mix(in srgb, var(--fg-muted, #888) 10%, transparent);
border-color: color-mix(in srgb, var(--fg-muted, #888) 30%, transparent);
opacity: 1;
}
.cookbook-serve-running-pill.is-clickable {
cursor: pointer;
transition: background 0.12s, border-color 0.12s;
}
.cookbook-serve-running-pill.is-clickable:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
}
/* Brief highlight on the matched task card when jumping from the
running pill, so the user can spot it among a long list. */
.cookbook-task-flash {
animation: cookbook-task-flash-anim 1.6s ease-out;
}
@keyframes cookbook-task-flash-anim {
0% { box-shadow: 0 0 0 2px var(--accent, var(--red)); }
100% { box-shadow: 0 0 0 2px transparent; }
}
/* Cookbook header "downloading" status label sits 2px too far left
against the rest of the cookbook chrome nudge it right. */
#cookbook-bg-status {
left: 2px;
}
.cookbook-serve-dir-edit {
font-size: 9px;
color: var(--fg-muted);
@@ -33535,8 +33598,15 @@ button.cal-view-btn {
of days it covers within the row, --slot stacks parallel bars. */
.cal-multiday {
position: absolute;
left: calc(var(--col, 0) * (100% / 7));
width: calc(var(--span, 1) * (100% / 7));
/* Fractional offsets let timed events that cross midnight render at
true proportional width. start-frac shifts the left edge into the
first day; end-frac trims the right edge inside the last day.
All-day events default to (0, 1) and still fill whole columns. */
left: calc((var(--col, 0) + var(--start-frac, 0)) * (100% / 7));
width: calc(
(var(--span, 1) - var(--start-frac, 0) - (1 - var(--end-frac, 1)))
* (100% / 7)
);
top: calc(2px + var(--slot, 0) * 12px);
height: 11px;
font-size: 8px;
@@ -35925,3 +35995,149 @@ body.theme-frosted .modal {
padding: 10px 18px;
border-top: 1px solid var(--border);
}
/* Cookbook serve panel: Launch + ^ split button pair */
.hwfit-serve-launch-group {
display: inline-flex;
align-items: stretch;
vertical-align: middle;
}
.hwfit-serve-launch-group .hwfit-serve-launch {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
margin-right: 0 !important;
}
.hwfit-serve-launch-group .hwfit-serve-schedule-arrow {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-left: 1px solid var(--border) !important;
padding: 0 8px !important;
min-width: 26px;
display: inline-flex !important;
align-items: center;
justify-content: center;
}
/* Schedule form mounted inside the cookbook serve panel. Uses the
theme tokens (--bg, --panel, --border, --accent, --red) so it
matches the rest of the cookbook chrome instead of inline whites. */
.hwfit-schedule-form {
margin: 10px 0 4px;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
display: flex;
flex-direction: column;
gap: 10px;
}
.hwfit-schedule-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
opacity: 0.95;
flex-wrap: wrap;
}
.hwfit-schedule-title svg { opacity: 0.7; flex-shrink: 0; }
.hwfit-schedule-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hwfit-schedule-title-spacer { flex: 1; min-width: 8px; }
.hwfit-schedule-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.hwfit-schedule-field {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 11px;
opacity: 0.75;
}
.hwfit-schedule-field input[type="time"] {
font: inherit;
min-width: 90px;
}
.hwfit-schedule-label {
font-size: 11px;
opacity: 0.75;
}
.hwfit-sched-days {
display: inline-flex;
flex-wrap: wrap;
gap: 5px;
}
.hwfit-sched-day-chip {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--border);
background: transparent;
color: inherit;
font: inherit;
font-size: 10px;
line-height: 1;
padding: 0;
cursor: pointer;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.hwfit-sched-day-chip:hover { border-color: var(--accent, var(--red)); }
.hwfit-sched-day-chip.is-on {
background: var(--accent, var(--red));
color: var(--panel, #fff);
border-color: var(--accent, var(--red));
}
.hwfit-schedule-actions-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.hwfit-schedule-actions-spacer { flex: 1; }
/* Days row hosts the day-chip strip on the left and the Cancel / Save
action buttons on the right. The spacer between them pushes the
actions to the right edge. */
.hwfit-schedule-days-row {
align-items: center;
}
/* Cancel + Save sit on the right side of the days row with matching
icon-plus-label chrome. */
.hwfit-sched-cancel,
.hwfit-sched-save {
display: inline-flex !important;
align-items: center;
}
.hwfit-schedule-mirror-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
opacity: 0.9;
cursor: pointer;
user-select: none;
}
.hwfit-schedule-mirror-label { white-space: nowrap; }
.hwfit-schedule-mirror-switch { transform: scale(0.85); transform-origin: left center; }
.hwfit-sched-err {
font-size: 12px;
color: var(--red, #ff6b6b);
display: none;
}
.hwfit-sched-err.is-visible { display: block; }
@media (max-width: 600px) {
.hwfit-schedule-row { gap: 8px; }
.hwfit-sched-day-chip { width: 36px; height: 36px; font-size: 11px; }
}
/* Brief outline flash on a note card when it's the target of a
#note-<id> link click from chat same pattern as the cookbook
task flash, just scoped to .note-card. */
.note-card-flash {
animation: note-card-flash-anim 1.6s ease-out;
}
@keyframes note-card-flash-anim {
0% { box-shadow: 0 0 0 2px var(--accent, var(--red)); }
100% { box-shadow: 0 0 0 2px transparent; }
}