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:
228
static/style.css
228
static/style.css
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user