diff --git a/.changelog/v2.26.0.md b/.changelog/v2.26.0.md new file mode 100644 index 000000000..7bd991cb0 --- /dev/null +++ b/.changelog/v2.26.0.md @@ -0,0 +1,89 @@ +# Release v2.26.0 + +Released: 2026-07-02 + +## Highlights + +- **Sketch & Annotation Canvas (phase 1).** Draw and erase over any generated image, pick per-stroke color and brush size, undo/clear, then save your markup and export a flattened PNG. Works with mouse, pen, and touch down to phone widths, and is reachable from ⌘K and voice ("annotate"). +- **Tribe relationship care got proactive.** Touchpoints now log automatically from synced calendar events and messages (deterministic matching, no AI calls), a new "who needs care" surface appears as both a Proactive Alert and a dashboard widget, and the overdue-contact cadence rules are single-sourced so the page, widget, and alert can never drift. +- **Two new dashboard widgets + an interactive architecture map.** A Feeds unread digest and a cross-domain health-logging streak join the dashboard, and a new Integration Flows doc visually maps how ~60 PortOS components talk to each other across 10 end-to-end flows. +- **Catalog ingredient media now accepts uploads and voice memos.** Drag-and-drop or pick image/audio/video files, or record a voice memo that's transcribed and attached — all federated to sync peers as real bytes, with inline players for audio and video. +- **Switch project workspaces from ⌘K and voice.** A new workspace-switch action snapshots the workspace you leave and reconciles the target project's git branch, shell sessions, and scoped tasks; CoS auto-snapshots a workspace before dispatching an agent to a different repo. +- **Security: closed two DNS-rebinding TOCTOU gaps.** RSS feed fetches and catalog URL ingest (via Chrome/CDP) now pin the connection to the SSRF-vetted IP and re-verify on every redirect, so a rebinding DNS answer can't steer a fetch to the LAN or cloud-metadata after validation passes. +- **Deep-linkable selections everywhere.** Authors, music artists/albums/tracks, sharing buckets, prompt stages/variables, and JIRA reports now encode the open record in the URL — shareable, bookmarkable, reload-safe, and reachable from ⌘K/voice/back button. +- **Clear delete confirmations.** Destructive actions across rounds, universes, series, share buckets, issues/episodes, Writers Room, Ask, and the Universe Builder now show an explicit inline "Delete? / Cancel" prompt instead of a hidden second click, with comfortable 44px touch targets. +- **Accessibility sweep.** ~260 config-form label/input pairs across ~80 files now flow through a shared `FormField` wrapper so clicking a label focuses its field and screen readers announce the pairing — styling preserved verbatim. +- **UI polish.** Layout-shaped loading skeletons for the DataDog/Jira/GitHub pages, a full-width Story Builder index, design-token cleanup of the last hardcoded colors, consolidated shared formatters, and fixes for double error toasts across ~50 call sites. +- **Reliability fixes.** CoS `/do:next --swarm` orchestrators are no longer reaped mid-merge (which left PRs half-cleaned), a POST daily-reminder false-nudge after a timezone change is fixed, and a batch of v2.25.0 release-review robustness follow-ups landed. + +## Media + +- **Sketch & Annotation Canvas (phase 1): draw over any generated image, save, reload, and export a flattened PNG.** Media History and Collection image cards gained an "Annotate" action that opens a new deep-linkable `/media/annotate/:mediaKey` page — a plain `` stroke layer over the image with draw/erase, per-stroke color + brush-size, undo, and clear. Pointer events cover mouse, pen, and touch (`touch-action: none`, so drawing works at 360px phone width without scrolling the page). Strokes persist as a per-image sidecar (`data/media-sketches/`, JSON vectors stored in natural-pixel space so they restore exactly at any display size) plus a flattened PNG export, via a new Zod-validated `GET/PUT /api/media/sketches/:key` (retrievable at `/png`). Reachable from ⌘K and voice ("annotate"/"sketch"). Phases 2 (feed the markup back into img2img re-render) and 3 (blank-canvas storyboard) remain open on #2036. + +## Dev Tools + +- **New interactive Integration Flows doc at `/devtools/flows`.** A single-page visual map of how PortOS packages talk to each other: ~60 components (client pages, client services, routes & sockets, server services, server lib, data stores, external systems) laid out in columns, with 10 documented end-to-end flows — Create Image, Start a Story, Voice Command, CoS Agent Run, Scheduled Task, Start/Stop App (PM2), Shell Session, Run Backup, Peer Sync, and Self-Update. Clicking a flow dims everything else, draws its numbered hops with transport-coded edges (HTTP / SSE / Socket.IO / EventEmitter / process spawn / file+DB I/O), and annotates what's passed at each hop; clicking a node shows its file, role, and the flows it participates in. The renderer (`client/public/flows.html`, dependency-free) is driven entirely by `client/public/flows.json`, so new flows are documented by editing JSON — an integrity test (`client/src/lib/flowsDoc.test.js`) verifies every referenced node/transport resolves and every referenced source file still exists, so refactors can't silently rot the doc. Launchable from the Dev Tools sidebar, ⌘K ("Flows"), and voice nav; selected flow is deep-linkable via `?flow=`, and "Open full screen" opens the standalone doc in a new tab. + +## Security + +- **RSS feed fetches now pin the connection to the SSRF-vetted IP, closing a DNS-rebinding TOCTOU.** `feeds.js` previously resolved a feed hostname, verified it wasn't private, then made a *separate, unpinned* fetch — undici re-resolved at connect time, so an attacker's DNS could answer public during validation and private (home-network / cloud-metadata) at the actual connect. Feed fetches now route through the shared `safeUrlFetch` guard (the same one moodBoard/Pinterest uses since #1859): the hostname is resolved once and the connection is pinned to that exact vetted address, re-validated and re-pinned on every redirect hop. `safeUrlFetch` gained a strict `blockPrivate` mode so feeds keep their historical block-all-private posture (an arbitrary user-added feed URL still can't be aimed at the LAN) while gaining the pin, and a `throwOnUnsafe: false` option so the feeds path returns its existing friendly "Could not fetch feed" result instead of a bubbled 400 (#2046). +- **Catalog URL-ingest (Chrome/CDP) now verifies Chrome's actual connection IP, closing the last DNS-rebinding gap.** Catalog "ingest from URL" pre-checked the hostname with a `dns.lookup` but then handed the fetch to Chrome, which re-resolves DNS itself — so a rebinding answer could steer Chrome to a loopback/link-local/cloud-metadata address after validation passed (a TOCTOU the undici pin in #2046 couldn't cover for a browser). Ingest now navigates through a pinned path (`navigateToUrlPinned`) that opens a blank tab, subscribes to CDP Network events, then drives the navigation — verifying against Chrome's OWN reported `remoteIPAddress` that every main-frame hop (initial request, each HTTP redirect, AND any client-side navigation that fires during the post-load settle window) connected to a non-blocked address, failing closed (tab torn down, ingest refused) on any disallowed hop or a top-level navigation still in flight. The DOM read runs on the SAME pinned CDP session and is re-verified after the read, so no late navigation can slip between "stop monitoring" and "read the page" (#2068). + +## Workspace Contexts + +- **Switch project workspaces from ⌘K and voice, and CoS now auto-snapshots the workspace it leaves.** A new `workspace_switch` action ("switch workspace to BookLoom", "restore my PortOS context") saves a snapshot of the workspace you're leaving, then reconciles the target project's saved context — git branch, in-repo shell sessions, scoped tasks — and reports what's still live to re-attach; it appears in the ⌘K palette and resolves by voice. When CoS dispatches a coding agent against a different app/repo than it was last working in, it now silently snapshots the previous workspace's context first (snapshot-only — no auto-restore, no LLM calls), so switching between repos preserves what each project looked like, visible on the Workspace Contexts page (#2035). + +## Catalog + +- **Catalog ingredient media now accepts file uploads and voice memos, replacing the two "coming soon" stubs.** The ingredient Media panel gained an "Upload file" picker + real drag-and-drop of image/audio/video files (dropping a file, not just an in-app gallery tile, now attaches it) and a "Record memo" button that captures a WAV via the browser, transcribes it through the existing Whisper pipeline, and attaches the audio with its transcript in the caption. Uploads land in the matching federating library dir (images as PNG, audio, video) so a sync peer receives the bytes, not a broken reference — the same two-layer path gallery attachments already use. Audio and video attachments now render inline players in the panel (#2038). + +## Tribe + +- **Tribe touchpoints now auto-log from synced calendar events and messages (#2033).** The Tribe model always supported `source: 'calendar'|'message'` touchpoints but nothing produced them. You can now record known emails/handles on a person (new "Emails & handles" field), and whenever a calendar sync or message sync ingests an event/message involving that person, PortOS logs a touchpoint automatically — deterministic email/handle matching (plus exact unique-name fallback), no LLM calls. Calendar touchpoints dedupe per event id and only count events that have already happened; message touchpoints dedupe per thread + day so a long thread doesn't spam the timeline. Auto-logged touchpoints advance `last_contact_on` so overdue/cadence status reflects them, and the Tribe timeline shows each touchpoint's source with an icon (Calendar / Message / Manual). Matching is pure and unit-tested (`server/lib/tribeMatch.js`); idempotency is enforced by a partial unique index on `(person_id, dedupe_key)`. +- **"Who needs care" now surfaces outside the Tribe page — as a Proactive Alert and a dashboard widget (#2032).** Overdue-contact status is now computed server-side (`getCareSummary`/`personCadenceStatus` in `server/services/tribe.js`, exposed at `GET /api/tribe/care`) as a single source of truth mirroring the Tribe page's cadence logic. Proactive Alerts gains a `checkTribeCadence()` check that emits "N people are overdue for contact" linking to `/tribe` (quiet when none are), and a new gated `Tribe Care` dashboard widget lists the most-overdue people (or an "all caught up" state), hidden entirely on installs with no Tribe people. + +## Delete confirmations + +- **Destructive actions now show a clear "Delete? / Cancel" prompt instead of a hidden second click.** Deleting a round, universe, series, share bucket, issue/episode, or a Writers Room work/folder — and removing an Ask conversation or deleting a world in the Universe Builder — used to silently re-arm the same button on the first click, with nothing on screen telling you a second click was needed. Each of these now pops an explicit inline confirm/cancel affordance right where you clicked, so it's obvious what will happen and easy to back out. The pipeline's "replace existing scenes / pages / audio lines" extract buttons got the same treatment (an inline "Replace? / Cancel" row) instead of arming via a fleeting toast. Delete/confirm controls in the Writers Room library were also enlarged to a comfortable 44px touch target for mobile. + +## Deep-linkable selections + +- **Picking an author, music artist/album/track, share bucket, prompt stage/variable, or JIRA report now lives in the URL — so it's shareable, bookmarkable, reload-safe, and reachable from ⌘K / voice / the back button (#2025).** These master-detail pages previously kept the open record in local state, so a pasted link or a reload always dropped you back on an empty list. Authors now open at `/authors/:id` (`/authors/new` to create), the Music tabs at `/music/:tab/:id`, Sharing buckets at `/sharing/buckets/:bucketId`, the Prompt Manager via `?stage=` / `?var=`, and JIRA Reports via `?reportApp=&reportDate=`. Deleting or clearing a selection returns to the index, and a stale/deleted id now shows a "could not be found" fallback instead of a blank pane. (OpenClaw's session picker stays local by design — its sessions are ephemeral runtime state, not user-owned records.) + +## Accessibility + +- **Config form labels now focus their field when clicked and read correctly to screen readers.** Many settings/config forms (AI Providers, DataDog, feature-agent config, message & calendar account setup, scheduled-task provider/model pickers, the agent world/schedule tabs, MeatSpace nicotine + POST drills, and more) rendered the label as a plain sibling of its input with no association, so clicking the label did nothing and assistive tech couldn't announce the pairing. These fields now flow through a shared `FormField` wrapper that generates a stable id and wires `htmlFor`/`id` automatically, keeping the exact same styling (#2027). Remaining forms are tracked for a follow-up sweep (#2051). +- **The follow-up sweep wired the remaining config-form labels to their controls.** Continuing #2027, ~130 more sibling `