Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .docs/implementation-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Implementation plan — WP1–WP7, refined

`prompt.md` is the spec. This file is the delta: the assumptions it asked us to confirm, the places where the spec
as written would not work against the code as it is, and the order. Every PR description links here.

Before (main @ 379bf25, `python scripts/prove.py`): CO₂ −69.3 %, bill −3.3 %, peak −1.0 %, 530 solves, max 70 ms,
0 fallbacks, 53 tests green.

## Assumptions the spec asked us to confirm (answered here, repeated in each PR)

| # | Spec asked | Decision | Why |
|---|---|---|---|
| A1 | `employee_rate_usd_per_kwh` default `0.0`? | Code default **0.0** (free workplace charging). `data/site.json` ships **0.25** — the "flat employee rate" case in `business.md` §0b — so the demo shows the §7b math instead of three $0.00 chips. Set it to 0 for a free site. | Both cases are the customer; the demo has to show a number. |
| A2 | `contracted_peak_kw` default = `feed_kw`? | Yes. | Nothing better is known without the bill. |
| A3 | `package` default `pilot`? | Yes. | §9b. |
| A4 | `safe_share_kw` computed default | `min(p_max_kw, (feed_kw − max(building_load_kw)) / n_connectors)` = 1.83 kW on site-1. | `solutions.md` §5. |
| A5 | queue: a done car occupies the bay forever in the sim | The sim assumes a notified driver **moves within 10 sim-minutes when someone is waiting** (`Sim.MOVE_AFTER_MIN`). Real-world signal: OCPP StatusNotification. | Without it the queue can never drain and the WP3 test is untestable. |
| A6 | `/sessions/{id}/boost` = alias of `urgency now` | Yes: sets `boost`, `urgent`, deadline = now. WattWise's "Charge now" therefore forfeits the discount, per §4b. | One path, one price rule. |

## Deviations from the spec text (each one is in the code as a comment too)

1. **`price()` denominator.** Spec: pass `kwh_delivered`. At plug-in that is 0, so `α·S/ε` would clamp every new
session to $0. The receipt's saving is computed over the *planned* series (metered + remaining plan), so the
matching denominator is `kwh_needed` while charging and `kwh_delivered` once ended (an early leaver shifted less).
2. **`/price` preview (no session yet).** The spec is silent. Preview = `R − α × (today's site saving rate so far)`
for flex/green, `R` for boost. Labelled estimate; settled on the receipt. No LP call for a preview.
3. **`urgency now` does not move the sim's physical departure.** "Leaving now" means *charge at full power until I
unplug*; if the sim also unplugged the car a minute later there would be nothing to show. `soon` moves both the
deadline and the physical departure (the driver said when they leave).
4. **Never-worse-than-dumb window.** Asserted for the first hour after plug-in only: `min(need, 3.5 kWh)` by 60 min.
Beyond that the guarantee is the progress floor (≥ 50 % pro-rata at every 30-min checkpoint), which the day test
already checks; a dumb-charger bound over *every* rolling hour would forbid the very shifting the product sells.
5. **Fail-safe in the sim mirrors OCPP.** `Connector` gets `safe_kw`; `plug_in` applies it (the static profile); a
dynamic limit expires 15 sim-minutes after the last `set_limit` and the connector reverts to `safe_kw`. In normal
operation `apply_limits()` refreshes every minute, so nothing changes; when the loop dies, the sim degrades the way
a real charger does. This is what makes the kill-the-loop test mean something.
6. **Observation guard needs a physical car in the sim.** Sessions carry `car_kw` (what the car can actually draw;
sim only, defaults to the declared `max_kw`) separately from `max_kw` (what the planner believes). The guard moves
`max_kw` toward the meter; a test sets `car_kw = 3.3` on a bay the planner thinks is 7 kW.
7. **Assistant errors.** Anything that is not `RateLimitError` falls back (`degraded: true`) — including a 400 from an
SDK/parameter mismatch. A demo must never 500 because of the model call.
8. **`connectors_asap` = `["c37", "c38"]`.** The smoke test and the replayed day use `c01–c36` and the top free bays
(`c58–c60`); `c37/c38` collide with nothing and the smoke test now plugs a van into `c37` to prove the flag.
9. **Multi-day replay.** Only one real day is on disk. `scripts/replay_days.py` takes `data/days/<date>/{signal,sessions}.json`
and says which days are missing instead of inventing them.

## Order and size

