Skip to content

Latest commit

 

History

History
490 lines (332 loc) · 81 KB

API.md

File metadata and controls

490 lines (332 loc) · 81 KB

Local API

Optional account connections

All connection routes require an authenticated dashboard session. GET /api/connections returns redacted {chatgpt, services, settings} status without external requests. Every /api/connections/* authorization, preference, model-list and refresh subroute is restricted to loopback. Writes also require the normal same-origin/dashboard headers.

Method Route Body / result
POST /api/connections/chatgpt/login Optional {registrationId}; returns {authUrl,expiresAt} for the pending OpenAI OAuth flow
POST /api/connections/chatgpt/cancel {}; cancels the pending sign-in
GET /api/connections/chatgpt/models {models:[{id,name}]} from the connected account's catalog
POST /api/connections/chatgpt/account {id} selects a saved registration and returns conversation selection to Ollama
DELETE /api/connections/chatgpt Disconnects, attempts revocation, returns {revoked,...snapshot} and selects Ollama
PUT /api/connections/chatgpt/preferences `{conversationProvider:"ollama"
PUT /api/connections/services A nonempty subset of {githubUsername,githubEnabled,netlifyEnabled}; saves flags without external requests
POST /api/connections/services/github/refresh {}; reads public repositories when enabled
POST /api/connections/services/netlify/refresh {}; reads sites using the existing local Netlify CLI account when enabled

These routes never return account tokens. ChatGPT credentials and service metadata are stored outside jarvis.json; ordinary JSON export excludes both connection files. The core settings endpoint does not accept conversation-provider changes. The shared /api/chat router still handles local built-in commands first; explicitly selected cloud conversation returns mode:"chatgpt". Only completed cloud responses are stored as successful ChatGPT exchanges. See CONNECTIONS.md for the exact account, history, privacy and usage boundaries.

These routes are implemented in server/app.js. The browser and API share the same origin, normally http://localhost:3210 on the server and http://SERVER_IPV4:3210 on another device. The optional parallel HTTPS listener normally uses port 3443. The API is intended for the local dashboard. It has no public cloud endpoint.

Session and request rules

GET /api/session is available before authentication and returns { authenticated, local, assistantName }. To create a session, send POST /api/session with {} from loopback or { "code": "CURRENT_PAIRING_CODE" } from a network client.

All API requests using methods other than GET or HEAD require X-Jarvis-Client: dashboard. Requests with JSON bodies also require Content-Type: application/json; JSON bodies must be objects and are limited to 32 KB. Voice transcription is the binary exception described below. Browser writes must come from the dashboard's own origin; cross-site writes are blocked. The Host header must identify localhost, the server's current host name, or one of its current IPv4 addresses, at that listener's active port.

The session is an HttpOnly; SameSite=Strict cookie with a fixed 12-hour lifetime. HTTPS uses a separate cookie with the Secure flag. Pair HTTP and HTTPS separately and send the relevant cookie with subsequent requests. Pairing attempts from network clients are limited. Unexpired cookies survive backend restarts and updates for the remainder of their original lifetime; ordinary reads and restarts never renew that deadline. DELETE /api/session durably revokes the current session before clearing its browser cookie, so it remains revoked after restart. There is no separate bearer-token API.

Session persistence uses owner-only .sessions.json in the active data directory, storing only SHA-256 token hashes and expiration timestamps. Raw cookie tokens are never written to that file; a stored hash cannot itself authenticate. The registry is excluded from /api/state, /api/export, storage-file listings and public source routes. It is separate from jarvis.json; malformed or unsafe registry files fail startup without being overwritten. Pairing, Host/Origin/header checks and the original session duration are unchanged. An upgrade from the earlier volatile registry can require one dashboard refresh and re-pair because those old in-memory tokens cannot be recovered. Navigation/follower signals and pending creation drafts still reset on restart rather than replaying earlier actions.

The default pairing code is random per backend start. An optional private pairing-code.txt in the active data directory supplies a persistent chosen code; it is read only at startup and is not part of the JSON app records. Any configured workstation value is omitted from this public source. The override must be a regular file of at most 256 bytes containing 4–100 ASCII letters, numbers, hyphens, or underscores; a final newline is allowed. Keep it mode 600, stop the backend before changing it, and restart to apply it. See the setup instructions.

Routes

Except for the session routes, all routes require a valid session. Pairing-code retrieval additionally requires a loopback client.

Method Path Behavior / body
GET /api/session Authentication state and local/network classification
POST /api/session Create session; {} locally, { code } over LAN
DELETE /api/session End the current session and clear its cookie
GET /api/state Settings, saved jobs, client prospects, Software projects, websites, alerts, School tasks/classes, Calendar, activity, and integration states
GET /api/status Timestamped host CPU, memory, disk, uptime, network, and app-storage readings
GET /api/weather Current weather and today's forecast for the saved location; cached provider snapshot with explicit ready/stale/unavailable status
GET /api/weather/locations?q=QUERY Search public town/city names; returns {locations} with at most five normalized choices
PUT /api/weather/location Save a complete {name,latitude,longitude,timeZone} location; returns {location}
GET /api/navigation Return {navigation,serverTime} with null or the latest short-lived section/control request; renew this authenticated session’s recent-follower presence
POST /api/navigation/view Report exact {viewId,section,visible} for one authenticated dashboard tab; return the same fields
GET /api/pairing Current pairing code; authenticated loopback only
GET /api/security Access/configuration information and capability flags
GET /api/software/projects Return {projects} for the saved local project catalogue
POST /api/software/projects Save one reviewed descriptive project record; return HTTP 201 {project}
PUT /api/software/projects/:id Partially update only priority, planning stage and/or notes; return {project}
PUT /api/settings Partial update of assistantName, wakeName, voiceDevice, speechVoice, speechSpeed, websiteSpokenAlerts, refreshSeconds, ollamaEnabled, ollamaModel
PUT /api/job-preferences Save { keywords, location, remoteOnly }
GET /api/integrations Connection/scaffold states
POST /api/integrations/ollama/test Probe the fixed local Ollama service and return installed models
GET /api/chat Saved chat messages
POST /api/chat Send { message, viewId? }; returns {reply,mode,serverTime,action?,dashboardNavigation?,pageReview?,awaitingPageReviewInput?,awaitingCalendarInput?,awaitingPlannerInput?,awaitingClientInput?,awaitingReminderInput?,awaitingResearchInput?,awaitingAssistanceInput?,assistance?,sources?}
POST /api/chat/start Generate a short assistant opener with optional { topic: "general" }; returns { reply, mode: "proactive", topic }
GET /api/voice Local speech-engine availability, installed voice choices, and recording-duration limit
GET /api/voice/native Sanitized heartbeat of the local Mac companion, used to avoid duplicate browser wake listeners
GET /api/briefings/morning Saved morning-conversation status, Calendar time zone, morning window, last claimed local date, and localhost capability
PUT /api/briefings/morning Loopback-only exact {enabled:boolean}; returns status without claiming a greeting
POST /api/briefings/morning/unlock Loopback-only exact {} from the native app after successful voice-code unlock; claims at most one local morning and returns a local saved-data briefing when eligible
GET /api/room-guard Room-guard preference, safe native runtime status, saved clip metadata, localhost capability, and keep-awake status
PUT /api/room-guard Loopback-only {armed:boolean}; returns the same snapshot as GET
GET /api/room-guard/native Authenticated loopback-only fixed configuration for the native Mac recorder, including its private recording directory
GET, HEAD /api/room-guard/recordings/:id Stream a finalized private video to authenticated dashboards; supports a single byte range
DELETE /api/room-guard/recordings/:id Loopback-only explicit clip removal; returns {removed:true,id}
GET /api/power Return sanitized Mac keep-awake preference and current AC/assertion status
PUT /api/power Loopback-only {enabled:boolean}; enable/disable the AC keep-awake helper and return its status
GET /api/voice/android Sanitized current Android microphone ownership: { active, label }
POST /api/voice/android/acquire Claim a 15-second Android microphone lease with { label }
POST /api/voice/android/heartbeat Renew this session's existing Android microphone lease with {}
DELETE /api/voice/android Release this session's Android microphone lease
GET /api/voice/ca-certificate Download the fixed project-local public CA certificate after HTTPS setup; requires a paired session
POST /api/voice/transcribe Binary audio/wav upload; returns { text, engine, durationMs }
POST /api/voice/speak JSON { text, voice? }; returns generated audio/wav
POST /api/jobs Save a job; fields described below
PATCH /api/jobs/:id Partial update of a saved job
DELETE /api/jobs/:id Remove a saved job
POST /api/jobs/search Search public listings with {keywords,location,remoteOnly}; return source-backed results without saving or applying
GET /api/clients Return {clients} for saved local prospects
POST /api/clients Save a reviewed prospect; return HTTP 201 {client}
PUT /api/clients/:id Partially update a prospect’s fields/notes/progress; return {client}
DELETE /api/clients/:id Remove a prospect; return {ok:true}
PUT /api/client-preferences Save {businessType,location,radiusKm} for generic voice client requests and initial Clients form values
GET /api/clients/directory Return installed local-directory availability, source date, coverage, record counts, and attribution
POST /api/clients/search Search the free local Ohio index with {businessType,location,radiusKm?}; return evidence without saving or contacting
POST /api/clients/:id/draft Return {draft} containing a local outreach template; no sending or progress change
POST /api/websites Save {name,url,isPrimary?,monitoring?}; the URL must be public HTTP(S)
PUT /api/websites/:id Partially update site identity and monitoring preferences
POST /api/websites/:id/check Run a fresh check and save its result
POST /api/websites/check-all Check saved websites with up to three concurrent checks
DELETE /api/websites/:id Remove a saved target
GET /api/website-alerts Return {alerts} with retained website check alerts
PATCH /api/website-alerts/:id Acknowledge with only {read:true}; return {alert}
POST /api/website-alerts/:id/claim Loopback-only native speech attempt claim; return {alert} or {alert:null}
GET /api/school/classes Return {classes} for weekly class schedules
POST /api/school/classes Save a class and its meetings together; return HTTP 201 {class}
PUT /api/school/classes/:id Update a class and its meetings together; return {class}
DELETE /api/school/classes/:id Remove a class and its Calendar meetings; return {ok:true}
GET /api/export Download JSON app records; excludes pairing codes, the private pairing-code file, and session credentials

Record fields and limits

Jobs accept title and company (required, up to 150 characters each), url (optional complete HTTP/HTTPS URL), location (up to 200), notes (up to 4,000), and stage. Stages are saved, applied, interview, offer, and closed; new jobs default to saved. The backend adds an ID and creation/update timestamps. The tracker allows up to 500 records.

Website names are up to 100 characters. Targets are canonicalized HTTP/HTTPS URLs without embedded credentials or fragments. Local/private/reserved addresses are blocked, including on DNS resolution and redirects. Each check has an 8-second deadline and follows up to four redirects. Results include checkedAt, status (up, down, or blocked), statusCode, responseMs, finalUrl, and error. There is a limit of 50 targets, and duplicate URLs return a conflict. Automatic monitoring and main-business metadata are described below.

Assistant names are 1–40 characters; wake names are 1–60. refreshSeconds is a whole number from 2 to 60. ollamaEnabled is boolean, and enabling it requires a nonempty ollamaModel. A model name may be up to 120 characters. These settings do not download or install a service.

Chat messages are limited to 4,000 characters. Built-in commands return mode: "command"; successful local-model replies return mode: "ollama". A navigation result contains an action such as { "type": "navigate", "section": "jobs" }. The UI acts on only its known sections. There is no shell-command action.

Local capability and voice-command help

POST /api/chat handles complete help requests such as “what all can you do?”, “tell me what you can do,” “remind me of your voice commands,” “what can you control?” and “how can I navigate my dashboard with my voice?” before ordinary model conversation. Replies use mode:"command" and a bounded local overview or navigation guide, with no action or new navigation signal. Help reads no private task records, calls no model, and cannot save an event, client, assignment or reminder. Matching requires a complete recognized request; quoted instructions, negation, arbitrary extra actions, URLs and code are not interpreted as capability commands. See server/capabilities.js and VOICE-COMMANDS.md.

General Ollama and ChatGPT conversation retains the existing application-command reference. Its instructions distinguish the Jarvis application’s deterministic commands from the conversational model’s own execution authority. Model prose cannot create an action, navigate a screen, or confirm a save.

Shared dashboard navigation

Authenticated GET /api/navigation returns {navigation:null,serverTime} when no current command exists. serverTime is the current server ISO timestamp, also supplied on ordinary /api/chat results. Otherwise its event is exactly one of {id,section,createdAt} or {id,control,createdAt}. Legacy section events keep their original shape; a control event has no section. Each uses a generated UUID, an allowlisted section or control and a server ISO timestamp. It contains no chat text, records, project source paths, selectors, script or arbitrary URL. A successful read registers/renews this authenticated session as a follower for 15 seconds; the in-memory registry accepts at most 100 recent sessions. followingScreens counts recent sessions, not physical displays or tabs. Logout and session expiry release presence; backend restart clears presence and navigation.

When a built-in POST /api/chat result has {action:{type:"navigate",section}}, the server publishes a new event and adds dashboardNavigation:{id,section,createdAt,followingScreens} to that reply. Signals expire after 60 seconds and are never written to jarvis.json or exports. The allowed sections are dashboard, jobs, clients, calendar, school, websites, files, servers, security, software, stocks, and settings. Natural commands such as “open School,” “show my calendar,” “take me to Software,” “open up my school page,” “bring me to websites,” “switch over to stocks,” “take me into my calendar” and complete labels such as “Calendar page” resolve only to these known sections. The parser accepts bounded polite prefixes, suffixes and known page aliases; unrelated text and multiple actions do not become navigation. Model-generated prose cannot supply a URL or launch a project. There is no general navigation-write route.

The frontend polls this lightweight route every two seconds, independently of telemetry refresh, while the target dashboard is open, paired and visible. Its first successful read establishes a baseline without replaying an existing event. It deduplicates IDs, ignores stale/malformed/unknown events and rejects records containing both a section and a control. The backend retains only its latest signal for up to 60 seconds; commands issued between reads may coalesce. The client accepts section requests for 60 seconds and control requests for only 10 seconds, including deferred time. Expired requests are not replayed after returning to a hidden tab. The tracker calibrates its time comparison from the response’s strict UTC serverTime, then advances that estimate with locally elapsed time. A TV computer whose clock differs from the Mac therefore does not silently reject fresh short-lived controls. Calibration does not extend the lifetimes: events more than five seconds in the future or older than the applicable limit are still rejected against the estimated server clock; deferred requests expire on that same basis. Missing or malformed serverTime is not used as a calibration value.

Before applying section navigation, it fetches current app state. An open modal, dirty form, focused input, in-flight send or active recording/playback defers navigation and shows a notice. It rechecks these guards after the state request, so an intervening edit is preserved. The notice’s Open action honors the same guards. Display controls apply matching guards according to their effect: scrolling and zoom do not discard field values; closing a panel requires clean work, and navigation/editor/refresh operations defer while protected work is active. Background browser timer throttling can delay following.

Fixed display controls

A recognized POST /api/chat display request returns mode:"command", action:{type:"dashboard-control",command:CONTROL} and dashboardNavigation:{id,control,createdAt,followingScreens}. It uses the same authenticated chat route and request checks; there is no general control-write route. Recognition is deterministic and runs before model inference, while existing pending confirmation/cancellation takes priority. A display request cannot become a missing spoken-draft title: fields are retained and its previous confirmation preview is invalidated. The next confirmation repeats that preview before a write.

The exact CONTROL allowlist is:

scroll-down, scroll-up, page-down, page-up, scroll-top, scroll-bottom,
auto-scroll-down, auto-scroll-up, scroll-stop,
back, forward, tv-on, tv-off, command-open, panel-close,
calendar-next, calendar-previous, calendar-today, refresh,
zoom-in, zoom-out, zoom-reset, fullscreen-on, fullscreen-off,
add-calendar, add-school, add-website-task, add-website, add-job

There are 29 controls. scroll-down/scroll-up and the existing auto-scroll-down/auto-scroll-up aliases start continuous movement at 30 pixels per second. page-down/page-up move a finite page step. scroll-stop accepts exact “stop,” “stop scrolling,” “stop the scroll” and “stop moving.” The browser reports its active movement with a Scrolling slowly badge and Stop button. Fractional scroll positions are retained between animation frames so whole-pixel rounding in a TV browser cannot discard every small step.

Examples include “scroll down,” “scroll down a little bit,” “scroll down for me,” “scroll down on Files,” “scroll the Files page down,” “I want you to scroll down slowly,” “page down,” “stop,” “turn on TV view,” “next month,” “show today,” “open the command center,” and “open new calendar event form.” “Scroll slowly” and “keep scrolling” default down; “scroll down until you hear me say stop” also maps to the fixed continuous-scroll request. Optional polite prefixes and a trailing “please” are accepted, after the normal wake-prefix stripping. The verified recognition variant “scroll down a little bell on my Files page” also matches the finite grammar. Known page nouns label the current surface and do not add an implicit navigation action or alter the event union. “What can you control?” returns local help without publishing a control event. Unknown commands, extra actions, selectors, code and URLs cannot become controls. User/assistant chat text retains its normal local history behavior; only the transient signal is excluded from app data and exports.

Replies describe requested display operations. With zero recent followers, the response asks the user to open and pair a dashboard. New-record controls open normal forms and never submit them; no display control deletes records. TV view works entirely through the fixed tv-on/tv-off controls. fullscreen-on opens a tap-required prompt because a browser gesture is needed for entering native full screen; the reply explicitly warns about that step. fullscreen-off can exit an active full-screen display. The commands do not grant arbitrary computer or browser automation.

Native companion 1.1.8 / build 10 adds a narrow cancellation capability after a fresh accepted continuous-scroll action. While unlocked, exact “stop” or “stop scrolling” may reach scroll-stop after the 90-second follow-up window has ended. It does not accept arbitrary wake-free commands, arm from an unaccepted transcript, bypass the voice code gate or change draft confirmation. Pause, sleep, lock and invalidation clear the capability. The companion otherwise sends the usual command request; paired browsers receive and execute the allowlisted control. The section-opening browser fallback described below applies only to a valid navigation section, not to a control.

Mac companion 1.1.5 / build 7 reads dashboardNavigation from its normal command reply. With a matching recent-follower response it lets the open dashboards follow. Otherwise it opens only http://localhost:3210/#SECTION for an allowlisted section in the Mac’s default browser. This fallback does not accept remote URLs, inspect arbitrary folders or prove that a browser/physical Samsung screen is currently visible. Navigation transport and native fallback checks do not replace an on-device microphone/screen test.

Saved-page reading and feedback

Authenticated POST /api/navigation/view accepts exactly {viewId,section,visible} with a UUID viewId, one of the twelve known dashboard sections, and boolean visible. It follows the standard JSON, same-origin and X-Jarvis-Client rules and returns those three fields. Unknown keys/types/sections are rejected. Per-session, per-tab reports remain in backend memory for page resolution; a visible report is usable for 15 seconds. They do not publish navigation, prove a physical display is watched, or enter app exports. Logout clears the session's reports; restart clears all reports. The bounded registry can return HTTP 429 when fresh visible reports fill its capacity.

POST /api/chat accepts an optional UUID viewId; only message and viewId keys are accepted. The browser supplies its per-tab ID and reports its actual rendered section. A fresh, visible report for that session and tab wins. An unknown, hidden, or stale explicit tab cannot inherit another screen's page. With no viewId, as on the native voice path, the server resolves a section only when all fresh visible reports agree. Otherwise the reply asks which section. Explicit recognized requests such as “read my School page” choose that allowlisted section without requiring view presence or publishing a screen change.

Recognized local requests include “read this page,” “give me feedback on this page,” “review my Websites page,” and “what should I focus on in my School page.” They return mode:"command", pageReview:{section?,kind:"read"|"feedback",engine} and boolean awaitingPageReviewInput. Engine values are saved-records for a plain readout, local-model for a valid Ollama suggestion order, local-guidance for deterministic saved suggestions, and clarification when a section is needed. Here local-model means constrained suggestion ranking only; the UI labels it PAGE FEEDBACK / LOCAL PRIORITIES. Saved assistant chat entries retain this metadata so the UI labels the actual answer path. No page-review reply carries an action or writes a task record.

The snapshot builder projects only bounded allowlisted saved fields for Dashboard, Calendar, School, Clients, Jobs, Websites, Files, Servers, Security, Software, Stocks, and Settings. It can include available host telemetry but performs no arbitrary file/URL access. It excludes credentials, codes, camera recordings, personal memory notes, file contents, and paths. The result is exact {section,title,summary,evidence:string[],suggestions:string[]} with serialized size below 6,000 characters. Readout summaries are concise selections, not a full DOM, every table row, the scrolled viewport, unsaved form text, or an external site's HTML.

Feedback may call ollama.reviewPage only when local Ollama is enabled with a configured model. That method sends only the current request, configured name, and projected snapshot to fixed http://127.0.0.1:11434/api/chat, with no history, memories, research, web fetch, or cloud provider call. think:false, keep_alive:"2m", a 4,096-token context, a 100-token generation limit, and at most a 30-second request timeout keep this bounded. Ollama is asked for an exact JSON object {suggestionOrder:[indices]} using a per-request schema. The client accepts only distinct in-range zero-based integer indices, at most three, and rejects free prose, extra keys, duplicates, or rewritten facts with OllamaError code invalid_page_review. Empty supplied suggestions return an empty order without inference. The service renders its original saved summary and suggestion strings in the validated order; no generated prose enters the response. Record strings remain untrusted data in the prompt. Invalid input or unavailable/invalid ranking uses grounded local guidance; the response metadata does not claim model success. Selecting ChatGPT for ordinary conversation does not send this page data to ChatGPT.

Page context lasts five minutes, keyed by authenticated session, optional tab ID, and current provider/account/model scope. Narrow requests such as “tell me more,” “give me feedback,” and “what should I do next?” regenerate the saved snapshot rather than forwarding the preceding private answer as model history. A changed or invalid actual-view context requests clarification. Other ordinary conversation retains its existing route. Page outputs stay mode:"command" and are excluded from ordinary provider conversation history. Requests and replies remain in the normal local chat log, paired-dashboard reads, and private app exports. This feature is read-only; Calendar and planner creation keep their separate confirmation workflows. See PAGE-READING.md.

Software project catalogue

GET /api/software/projects returns {projects}. /api/state includes the same records under softwareProjects, and the private JSON app export includes them. Each record has a server-generated UUID, creation/update timestamps and descriptive fields. There is a limit of 150 records. Names must be unique after Unicode normalization and case folding. Existing data files with no softwareProjects array start with an empty catalogue; startup does not scan project folders or recover conversations automatically.

POST /api/software/projects accepts a reviewed record with required name (1–150 characters), category (1–80), description (1–1,500), lastKnownState (1–1,000), evidenceDate (a real YYYY-MM-DD date), evidenceNote (1–2,000), growthIdea (1–1,500), and nextStep (1–1,500). Optional fields are sourcePath (up to 1,000 characters), sourceExists (boolean, default false), url (public HTTP/HTTPS reference or empty), priority (default normal), stage (default prototype) and notes (up to 3,000 characters, default empty). A nonempty source path must be absolute; sourceExists:true requires a recorded path. The flag records the review observation; this route does not inspect or serve that path. Unknown fields, invalid values and duplicate names are rejected. The dashboard uses the recovered catalogue; it does not provide a general add-record form.

PUT /api/software/projects/:id accepts a nonempty object containing only priority, stage and/or notes. Priorities are high, normal, later. Planning stages are idea, prototype, developing, paused, ready. These are personal planning labels, separate from the recorded technical evidence. Identity, descriptions, paths, evidence and growth suggestions cannot be overwritten through this partial-update route. Missing records return 404; invalid UUIDs or fields return 400. There is no project-delete, source-file, file-open, execution or deployment route.

Chat requests such as my software, my projects and software projects return a read-only catalogue summary plus {type:"navigate",section:"software"}. The known Software action uses the shared-navigation signal above. Mac companion 1.1.5 / build 7 follows that contract and uses its fixed local browser fallback when no recent dashboard follower is reported. Ordinary optional Ollama chat receives a bounded context for at most four projects, favoring project-name matches: short name/description/recorded state/evidence date/growth/next-step fields plus priority and stage. Source paths and private project notes are omitted. As usual, text deliberately sent as a chat message may remain in recent chat history.

The catalogue and source paths are private app records visible to paired sessions and included in /api/export. The reusable source ZIP excludes the personal catalogue and active data directory. Adding descriptive records does not run other projects, expose their source files or check their remote hosting. See SOFTWARE.md for the user workflow and evidence limits.

Briefing, website checks, and class commands

POST /api/chat recognizes daily briefing, weekly class schedule queries, saved website status, and explicit fresh-check requests as local commands. Briefings report actual saved Calendar plans, School/website deadlines, class meetings, and saved website observations. Saved check results retain their observation time; a status query does not imply a new network check. An explicit “check my websites” request uses the normal bounded public-site checks and shared monitoring observation path. Calendar/class reads do not load the local model.

Weekly class creation uses the School form and existing class API; voice supports schedule queries but cannot create weekly classes. Explicitly saved class-day memories can appear separately in School without creating timed Calendar meetings. Ordinary model-generated prose cannot create classes. See ASSISTANT-ACTIONS.md for user-facing examples.

Morning voice-unlock conversation

These routes require the normal authenticated session. Writes require the existing same-origin, JSON content-type, and X-Jarvis-Client: dashboard headers. Paired LAN clients can read status. PUT and POST require an actual loopback connection; forwarding headers do not grant this capability.

  • GET /api/briefings/morning returns {enabled,timeZone,morningStart:"05:00",morningEnd:"12:00",lastBriefingDate:null|"YYYY-MM-DD",canConfigure}. The time zone is the saved Calendar time zone, not the viewing browser’s zone. canConfigure is true only for localhost. Reading status does not claim the day or generate speech.
  • PUT /api/briefings/morning accepts exactly {enabled:true} or {enabled:false} and returns the same status. No other keys are accepted. Configuration preserves the last claimed date and never triggers a briefing. Fresh installs start disabled; an existing explicitly enabled preference persists.
  • POST /api/briefings/morning/unlock accepts exactly {}. The native Mac app calls it after its local voice-code verifier accepts an unlock. A code or transcript in the body is rejected with HTTP 400. Wrong-code and wake-only paths never call it. The response is either {delivered:true,reply,localDate} or {delivered:false,reason,localDate?}. Decline reasons are off, outside-window, and already-delivered. A successful reply is also appended as an assistant command message to bounded saved chat history, so paired dashboards can display it.

Eligibility is 05:00 inclusive to 12:00 exclusive on the Calendar local date. The enabled preference and last claimed date are stored in private morning-briefing/settings.json below the active data directory, separate from app-record exports. Requests are serialized and the daily marker is atomically persisted before a delivered response. This gives at most one claim per local date across restarts; response loss, cancelled playback, or a speech failure does not reset the marker or cause a retry. delivered:true means the backend returned the briefing, not proof that the speaker played it. Unsafe or malformed optional feature storage produces HTTP 503 without disabling the rest of Jarvis.

The deterministic reply greets the unambiguous first name from an explicit saved personal-name memory, summarizes today’s saved Calendar plans, due or pending reminders through today, and unfinished School deadlines with overdue counts and the next deadline. School-linked Calendar deadline markers are excluded from the Calendar sentence to avoid reading the same assignment twice. Completed tasks and sent/canceled reminders are excluded. The briefing performs no model generation, web request, Calendar/School/reminder creation, or email send; it does not confirm Gmail delivery. It ends with “Would you like a quick tour of my voice commands, or should we start with your plans?”

A delivered briefing clears that caller’s pending Calendar/School/website, Client, reminder, research, assistance and workspace-continuation state, then installs a capability-tour invitation. The invitation is memory-only, expires after five minutes, and belongs to the same authenticated session and provider/model/account scope. Only the next chat utterance can answer it; a supported affirmative such as “yes” or “sure” returns the local navigation guide, while a different request consumes the invitation and proceeds normally. Another session, a changed provider/model/account, logout or backend restart cannot reuse it. It grants no permission to save a record and is excluded from app data and exports.

Mac companion 1.2.1 / build 13 requests a claim with a five-second timeout and no automatic retry. A valid briefing replaces the ordinary unlock acknowledgement; a decline, invalid reply, or server failure keeps that acknowledgement. Local synthesis/playback uses the existing selected voice, with capture stopped until playback and echo cooldown finish. The usual 90-second reply window then opens, and the existing Continue conversation preference controls later replies. Paused or sleeping Macs cannot hear an unlock; these routes do not wake or start the listener. See MORNING-BRIEFING.md for setup and the physical morning test.

Client prospects and public searches

Client save fields are {name,url?,summary?,location?,businessType?,notes?,stage?}. Only a business name is required; name-only records are valid. Omitted text fields default to empty strings, and blank url is stored as an empty string. Stages are new, researching, contacted, won, and lost; a new prospect defaults to new. Names, location, and business type are up to 150 characters each; summaries are up to 2,000 characters and notes up to 4,000. At most 200 prospects are stored. Nonempty URLs must be valid canonical public HTTP(S) source or website addresses and remain unique across saved records; blank URLs are optional and are not treated as duplicate website addresses. The server supplies IDs and timestamps. State and JSON exports include clients; only deliberately saved prospects become app records.

GET /api/clients/directory returns {available,provider,coverage,dataAsOf?,generatedAt?,placeCount?,businessCount?,attributionText?,attributionUrl?,licenseUrl?,error?,code?}. /api/state includes the same snapshot under clientDirectory. A missing or invalid directory returns available:false; this does not make the rest of the dashboard unavailable. The index lives at business-directory-ohio.json in Jarvis’s data directory and is separate from personal JSON exports.

PUT /api/client-preferences accepts exactly {businessType,location,radiusKm}, with required business type/location text up to 150 characters and integer radius 1–50 km. It saves settings.clientPreferences; generic “find me clients” voice requests use these preferences, while explicit category/location requests override the relevant values. The Clients form initializes from saved preferences once per page load without overwriting a typed search draft.

POST /api/clients/search accepts {businessType,location,radiusKm?}. Business type and location are required text, each at most 150 characters; their combined query must fit 300 characters. Radius is a whole number from 1 to 50 kilometres, default 25. The installed local Ohio directory is used before any general web source. A configured directory error or an empty directory result does not silently fall back to web results. Missing setup/invalid data returns 503, unknown/ambiguous Ohio town names return 422, and invalid category/radius/fields return 400. Search is read-only and needs no network after setup.

A successful directory response contains {query,results,searchedAt,provider,warning,attributionText,attributionUrl,licenseUrl,dataAsOf,resolvedLocation,radiusKm,coverage,local,totalMatches,returned} when supplied by that source. cached is optional. dataAsOf is the map snapshot timestamp; searchedAt is when the local query ran, not evidence of a fresh business check. Up to 20 results are shown. Each result is {title,url,snippet,sourceUrl,websiteUrl?,category?,location?,address?,distanceKm?,websiteStatus?,brand?} with bounded, source-provided metadata. url/sourceUrl identify the stable OpenStreetMap record, not a guessed business domain. websiteStatus is listed or not-listed; only a validated actual website tag creates websiteUrl. An unlisted website or brand is not a claim about ownership, website existence, willingness to buy, or need. Candidate ordering favors no listed website, then no recorded brand, then distance. Source coverage includes selected named business points/ways and Ohio town centers; omissions and approximate way centers are documented in CLIENT-SEARCH.md.

The UI displays the source and license, actual listed address/location/category, approximate distance, separate source/search dates, and optional business-website link. Review before saving. A saved prospect summary retains source evidence and uses the returned business’s category/location rather than the requested search center. Searches do not persist prospects, change their stage, or contact businesses.

POST /api/jobs/search remains separate and accepts keywords up to 300 characters, location up to 200, and boolean remoteOnly. Responses include {query,results,searchedAt,provider?,warning?,attributionUrl?,attributionText?,sourceUrl?}; feed responses can also include feedFetchedAt, cacheExpiresAt, and stale. Job results contain {title,url,snippet} with optional company, location, jobType, tags, and publishedAt. Dates stay as reported, with no assumed UTC for an absent offset. The official Remotive public feed contains remote listings only, retains original source URLs and attribution, is delayed by 24 hours, and is cached for six hours across restarts when the cache can be saved. Location is a ranking preference rather than a verified eligibility filter. remoteOnly:false does not add local/on-site listings. Source failure is reported honestly, and an older cache is identified as stale. Search does not assert that the user qualifies for a role.

POST /api/clients/:id/draft accepts {} and builds a plain local outreach template from saved facts. It returns the draft without sending a message, extracting private contact details, changing progress, or adding a synthetic chat turn. The user can review, edit, and copy it. These search and draft routes use the same session, request-header, and Origin checks as other writes. No account login or paid API is required.

Natural Calendar and Clients commands

The authenticated POST /api/chat command router handles these deterministically before general AI conversation:

add a study session to my calendar tomorrow at 6 PM for 30 minutes
what's on my calendar tomorrow
read my calendar on Friday
show my calendar on Oct 21
what's on my calendar this week
read my calendar next week
what's my next event
add a client called Example Plumbing
add a client called Example Plumbing, website https://example.com, location New Philadelphia
list my clients
tell me about client Example Plumbing

Calendar reads use actual saved events, including linked School/Website deadlines, in the backend's calendar timezone with AM/PM. Date queries support today/tomorrow, weekdays, full/abbreviated month names, US/ISO dates, Monday-through-Sunday weeks, and inclusive date ranges of at most 31 days. A month/day read without a year uses the current year. Upcoming/next searches look within the next year and exclude ended events and completed linked deadlines. Readouts are bounded to five events, or one for the next event. Empty dates and unsupported/competing date expressions are reported locally, without inventing plans or calling a model. This is Jarvis's calendar; no Apple/Google synchronization or external appointment booking is implied.

Client creation accepts a business name alone. Optional labeled details include website/URL, location, business type, notes, stage, email, and phone. An explicitly supplied website/source address is normalized and validated; a name never becomes a guessed domain. Email and phone are stored as labeled text in notes, rather than adding new record fields. Stage defaults to new. Client reads list actual saved records or resolve an exact saved business name; ambiguous duplicate names require clarification. Neither reading nor saving contacts a business.

Calendar creation returns awaitingCalendarInput, School/Website creation returns awaitingPlannerInput, and Client creation/reads return awaitingClientInput. A true flag means further draft input or confirmation is expected; false marks a handled read, completed save, or cancellation. Merely seeing one of these properties does not prove a record was saved: the reply explicitly reports the saved result. Missing fields and ambiguous dates/times are clarified, a complete record is previewed, and only a separate same-session confirmation writes it. A session can own only one write draft across these three domains. Starting a different explicit creation discards the prior draft. Calendar/Client reads, ordinary conversation, and display commands retain unfinished fields while revoking its previous confirmation preview, so the next yes repeats the preview before another confirmation can save. Drafts expire after ten minutes and disappear on logout or restart.

Handled Calendar reads, complete previews and successful saves request Calendar navigation; Client prompts, reads and saves request Clients navigation. Typed record previews, clarification questions and readouts remain visible in the originating chat panel while the other already-open paired dashboards follow the section request. State refreshes display a newly saved record without a manual reload. The normal unsaved-work guards, polling, freshness limits and browser full-screen restrictions still apply. Model-generated text cannot submit records, and existing-record edits/deletes continue to use their forms/API. See ASSISTANT-COMMANDS.md for the user-facing flow.

Assistant conversation starters

POST /api/chat/start lets a paired control panel ask Jarvis to begin a conversation. The JSON body may be {} or contain only topic, one of general, business, or planning; the default is general. It requires the same session, client header, and Origin checks as normal chat. Local Ollama must be enabled with an installed model selected in Settings. The request cannot select a remote endpoint, supply an arbitrary generation prompt, or invoke a cloud fallback.

The server asks the fixed loopback Ollama service to select one short, friendly, open-ended question from five reviewed variants for the chosen topic. It supplies the assistant's name and a bounded excerpt of recent successful conversation: at most six context messages and 3,500 characters. Command/status output is excluded. The allowed questions ask about interests, ideas, or priorities; they make no claims about observed screens, notifications, tasks, books, events, clients, or physical meetings. The request uses Ollama's JSON Schema format support, with an exact allowed-opener enum. The server independently checks schema shape and exact topic-specific membership before returning or saving anything. Unknown prose, modified questions, extra fields, or an invalid selection produce 503; there is no automatic canned fallback. Selection uses a 2,048-token context, a 100-token prediction limit, and a maximum 30-second local request deadline. Subsequent user replies use normal local-model conversation.

A successful request returns, for example, { "reply": "What's on your mind today?", "mode": "proactive", "topic": "general" }; the local model selects the approved question based on recent context. The backend saves exactly one assistant message with mode: "ollama", proactive: true, and its topic. It adds no synthetic user turn, activity notification, or invented task. Subsequent normal chat includes the opener as real assistant context. Chat remains bounded to 200 stored messages, and these records are included in normal backups.

Conversation starters have a per-session minimum interval of 60 seconds and reject another in-flight attempt with 429 and a Retry-After header. A failed model attempt also consumes that interval; disabled-model and invalid-topic requests do not. Model unavailability, missing model, incomplete/invalid output, and local generation failures return a clear 503 without adding any message to history. Logout before generation finishes also prevents saving the opener. Limits reset on server restart, and session cleanup removes expired rate-limit entries. This API generates only when called: optional idle timing, quiet hours, daily limits, and voice playback belong to the requesting app and do not create a Codex automation or a server scheduler.

The status endpoint's storage view lists regular files directly inside the configured data directory, excluding temporary write files, symlinks, the private pairing-code file and .sessions.json. It does not enumerate the computer's other folders. CPU is sampled, memory is OS allocated memory including cache, and disk capacity describes the volume containing the data directory.

Local voice

GET /api/voice probes the server's local tools and model files. Availability is cached for up to 30 seconds. A typical response is:

{
  "stt": { "available": true, "engine": "whisper.cpp", "model": "base.en" },
  "tts": {
    "available": true,
    "engine": "macos-say",
    "voice": "Daniel",
    "voices": [{ "id": "Daniel", "name": "Daniel", "language": "en-GB" }]
  },
  "maxRecordingSeconds": 30
}

The example voice array is abbreviated; actual choices come from the installed engine. Unavailable stt or tts entries include an error string. A configured non-macOS speech host can report engine piper and voice ID piper-local. Capability reporting does not install tools or download models.

GET /api/voice/native requires the same authenticated session as other voice routes. It reads only the fixed local companion heartbeat in the user's Application Support folder; request parameters cannot select a file. A valid heartbeat must be at most 15 seconds old, name a live process, and contain bounded state/boolean/timestamp/wake-name/counter fields. It returns live, active, and sanitized listener state. It omits raw error detail, process ID, paths, speech text, and audio. A missing, stale, or invalid heartbeat reports an inactive companion. The server-only JARVIS_NATIVE_STATUS_FILE override exists for isolated validation.

The browser checks this route at voice setup and during normal status refreshes. A live authorized Mac companion owns wake listening while connecting, listening, transcribing, thinking, or speaking; the browser releases its own hands-free stream and shows the Mac-app status. Browser re-enable also performs a fresh ownership check. Pausing or quitting the Mac app lets the browser's hands-free control become available again. Manual Talk remains available with a reminder to pause the Mac listener first.

Android native microphone ownership uses separate authenticated routes and leaves /api/voice/native unchanged. Before opening its microphone, the Android companion sends POST /api/voice/android/acquire with a printable device label of 1–60 characters, for example { "label": "Galaxy S21 Ultra" }. A successful claim returns { "active": true, "label": "Galaxy S21 Ultra" }. While listening or processing a voice turn, the companion sends POST /api/voice/android/heartbeat with {} every five seconds. Each accepted heartbeat renews its server-timed lease for 15 seconds. On pause or stop it closes the microphone and sends DELETE /api/voice/android. A failed heartbeat requires the companion to stop capture; an expired lease cannot be revived by heartbeat and must be explicitly acquired again.

Only one authenticated session can own the Android lease. A different session cannot acquire, heartbeat, or release it and receives 409; session cookies are the ownership credential, and no separate lease secret is returned. GET /api/voice/android exposes only active and label to paired clients, returning { "active": false, "label": null } when inactive. Browser wake listening must stop while either the native Mac status or Android status reports active. Android status carries no audio, transcripts, device IDs, paths, or session data and is never written to app records or backups.

The native Mac companion keeps priority: an active Mac heartbeat rejects Android acquire/heartbeat with 409 and revokes any Android lease when Android ownership is next checked. A failed Mac-status read rejects acquisition with 503. Android ownership also ends on owner logout, session expiry, server restart, explicit release, or a heartbeat gap of 15 seconds. This coordinates cooperative clients; it cannot turn off a device microphone remotely. The Android companion must close its own microphone promptly on a rejected or failed heartbeat and must not resume automatically after losing ownership.

POST /api/voice/transcribe requires Content-Type: audio/wav, the usual client header, and a valid session. The body must be mono, 16 kHz, PCM16 RIFF/WAVE with internally consistent headers. Uploads are capped at 1,000,000 bytes, and recordings may last 200 milliseconds through 30 seconds. Silent/very quiet input is rejected. The browser produces this format directly with AudioWorklet capture and resampling; no ffmpeg service is used. The current adapter transcribes English with local whisper.cpp. durationMs is recording duration. A successful response does not save a chat message; the browser separately submits the recognized text to /api/chat.

Hands-free mode uses the same transcription route for bounded speech segments while explicitly enabled. Outside a conversation window, the browser checks the configured wake phrase and discards unrelated transcripts. A wake-only utterance opens a 10-second request window. With the browser's optional Continue conversation setting enabled, a successful voice request opens a 30-second follow-up window only after reply playback finishes and a brief cooldown; requests in that window need no wake name. Typed drafts and review-before-send rules still apply. Quiet expiry or an accepted end-conversation phrase returns to wake listening, and an accepted “stop listening” command closes the browser microphone. This route does not save ambient transcripts or enable a server microphone. Browser voice state belongs to the open visit, while native companions own their separate listening lifecycle.

POST /api/voice/speak accepts only text (a nonempty string, at most 2,500 characters) and optional voice (one of the current installed voice IDs). It returns Content-Type: audio/wav and an X-Jarvis-Voice-Engine header. On macOS the engine generates PCM16 WAV at 22.05 kHz using /usr/bin/say. The response is played by the requesting browser. This endpoint does not itself send sound to the server's speaker or modify chat history.

Each local engine allows one request of its kind at a time. Temporary recording/transcript/output directories are removed after processing. Closing/canceling the browser request aborts its local child process. Native processes run without a shell, with output limits and a 60-second deadline. Server-owned executable/model paths come from configuration; the API does not accept them from the browser.

The authenticated CA-download route exposes only runtime/tls/jarvis-local-ca.crt; it does not accept a filesystem path or return private keys. It is available over HTTP to bootstrap a trusted HTTPS phone connection. Device trust remains a manual action; see VOICE.md.

Errors and extension guidance

Errors use a JSON object with an error string and an appropriate HTTP status. Expected cases include 400 invalid data, 401 missing/invalid session or pairing code, 403 access/origin restriction, 404 missing route/record, 409 duplicate target, 413 oversized body, 415 wrong content type, and 429 pairing/session limit. Unexpected internal errors return a generic 500 message.

Voice requests also use 422 for no clear speech, 429 when that engine is busy, 502 for an engine/output failure, 503 when a local engine is unavailable, and 504 for an engine timeout. Canceled/disconnected requests stop processing rather than saving partial audio or creating a chat entry.

The optional Ollama probe returns an explicit unavailable state if the local service cannot be reached. An optional chat failure returns a readable command-mode reply, preserving built-in command access.

Keep new integrations behind authenticated routes and the same browser-write checks. Use INTEGRATIONS.md for the proposed adapter contracts. Gmail reminder delivery has its own explicit private setup described below; it does not provide arbitrary email or inbox access. A personal file browser, an import UI, and remote server agents still need their own implementations; none are implicit in these routes.

Voice device selection and explicit memory

PUT /api/settings accepts voiceDevice: "mac" | "any". Existing data defaults to any. Selecting mac disables browser capture and speech on every dashboard and revokes/rejects Android listener ownership; the native Mac companion continues to use the local speech APIs. It does not assert the Mac is currently listening: /api/voice/native remains the source of fresh runtime status. Returning to any requires an explicit new browser/Android start.

Saved notes are available through authenticated GET/POST /api/memories and PUT/DELETE /api/memories/:id. Writes accept only text; see the memory guide for limits and examples. /api/state and exported backups include memories. Explicit “remember that …” chat commands save through the same service without asking the model to invent a memory. Regular local replies receive a bounded relevant selection of the notes.

Natural speech selection

PUT /api/settings also accepts speechVoice (a voice ID up to 120 characters; empty means server default) and numeric speechSpeed (0.75–1.25). These are global persisted defaults. POST /api/voice/speak accepts { text, voice?, speed? }; explicit per-request values override saved defaults. The speech service validates the chosen voice against installed voices and rejects unavailable choices. The native Mac app sends text only, so it uses the saved defaults automatically. GET /api/voice adds selectedVoice and speechSpeed alongside the engine's own default configuration.

An installed Kokoro engine reports tts.engine: "kokoro-onnx" and voice IDs such as kokoro:am_michael or kokoro:af_heart. It generates mono PCM16 24 kHz WAV through a fixed local Python process with text on stdin, fixed model paths, bounded work, cancellation, and temporary-file cleanup. The response's X-Jarvis-Voice-Engine identifies the actual engine used. Existing macOS and Piper alternatives remain available; synthesis failures do not silently switch voices. See natural voice setup.

Local calendar

Authenticated GET/POST /api/calendar/events and PUT/DELETE /api/calendar/events/:id provide the local single-day calendar. State adds calendarEvents and calendarTimeZone; full exports include saved records. See CALENDAR.md for exact fields, validation, timezone behavior, commands, and limits. Agenda queries use actual saved events without calling the model. POST /api/chat also handles local English creation drafts with awaitingCalendarInput; only a separate same-session confirmation saves the previewed event. Drafts expire after ten minutes, logout, or restart. Generated model text never writes calendar data. Existing-event edits/deletes use the form/API.

School

Authenticated GET/POST /api/school/tasks and PUT/DELETE /api/school/tasks/:id manage local assignment/study tasks. State adds schoolTasks; full exports include those records. Tasks accept optional dueTime and have a managed calendarEventId; dated saves/edits update task and Calendar marker atomically. No external school account or submission access is connected.

Weekly classes use GET/POST /api/school/classes and PUT/DELETE /api/school/classes/:id. Create fields are {title,weekdays,startDate,endDate,startTime,endTime,location?,notes?,skipDates?}. Weekdays are distinct integers Sunday 0 through Saturday 6; dates are YYYY-MM-DD, and times are HH:mm. The service creates linked timed meetings atomically and returns a class record with UUID id, managed calendarEventIds, and ISO timestamps. Updates accept a nonempty partial field object. Class deletion removes its meetings, leaving separate assignments unchanged. State adds schoolSchedule; exports include classes and meetings, and restore validates their links. Direct Calendar PUT/DELETE for a class meeting returns HTTP 409. Limits are 50 classes, first/last dates no more than 366 days apart, 100 distinct skipped dates inside the range, and the shared 1,000-event Calendar limit. See SCHOOL.md for exact fields, text limits, and validation.

Website work and planner conversation

PUT /api/websites/:id updates a saved site's name/address, isPrimary, or monitoring settings. Changing an address clears its old check/incident state; historical alerts retain their original address and cannot be spoken as a current incident for the changed URL. GET/POST /api/websites/tasks and PUT/DELETE /api/websites/tasks/:id manage tasks tied to actual saved sites. State adds websiteTasks. Site deletion cascades its local tasks, Calendar markers, and alert history. Full exports include all records; restore validation rejects missing sites, broken links, and duplicate marker ownership. See WEBSITE-WORK.md.

POST /api/chat recognizes spoken/typed assignment, website registration, and website work creation. It gathers missing fields, previews the result, and requires a separate same-session confirmation. Responses include awaitingPlannerInput. Ordinary model text never writes records. Starting a new creation flow discards the other pending flow; unrelated discussion revokes confirmation. Client chat requests are serialized per authenticated session to keep follow-ups in order. Drafts expire after ten minutes and disappear on logout/restart. No paid API, network fetch, or live website mutation is required for planner saves.

Conversation, research summaries, and optional conversation starters now send Ollama keep_alive: "2m". This releases idle model memory sooner while preserving the Mac’s 90-second spoken follow-up window. The selected lighter model remains a persisted setting; no account connection or paid API is added.

Dashboard weather

Weather routes require the normal authenticated dashboard session. The location write also requires the existing same-origin/dashboard write headers. They use fixed Open-Meteo forecast/geocoding endpoints through the local backend, with no paid API, API key, browser GPS or model call. The default location is the New Philadelphia, Ohio town centre at approximately 40.48979, -81.44567, with America/New_York weather time. It is not a detected home/device location.

GET /api/weather returns a snapshot directly, not a {weather} wrapper:

{
  status: "ready" | "stale" | "unavailable",
  location: { name, latitude, longitude, timeZone },
  day: "YYYY-MM-DD",
  timeZone,
  temperatureUnit: "°F",
  windUnit: "mph",
  current: {
    temperature, feelsLike, weatherCode, condition,
    isDay, windSpeed, observedAt
  },
  today: { date, high, low, precipitationProbability },
  hourly: [{ time, temperature, weatherCode, condition, precipitationProbability }],
  fetchedAt,
  observedAt,
  source: { name: "Open-Meteo", url: "https://open-meteo.com/" },
  message: null | string
}

Temperatures are Fahrenheit, wind speed is mph, and probabilities are percentages. today.precipitationProbability is the forecast maximum probability for the day; it is not measured rainfall or a rain-only probability. Hourly entries are up to six forecast points distributed across the remaining same-day hours; the compact home card displays up to four of them. time, observedAt and fetchedAt are ISO timestamps with Z; the browser formats them as 12-hour times in the saved weather timezone. isDay is boolean or null. Unknown numeric/condition fields remain null, and no report uses fabricated zero readings.

Successful reports are cached in server memory for 15 minutes, keyed by the complete location and local date. Concurrent requests share one upstream request. Failed refreshes have a 60-second retry backoff; a same-location, same-day saved report is returned as stale only while both its fetch and observation are at most two hours old. Otherwise the route returns an unavailable snapshot with null readings and an empty hourly array. Provider failures therefore normally produce HTTP 200 with an explicit snapshot status, rather than making the rest of the app fail. The provider request has an eight-second timeout and a bounded JSON response. There is no server background weather timer.

GET /api/weather/locations?q=QUERY accepts a trimmed search of 2–100 printable characters. It returns {locations} containing up to five complete normalized location objects. An empty match returns an empty list; invalid queries return HTTP 400 and provider search failures return HTTP 503. The query is passed to Open-Meteo's fixed geocoding endpoint, never interpreted as an arbitrary URL.

PUT /api/weather/location requires exactly the four complete editable fields, with no partial update or unknown keys: name is 1–120 printable characters, latitude is a finite JSON number from -90 through 90, longitude is a finite JSON number from -180 through 180, and timeZone is a valid runtime-supported timezone of at most 80 characters. The route returns {location} after saving settings.weatherLocation. It does not return or immediately fetch a forecast.

The browser requests weather separately from app startup and rechecks every five minutes only while authenticated Home is visible. Refresh requests the same cache-aware GET; it does not bypass the backend's freshness/backoff rules. Weather updates replace only the weather slot so chat, voice state, forms and the clock keep running. The selected location appears in settings, /api/state and private JSON exports. Forecast cache entries stay in memory and are not added to saved records or exports. This external forecast/geocoding feature needs internet; see WEATHER.md and the Open-Meteo forecast documentation.

Stocks and investment records

These routes use the normal authenticated dashboard session and same-origin/write-header rules. They operate only on saved local investment records. No route fetches market quotes, connects a brokerage, or submits an order.

Method Route Result
GET /api/stocks/investments {investments} for all saved Personal/Business accounts and time horizons
POST /api/stocks/investments Create one entry; HTTP 201 {investment}
PUT /api/stocks/investments/:id Apply a nonempty partial field object; HTTP 200 {investment}
DELETE /api/stocks/investments/:id Remove the identified entry; HTTP 200 {ok:true}

/api/state and the private JSON app export include stockInvestments. The maximum is 500 entries across both accounts and horizons. GET results sort by updatedAt descending, then symbol. Creates and updates are saved through the same local store as other app records. Existing data without this array starts empty; startup does not add sample investments.

Each returned investment has exactly {id,account,horizon,symbol,name,kind,quantity,averageCost,referencePrice,priceAsOf,notes,createdAt,updatedAt}. IDs are generated UUIDs; the server supplies ISO creation/update timestamps. Request bodies may contain only the ten editable fields below. Unknown fields and invalid data return HTTP 400, missing valid IDs return 404, and an add at the 500-entry limit returns 409.

Editable field Validation / default on create
account personal or business; default personal
horizon long-term, short-term, or day-to-day; default long-term
symbol Required 1–20 ASCII ticker letters, digits, dots, or hyphens, beginning with a letter/digit; trimmed and uppercased
name Optional printable text, at most 120 characters; default ""
kind holding or watchlist; default watchlist
quantity For holdings, a finite JSON number greater than 0 and at most 10,000,000; for watchlists, null
averageCost USD average cost per share; for holdings, a finite JSON number from 0 through 1,000,000; for watchlists, null
referencePrice Optional manually entered USD price per share; null or a finite JSON number from 0 through 1,000,000; default null
priceAsOf A real YYYY-MM-DD date from 1900 through 2100 whenever a reference price is present; otherwise ""
notes Optional printable text, at most 4,000 characters; default ""

Text is trimmed and CRLF/CR normalized to LF. Numeric strings, NaN, infinity, and booleans are rejected as API numbers; zero is valid for costs/prices. Holding quantity and cost are required. Changing a holding to watchlist must also set quantity and averageCost to null; clearing referencePrice requires priceAsOf:"". The dashboard form performs these normalizations when submitting its full field object. PUT merges supplied fields with the existing record and then validates the complete result.

UI calculations use quantity × averageCost for recorded cost and quantity × referencePrice for manual reference value. Watchlists are excluded. Account totals remain unavailable unless every holding has the required data for that total; coverage counts accompany the values. The unrealized difference requires both complete totals. These calculations retain each manually entered price's date and do not imply a current market observation. See STOCKS.md for the page controls and backup scope.

Automatic website monitoring and alerts

Site isPrimary is optional boolean metadata. Setting it true clears that designation on other saved sites. Site monitoring is {enabled:boolean,intervalMinutes:number}, with a 5-minute default and whole-number range 2–60. Older sites with no monitoring settings remain manual. The backend manages monitorState:{consecutiveFailures,incidentOpen,blockedAlerted,lastCheckedAt,nextCheckAt,lastError}; site writes cannot supply it. Disabling monitoring clears that state. Changing the interval queues a fresh schedule.

The existing backend starts a small monitoring timer after a brief startup delay. A tick checks at most two due sites. Manual and automatic requests for the same site share an in-flight sample; changing/removing a target prevents an old-address result from updating the new record. After sleep/restart it resumes due work without manufacturing historical samples. Two consecutive failures open one incident; a later successful check emits recovery. A sufficiently long gap resets a pending single-failure count. Private/unsafe checks and HTTP 401/403/429 are blocked/uncertain observations, not confirmed outages. These checks depend on the server's own connectivity, require an awake running server, and do not measure global uptime. No model inference or paid monitoring API is involved.

State/export includes websiteAlerts, newest first, bounded to 100 records. Each record is {id,websiteId,name,url,type,message,createdAt,read,spoken} with type one of down, recovered, or check-blocked. PATCH /api/website-alerts/:id accepts exactly {read:true} and returns {alert}. Alerts and monitoring links are validated on restore; missing older fields default safely without a startup rewrite.

PUT /api/settings accepts boolean websiteSpokenAlerts, default false. Native companion 1.1.3 uses authenticated POST /api/website-alerts/:id/claim from loopback. The claim succeeds only for the newest unread, unclaimed outage/recovery alert for that still-monitored address, at most 30 minutes old, consistent with the current incident. It returns {alert:null} when no longer eligible and HTTP 403 from a non-loopback client. A successful claim sets spoken:true before synthesis to prevent duplicate attempts. This records an attempt, not confirmed playback; failure leaves the visual alert unread. The native app waits for an idle, unlocked listening state, closes capture while speaking, and does not turn an alert into an open conversation window. See MAC-BACKGROUND.md.

Public research and assistance chat

POST /api/chat accepts up to 4,000 characters. Explicit public search/read requests return up to five validated source links in sources. Missing search terms return awaitingResearchInput:true; the next non-command turn provides the query. Writing/problem workflows return awaitingAssistanceInput and display-only assistance:{kind:"writing"|"problem",state:"active"|"clarifying"}. They use the selected provider, bounded task history and account/model scope; model output cannot supply actions. Drafting never sends a message. See WEB-AND-ASSISTANCE.md for examples, lifetime and data boundaries.

Email reminders

GET /api/reminders returns {reminders,timeZone,email,canConfigureEmail}. Email status is sanitized and never includes a password. GET /api/state also includes reminders, reminderEmail, and canConfigureEmail.

POST /api/reminders accepts {title,date,time,timeZone} using the server time zone, or {title,dueAt,timeZone} with an explicit ISO timestamp. The recipient is fixed; caller-provided recipients or SMTP hosts are rejected. POST /api/reminders/:id/cancel cancels an unsent record. POST /api/reminders/:id/retry retries a failed record; an uncertain result additionally requires {acknowledgePossibleDuplicate:true} after checking Gmail.

Loopback-only Gmail routes: PUT /api/reminders/email with {sender,appPassword} verifies before saving private credentials; DELETE /api/reminders/email disconnects; POST /api/reminders/email/test with {} sends one setup email to the fixed recipient and is limited to once per minute. These retain normal session, same-origin and client-header checks. Public/paired API responses and JSON exports omit email credentials.

Reminder chat returns awaitingReminderInput and fixed Calendar navigation. One explicit save confirmation is required after a full preview; model-generated text never sends email or saves reminders. See REMINDERS.md.

Room guard, recordings and Mac power

All routes in this section require the normal authenticated dashboard session. Every write additionally uses the existing same-origin, X-Jarvis-Client: dashboard, and JSON content-type rules. Loopback restrictions use the connection's peer address, not a caller-supplied forwarding header. Paired LAN clients can read public status and saved media, but cannot arm/disarm room guard, retrieve native configuration, delete clips, or change power preferences.

GET /api/room-guard and successful PUT /api/room-guard return a snapshot directly:

{
  armed: boolean,
  configurationRevision: positive safe integer,
  updatedAt: ISO timestamp,
  clipSeconds: 30,
  cooldownSeconds: 300,
  canArm: boolean,
  recordingsCount: integer,
  totalBytes: integer,
  storageFull: boolean,
  limits: { maxRecordings: 20, maxBytes: 1073741824 },
  native: {
    live: boolean,
    active: boolean,
    roomGuard?: {
      armed: boolean,
      state: "off" | "armed" | "challenging" | "recording" | "cooldown" | "unavailable",
      cameraAuthorized: boolean,
      voiceCodeConfigured: boolean,
      reasonCode: string,
      reason: safe fixed message,
      recordingId?: UUID
    }
  },
  recordings: [{ id, filename, bytes, createdAt, durationSeconds, contentType, downloadUrl }],
  canConfigure: boolean,
  keepAwake: { enabled, supported, active, onACPower, message }
}

The persisted armed preference starts false and is separate from live native confirmation. canArm requires a fresh live native listener exposing the new capability and a configured voice code; the Settings UI also requires canConfigure. It does not promise a currently active microphone or granted camera permission. Arming with PUT /api/room-guard accepts exactly {armed:true} or {armed:false}. A missing live updated listener or missing voice-code setup returns HTTP 409 when arming. Disarming is allowed while the listener is offline. Camera permission can be pending when arming so the native app can open the normal macOS prompt. Only changed preferences increment configurationRevision.

The optional native roomGuard field is sanitized from the fresh heartbeat; malformed optional capability data is omitted without disabling the rest of native status. It is also included by authenticated GET /api/voice/native when valid. Unknown raw reasons, transcript text, voice codes, and recording paths are never copied through. Supported reason codes are NONE, CAMERA_PERMISSION, AWAITING_CAMERA_PERMISSION, CAMERA_UNAVAILABLE, STORAGE_FULL, STORAGE_UNAVAILABLE, CONFIGURATION_UNAVAILABLE, LISTENER_UNAVAILABLE, and CAPTURE_FAILED. recordingId is present only during a reported active recording and must be a valid UUID.

Authenticated loopback GET /api/room-guard/native returns exactly {armed,configurationRevision,clipSeconds,cooldownSeconds,recordingDirectory}. The fixed absolute directory is supplied by the backend; the request cannot choose it. The native app refreshes this roughly every two seconds. Configuration older than fifteen seconds prevents new triggers and stops active capture. Unlike this narrow local native route, public room-guard status contains no filesystem path.

The native trigger requires armed room guard, active Mac listening, locked voice access, a configured private voice code, granted camera access, fresh backend configuration, and available storage. Ordinary locally transcribed speech while locked can prompt for the code. Its ten-second countdown begins after that spoken prompt; a correct code cancels the pending recording. Timeout attempts an up to thirty-second video-only H.264 .mov clip, then a five-minute cooldown. Speech is a presence signal, not speaker identification. Unlocking, disarming, listener suspension, or stale configuration stops capture; a shorter finalized clip can remain saved. It does not bypass the private code gate or allow camera recording during actual Mac sleep.

Recordings live in room-guard/room-recordings under the active private data directory, separate from jarvis.json. A finalized UUID .mov or .mp4 must have matching private completion metadata {id,createdAt,durationSeconds,bytes,completed:true} before it can be listed or opened. createdAt is the native completion metadata timestamp. Lists sort newest first. filename is the UUID basename, contentType is video/quicktime or video/mp4, and downloadUrl is the same-origin /api/room-guard/recordings/:id route. IDs accept canonical lowercase UUID text. The backend checks owner-only ordinary files, ownership, sizes, and descriptor identity without following links. Malformed, partial, ambiguous, symbolic-linked, and hard-linked records are not published.

Library capacity is twenty clips or 1 GiB; the native writer separately caps each capture at 100 MiB and requires 64 MiB of available volume space. Saved clips are never purged automatically. Full storage refuses another capture. Clip settings/files are excluded from /api/state, /api/export, and reusable source ZIPs; protected clip access is through the dedicated routes. Malformed or unsafe room-guard configuration is preserved and produces safe HTTP 503 responses for this optional feature while the rest of Jarvis remains online.

GET and HEAD /api/room-guard/recordings/:id return Content-Type, Content-Length, Accept-Ranges: bytes, Cache-Control: no-store, and an inline UUID Content-Disposition. A full response uses HTTP 200. A single valid Range: bytes=START-END, bytes=START-, or bytes=-SUFFIX uses HTTP 206 with Content-Range. Endpoints extending beyond the clip are clamped; invalid, unsafe-integer, unsatisfiable, or multiple ranges return HTTP 416 with Content-Range: bytes */SIZE and no body. HEAD sends matching headers without media bytes. File descriptors close after HEAD, rejected ranges, successful streams, and disconnected clients. Invalid recording IDs return 400; absent records return 404; unsafe private storage returns 503.

Loopback DELETE /api/room-guard/recordings/:id removes only the named validated video and its metadata, returning {removed:true,id}. The UI presents a separate destructive-action confirmation before this request. Deleting a clip currently owned by the native recorder returns 409. There is no deletion through generated model output, a voice draft, or a public static file path.

GET /api/power returns {enabled,supported,active,onACPower,message} directly. supported is true only for macOS, onACPower is boolean or null when unverified, and active requires the enabled helper to be running with confirmed AC power. Loopback PUT /api/power accepts exactly {enabled:boolean}. The preference persists in private power/settings.json under the active data directory. On macOS the helper runs the fixed /usr/bin/caffeinate -s -w BACKEND_PID command, with no caller-controlled command or arguments. The display may sleep; the assertion applies to system sleep on AC power and ends with the backend or when disabled. The helper's active state does not override manual Sleep, lid closure, shutdown, or macOS session suspension. Unsupported hosts return HTTP 501 on writes; unavailable power settings/helper return 503. A physically sleeping Mac cannot listen, record, or deliver reminders on time.

See ROOM-GUARD.md for setup, review/delete controls, storage scope, power behavior, and the separate physical recording check.