| PR | WP | Touches | Tests that fail on old code |
|---|---|---|---|
| 1 | WP1 pricing + emergency + first-hour floor | `scheduler.py`, `api.py`, `models.py`, `data/site.json`, `smoke.py` | price ≤ R / urgent = R / §6b number; `now` full power; `priority` ≥ 90 % pro-rata; priority car 0 shortfall when oversubscribed; 3.5 kWh at 60 min; dumb-charger window |
| 2 | WP2 safe share + OCPP two profiles | `sim.py`, `api.py`, `scheduler.py`, `ocpp_gateway.py` | kill-the-loop; `full` mode = safe share; gateway self-check |
| 3 | WP3 vehicle / soc / guard / asap bays / done / queue | `models.py`, `api.py`, `sim.py` | 3.3 kW car meets deadline; guard trips; queue of 3 tightens the slackest and a bay frees |
| 4 | WP4 evidence scripts | `scripts/prove.py`, `fetch_data.py`, new `fit.py`, `replay_days.py`; `impact(health_usd=)` | script self-checks |
| 5 | WP5 ops dashboard | `web/`, plus `impact.csv`, `renewable_share`, status fields | lint + build |
| 6 | WP6 operator assistant | `assist.py`, `assist_knowledge.md`, `/assist`, chat drawer | `tests/test_assist.py`, smoke `/assist` |
| 7 | WP7 docs | README, ARCHITECTURE §7/§9, CHANGELOG, AGENTS, ROADMAP §0 | contract-file diff |

## Out of scope (spec "what not to do", kept)

No new front-end dependencies, no ChargePoint, no bookings ahead of arrival, no V2G, no separate chatbot service,
`SIM_SPEED` untouched, `wattwise/` touched only to keep it building.
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# AGENTS.md — Project Memory (auto-maintained)
Last updated: 2026-09-13 | Sessions logged: 5
Last updated: 2026-09-13 | Sessions logged: 6

## Identity
Hackathon entry: "Noonshift" — a CPO-side, deadline-based EV charging scheduler that shifts flexible charging into low-marginal-carbon hours at daytime long-dwell sites (workplace/destination/depot). Four-person team, lanes in `team-plan.md`.

## Stack & Commands
- Python 3.12/3.13 · FastAPI · scipy 1.14 (HiGHS LP) · mobilityhouse/ocpp · asyncpg/Postgres (optional) · Docker Compose. Two Vite front-ends: `web/` (ops dashboard, React+JSX+Tailwind 3, :5173) and `wattwise/` (driver app, React+TS+Tailwind 4, :5174); `npm install && npm run dev` in each, both proxy `/api` and `/ws` to :8000. `npm run build` in each, then `docker compose up` serves them on :3000 / :3001.
- `python -m venv .venv && .venv/Scripts/pip install -r requirements-dev.txt`
- `python -m pytest` (52 tests, ~45 s) · `python scripts/prove.py` (slide 1) · `python -m noonshift.test_loop` · `docker compose up -d --build`
- `python -m pytest` (63 tests, ~60 s) · `python scripts/prove.py` (slide 1) · `python -m noonshift.test_loop` · `docker compose up -d --build`
- Data: `python -m noonshift.seed gen` (placeholders) · `python scripts/fetch_data.py signal|caiso|sessions --day ...` (real).

## Current State & Focus
Expand All @@ -24,8 +24,8 @@ Signals (WattTime MOER; CAISO fuel-mix fallback) + tariff table + sessions (dead
→ WebSocket frames (plan/meter/event) → driver PWA + ops dashboard. Ladder: live → cached → tariff-only → deadline-only → full power. Hardware limits stay on the charger.

## File Map
- `noonshift/scheduler.py` — `solve(cars, site, signal, tariff, now)`, `solve_lp()` (+info), `impact()`, `impact_detail()`, `price()`; module constants W_CARBON, ALPHA, FLOOR_EVERY, SPRINT_SLOTS, BUFFER_COST, M_SHORT, M_FLOOR, ASAP_EPS, E_MIN, MIN_KW.
- `noonshift/api.py` — FastAPI app, state dict `S`, `resolve()`, `step()`, `session_impact()`, `meter_history()`, ladder `pick_mode()`, `/demo/*`.
- `noonshift/scheduler.py` — `solve(cars, site, signal, tariff, now)`, `solve_lp()` (+info), `impact()`, `impact_detail()`, `price(slack, *, r, saving_usd, kwh, alpha, urgent)` (§7b: R − α·S/E, ≤ R), `tier_of()`; per-car optional `floor_alpha`, `priority`; constants W_CARBON, ALPHA, FLOOR_EVERY, FIRST_HOUR_KWH (3.5), SPRINT_SLOTS, BUFFER_COST, M_SHORT, M_FLOOR, ASAP_EPS, E_MIN, MIN_KW. Slot 0 carries only its remaining minutes (`dur[0]`); floor checkpoints are times since arrival, pro rata in the slot containing the mark.
- `noonshift/api.py` — FastAPI app, state dict `S`, `resolve()`, `step()`, `session_impact()`, `meter_history()`, ladder `pick_mode()`, `/demo/*`; `POST /sessions/{id}/urgency {level: now|soon|priority, leave_at?}` (`/boost` = alias of `now`; `URGENCY_MIN` 15 min; `PRIORITY` = floor 0.9 / weight 2.0); `site_rate()` = `site.json: employee_rate_usd_per_kwh` (0 = free); `session_price()`.
- `noonshift/sim.py` — `Sim`, `Connector` (taper from 80% SoC), `load_sessions()`.
- `noonshift/models.py` — pydantic REST bodies + WS frames (contract with front-end). `SignalHour` added for `GET /grid/signal` (hourly MOER g/kWh + $/kWh; frames unchanged).
- `web/src/context/GlobalStateContext.jsx` — ops app state: opens `/ws`, polls `/sites/site-1/impact|status`, derives the page shape (siteDetail/connectors Gantt on a 06–22 axis, sessions, alerts from events, chargersList); `triggerEvent()` maps demo buttons → `/demo/*`, prioritize → `/sessions/{id}/boost`.
Expand Down Expand Up @@ -67,6 +67,8 @@ Signals (WattTime MOER; CAISO fuel-mix fallback) + tariff table + sessions (dead
- Open limitation: 1 charger per car for the whole dwell; cars > chargers → deadline becomes 'move-by' time (proposal §9.5).

## Decisions Log
- 2026-09-13 — Emergency = three bands (now/soon/priority), every band pays exactly R; first-hour floor min(need, 3.5 kWh) costs 4.8 pts of CO2 saving on the real day (69.4 → 64.6 %), kept — it is the trust promise (business.md §4).
- 2026-09-13 — First-hour floor beats the signal and the tariff block but never the feed (M_FLOOR 1.0 > block overage 0.83 $/kW-day); a 40-car mass arrival bursts the block, staggered real arrivals do not.
- 2026-09-11 — Wedge = daytime workplace/destination sites, payer = site owner.
- 2026-09-11 — One plug-in question + one Boost toggle; no pricing lanes.
- 2026-09-11 — Elastic LP with progress floor; anti-herding = no broadcast; demo grid = California.
Expand All @@ -77,6 +79,7 @@ Signals (WattTime MOER; CAISO fuel-mix fallback) + tariff table + sessions (dead
- 2026-09-12 — Session data = ACN 2019-04-09 re-dated onto the 2026 signal day; kwh_needed = delivered energy, stated departure = the driver's own input (early leavers kept).

## Changelog
2026-09-13 | WP1 pricing + emergency + first-hour floor | scheduler.py, api.py, models.py, data/site.json, tests/, scripts/smoke.py, .docs/implementation-plan.md | price() = §7b shared savings (never above R); urgency endpoint; slot-0 remaining-duration + time-anchored checkpoints fixed a lost-energy bug that the 3.5 kWh promise exposed
2026-09-13 | Open-source docs pass: README rewrite (Mermaid, quick start, results, OCPP 1.6J badge), LICENSE/CONTRIBUTING/CODE_OF_CONDUCT/SECURITY/CHANGELOG, issue+PR templates, .docs/ARCHITECTURE.md (6 validated diagrams), .docs/README.md index; all human docs moved to .docs/; business.md (break points, §4b emergency scale at R, §7b shared-savings formula), metrics.md, solutions.md (real-world fixes → code; drivers under-state stay 29/36; prove.py plans with perfect foresight) | README.md, .docs/, LICENSE, CONTRIBUTING.md, SECURITY.md, CHANGELOG.md, .github/ | All Mermaid blocks validated with mermaid@11 + jsdom; ROADMAP emergency pricing reconciled to "R, no quota".
2026-09-13 | ROADMAP.md (layman plan: driver inputs, emergency, incentives, Tier 0/1, order of work) + GitHub Actions CI | ROADMAP.md, .github/workflows/ci.yml, wattwise/src/context/WattwiseContext.tsx, web/src/pages/ImpactView.jsx | CI = pytest + prove gate + smoke against live uvicorn + contract-diff + lint/build both apps; lint fixes only (refs not read during render, Card hoisted)
2026-09-13 | team-plan.md integration pass: price preview, peak-avoided, fail-safe banner, smoke gate, pitch/ | api.py, models.py, web/, wattwise/, scripts/smoke.py, scripts/dev.ps1, pitch/, README | Baseline peak = sum of frozen per-session baselines clipped at the feed (label "est."); cost-savings slide left TODO for lack of a source
Expand Down
2 changes: 1 addition & 1 deletion data/site.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"id": "site-1", "feed_kw": 150, "block_kw": 100, "connectors": ["c01", "c02", "c03", "c04", "c05", "c06", "c07", "c08", "c09", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20", "c21", "c22", "c23", "c24", "c25", "c26", "c27", "c28", "c29", "c30", "c31", "c32", "c33", "c34", "c35", "c36", "c37", "c38", "c39", "c40", "c41", "c42", "c43", "c44", "c45", "c46", "c47", "c48", "c49", "c50", "c51", "c52", "c53", "c54", "c55", "c56", "c57", "c58", "c59", "c60"], "p_max_kw": 7.0, "building_load_kw": [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 21.7, 23.3, 25.0, 26.7, 28.3, 30.0, 31.7, 33.3, 35.0, 36.7, 38.3, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 38.3, 36.7, 35.0, 33.3, 31.7, 30.0, 28.3, 26.7, 25.0, 23.3, 21.7, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0]}
{"id": "site-1", "feed_kw": 150, "block_kw": 100, "connectors": ["c01", "c02", "c03", "c04", "c05", "c06", "c07", "c08", "c09", "c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19", "c20", "c21", "c22", "c23", "c24", "c25", "c26", "c27", "c28", "c29", "c30", "c31", "c32", "c33", "c34", "c35", "c36", "c37", "c38", "c39", "c40", "c41", "c42", "c43", "c44", "c45", "c46", "c47", "c48", "c49", "c50", "c51", "c52", "c53", "c54", "c55", "c56", "c57", "c58", "c59", "c60"], "p_max_kw": 7.0, "building_load_kw": [20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 21.7, 23.3, 25.0, 26.7, 28.3, 30.0, 31.7, 33.3, 35.0, 36.7, 38.3, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 40.0, 38.3, 36.7, 35.0, 33.3, 31.7, 30.0, 28.3, 26.7, 25.0, 23.3, 21.7, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0, 20.0], "employee_rate_usd_per_kwh": 0.25, "driver_share": 0.5, "noonshift_share": 0.2}
99 changes: 98 additions & 1 deletion docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,60 @@
}
}
},
"/sessions/{sid}/urgency": {
"post": {
"summary": "Urgency",
"description": "business.md \u00a74b. now: deadline = now, full power (the ASAP path). soon: deadline = leave_at, the LP picks the\ncleanest slots inside the window (< 15 min away = now). priority: deadline unchanged, floor 90 % pro-rata and the\nshortfall weight doubled. Every band marks the session urgent, so price() returns exactly R.",
"operationId": "urgency_sessions__sid__urgency_post",
"parameters": [
{
"name": "sid",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"title": "Sid"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UrgencyIn"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SessionOut"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/sessions/{sid}/boost": {
"post": {
"summary": "Boost",
"description": "Alias for urgency \"now\" (WattWise's \"Charge now\" button).",
"operationId": "boost_sessions__sid__boost_post",
"parameters": [
{
Expand Down Expand Up @@ -292,7 +343,7 @@
"/price": {
"get": {
"summary": "Price Preview",
"description": "Deadline sets the price: preview the tier for a ready-by time before the driver commits.",
"description": "What a ready-by would cost before plugging in. No session yet, so the discount is today's site saving rate so far\n(sum of measured session savings over delivered kWh) applied to this need: an estimate, settled on the receipt.",
"operationId": "price_preview_price_get",
"parameters": [
{
Expand Down Expand Up @@ -1021,6 +1072,22 @@
"boost": {
"type": "boolean",
"title": "Boost"
},
"urgency": {
"anyOf": [
{
"type": "string",
"enum": [
"now",
"soon",
"priority"
]
},
{
"type": "null"
}
],
"title": "Urgency"
}
},
"type": "object",
Expand Down Expand Up @@ -1127,6 +1194,36 @@
],
"title": "StatusOut"
},
"UrgencyIn": {
"properties": {
"level": {
"type": "string",
"enum": [
"now",
"soon",
"priority"
],
"title": "Level"
},
"leave_at": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "null"
}
],
"title": "Leave At"
}
},
"type": "object",
"required": [
"level"
],
"title": "UrgencyIn"
},
"ValidationError": {
"properties": {
"loc": {
Expand Down
Loading
Loading