hero_cockpit — end-user control surface for a Hero OS demo VM (deep spec) #1

Open
opened 2026-05-20 21:41:38 +00:00 by mik-tf · 0 comments
Owner

hero_cockpit — end-user control surface for a Hero OS demo VM

Spec for the user-facing UI that ships on every demo VM provisioned by hero_os_tfgrid_deployer. Pulls together the meeting notes at hero_os_tfgrid_deployer#1, the s132 VM-bootstrap groundwork in hero_demo deploy/single-vm/scripts/setup-binaries.sh, and the canonical skills.

1. Goal

A simple, lightweight, web-based control surface that an end user reaches via their VM's public gateway URL (e.g. https://herolab.gent02.grid.tf/). Once authenticated through hero_proxy (OAuth via Forge), the user can:

  • See which Hero OS services are installed and their state (running / stopped / failed)
  • Enable / disable / restart services from a menu
  • Set their bring-your-own AI API keys (OpenRouter, Groq, SambaNova, Forge token)
  • Upgrade their installation via a button (triggers lab update through hero_proc)
  • Submit feedback (text + voice when ready) → flows into lhumina_public/feedback
  • Read a manual / docs page for the supported services
  • Be reminded prominently that this is a non-production demo and data may be lost

It is explicitly NOT:

  • A billing / onboarding surface (deferred per meeting decision)
  • A deployer admin tool (that's hero_os_tfgrid_deployer, separate repo)
  • A heavy productivity suite (Office is out, Books / Slides / Whiteboard / Agent / Voice are in)

2. Architecture

Scaffold from hero_template so cockpit follows the canonical Hero service workspace shape. Final crate layout:

Crate Binary Socket Purpose
hero_cockpit hero_cockpit Lifecycle CLI (--start / --stop / --status / --info)
hero_cockpit_server hero_cockpit_server hero_cockpit/rpc.sock OpenRPC backend — service state, BYO-key writes, upgrade trigger, feedback proxy
hero_cockpit_sdk (lib) Auto-generated typed client (used by future deployer + other services)
hero_cockpit_admin hero_cockpit_admin hero_cockpit/admin.sock Admin dashboard (lower-level config, secret rotation, log viewer) per /hero_ui_dashboard_admin skill
hero_cockpit_web hero_cockpit_web hero_cockpit/web.sock The actual user-facing UI — landing page + service menu + settings + feedback + manual

All _admin + _web use hero_admin_lib per the /hero_ui_dashboard skill (shared Bootstrap + unpoly + Chart.js + connection-status widget, base_path_middleware, /health + /.well-known/heroservice.json, axum 0.8, Askama templates).

hero_cockpit_web integrates the iframe / postMessage protocols (hero:theme, hero:route) per /web_embed so the cockpit pages embed cleanly inside the Hero OS shell when navigated to /hero_os/ui/cockpit.

3. Page inventory (the _web crate)

3.1 Landing — /

  • Hero header + "This is a non-production demo. Your data may be lost at any time." banner (always visible, sticky)
  • Status tiles: total services installed / running / stopped / disk free / memory free
  • Big primary buttons: Manage Services, Settings, Feedback, Manual
  • Quick-links to running services (deep-link into /hero_os/ui/<service> via hero_proxy)

3.2 Services — /services

  • Table of all installed services from the per-user manifest (see §6)
  • Per-row: name, description, state (running / stopped / failed), CPU, RSS, action buttons (start / stop / restart / disable)
  • Action buttons call hero_cockpit_server RPC → server invokes hero_proc service <action> <name> and the underlying lab service flow
  • Bulk actions: "Enable selected", "Disable selected" → updates the per-user manifest + applies via hero_proc
  • "Reset to default profile" button (reloads the deployer-written defaults)

3.3 Settings — /settings

  • BYO AI keys form: GROQ_API_KEY, OPENROUTER_API_KEY, SAMBANOVA_API_KEY, FORGE_TOKEN
  • Each field reads from hero_proc secret store (masked) + writes back via hero_cockpit_serverhero_proc secret set
  • "Test connection" button per key — server calls a minimal API on the provider to confirm validity (avoids polluting logs with bad-key noise downstream)
  • VM info: gateway URL, mycelium IP, node, OS, embedder model in use, hero_proc version
  • "Upgrade now" button → hero_cockpit_serverlab update (covered separately in §5)

3.4 Feedback — /feedback

  • Initial iteration: iframe pointing at https://forge.ourworld.tf/lhumina_public/feedback/issues/new?template=feedback
  • User signs in via Forge directly through the iframe; submits their issue; cockpit listens for postMessage confirmation
  • Future iteration: native form (text + voice when Scott's voice integration lands) that POSTs via Forge REST from cockpit_server, no iframe
  • No Rhai scripts (meeting decision — keep it simple)

3.5 Manual — /manual

  • Static markdown pages describing each supported service: what it does, how to use it
  • Generated at build time from docs/manual/*.md in the cockpit crate (we curate this content)
  • Index page + per-service pages
  • Embed-friendly (hero:theme / hero:route integration)

3.6 About / Warning — /about

  • Repeats the non-production warning prominently
  • Lists data locations + how to backup (hero_proc secret export, etc.)
  • Lists the components currently installed + their versions
  • Link to the public feedback repo
  • Credits / OSS

4. API surface — hero_cockpit_server (OpenRPC)

The server is the integration point. It owns:

Method Purpose Calls into
cockpit.list_services Enumerate per-user manifest + live state hero_proc service list
cockpit.start_service(name) Start a single service hero_proc service start
cockpit.stop_service(name) Stop hero_proc service stop
cockpit.restart_service(name) Restart hero_proc service restart
cockpit.enable_service(name) Add to per-user manifest + start manifest write + hero_proc
cockpit.disable_service(name) Remove from manifest + stop manifest write + hero_proc
cockpit.get_byok_keys() Read masked BYO keys hero_proc secret get
cockpit.set_byok_key(name, value) Write a BYO key hero_proc secret set
cockpit.test_byok_key(name) Test a key against its provider provider HTTP API
cockpit.upgrade() Trigger full upgrade hero_proc → lab update for each installed service
cockpit.system_info() Gateway URL, mycelium IP, versions, RAM/disk system facts
cockpit.submit_feedback(title, body, category) Native feedback flow Forge REST
cockpit.expose_service(service, subdomain) Add a URL mapping in hero_proxy hero_proxy domain.add
cockpit.unexpose_service(domain_id) Remove mapping hero_proxy domain.delete

5. Upgrade flow

User clicks "Upgrade now" on /settings:

  1. hero_cockpit_web → POST cockpit.upgrade on cockpit_server
  2. cockpit_server iterates the per-user manifest's [enabled] list
  3. For each enabled component: hero_proc job submit lab build <name> --download --install --force
  4. Stream job logs back via SSE to the cockpit web UI
  5. On completion, restart affected services via hero_proc service restart
  6. Confirm UI gets fresh cockpit.system_info() showing new versions

6. Per-user component manifest

The contract between the deployer (which writes it once at provisioning) and cockpit (which reads + edits it).

Path on VM: ~driver/hero/cfg/cockpit/services.toml

Format:

# Generated by hero_os_tfgrid_deployer for <user>.
# Cockpit edits this file when user enables/disables services.

profile = "demo"   # one of: demo, lightweight, books-only, custom

[enabled]
hero_proxy = true
hero_router = true
hero_proc   = true   # always-on (synthetic — not actually toggleable)
hero_embedder = true # uses small model on demo profile
hero_db = true
hero_books = true
hero_cockpit = true  # synthetic — itself
hero_slides = false
hero_whiteboard = false
hero_collab = false
hero_agent = false
hero_voice = false

[embedder]
model = "small"  # vs "default" — keeps RAM down on 8 GB VMs

[byok]
# Written by cockpit. Values stored in hero_proc secrets, never here.
groq = "<set>"
openrouter = "<set>"
sambanova = "<unset>"
forge = "<set>"

[deployer]
provisioned_at = "2026-05-20T20:00:00Z"
provisioned_by = "hero_os_tfgrid_deployer/v0.1.0"
user_forge_id = "demo_user_42"

Default profile = demo:

  • Always-on: hero_proxy, hero_router, hero_proc, hero_cockpit
  • Default-enabled: hero_embedder (small), hero_db, hero_books

Lightweight profile: only always-on + hero_books.

Books-only profile: like lightweight + nothing else.

Custom: nothing default-enabled; user picks via cockpit.

The setup-binaries.sh refactor (separate issue) reads this file to pick which lab build $repo --download --install calls to make.

7. Auth — hero_proxy + Forge OAuth

End-user accesses cockpit via https://herolab.<node>.grid.tf/ → hits hero_proxy → proxy enforces OAuth-via-Forge → on success, request is forwarded to cockpit_web's web.sock.

hero_proxy domain.add is called once at deployer-time:

  • domain = "herolab.<node>.grid.tf"
  • target_type = "socket"
  • target = "/home/driver/hero/var/sockets/hero_cockpit/web.sock"
  • auth_mode = "oauth"
  • oauth_provider = "forge.ourworld.tf"
  • allowed_pubkeys = [<user's forge id>]
  • strip_prefix = false
  • https_redirect = true

Cockpit reads the authenticated forge user from request headers (X-Hero-Claims per hero_admin_lib::middleware::HeroClaims) and uses that to gate write operations.

8. Dynamic URL mapping — "share a service"

From the meeting notes: "user wants to expose a channel on the whiteboard — we have whiteboard — can map url to their proxy — e.g. mik..grid.tf — can invite other people to play with it".

User flow:

  1. User in hero_whiteboard_web shares a channel publicly
  2. Whiteboard widget shows "Get a public URL" button
  3. Button posts to cockpit.expose_service(service: "hero_whiteboard", subdomain: "mik")
  4. cockpit_server → hero_proxy domain.add with domain = "mik.herolab.gent02.grid.tf" (or wildcard / dynamic subdomain pattern TBD with hero_proxy)
  5. cockpit shows the shareable URL + a copy button + an unexpose button

9. Boundary with deployer

  • Deployer (separate repo) provisions VM + writes initial per-user manifest at ~/hero/cfg/cockpit/services.toml
  • Deployer runs setup-binaries.sh (from this PR/the hero_demo repo) which reads the manifest
  • After bootstrap, cockpit owns the manifest (deployer doesn't touch it again unless user requests a reset)
  • Cockpit reports state back to the deployer via Forge metadata (the deployer admin UI calls cockpit.system_info over the VM's gateway to surface state in its dashboard)

10. Boundary with hero_compute

None. Cockpit lives inside the VM after hero_compute is done. Cockpit has no need to call hero_compute's API.

11. Out of scope (initial)

  • Billing / payment
  • Self-service onboarding (admin-driven for now)
  • Multi-user-per-VM (one demo user owns one VM)
  • Office / OnlyOffice (too heavy for 8 GB)
  • Voice (waits for Scott's hero_voice end-to-end work)
  • AI inference paid for by the team (BYO keys only)

12. Implementation plan — proposed session map

Session Focus
A1 (~s134) Scaffold from hero_template. Build empty crates that pass cargo check, register service.toml, /health + /.well-known endpoints.
A2 Service-list page (/services) + cockpit_server RPCs: list_services, start/stop/restart_service.
A3 Settings page (/settings) + cockpit_server RPCs: get/set/test_byok_key, system_info.
A4 Feedback iframe + Manual pages.
A5 Per-user manifest read/write. Profile switching.
A6 Upgrade button + cockpit.upgrade flow + SSE job log streaming.
A7 Dynamic URL mapping (cockpit.expose_service / unexpose_service) + hero_proxy admin integration.

13. References

## hero_cockpit — end-user control surface for a Hero OS demo VM Spec for the user-facing UI that ships on every demo VM provisioned by `hero_os_tfgrid_deployer`. Pulls together the meeting notes at [`hero_os_tfgrid_deployer#1`](https://forge.ourworld.tf/lhumina_code/hero_os_tfgrid_deployer/issues/1), the s132 VM-bootstrap groundwork in [`hero_demo` deploy/single-vm/scripts/setup-binaries.sh](https://forge.ourworld.tf/lhumina_code/hero_demo/src/branch/development/deploy/single-vm/scripts/setup-binaries.sh), and the canonical skills. ## 1. Goal A simple, lightweight, web-based control surface that an end user reaches via their VM's public gateway URL (e.g. `https://herolab.gent02.grid.tf/`). Once authenticated through hero_proxy (OAuth via Forge), the user can: - See which Hero OS services are installed and their state (running / stopped / failed) - Enable / disable / restart services from a menu - Set their bring-your-own AI API keys (OpenRouter, Groq, SambaNova, Forge token) - Upgrade their installation via a button (triggers `lab update` through hero_proc) - Submit feedback (text + voice when ready) → flows into `lhumina_public/feedback` - Read a manual / docs page for the supported services - Be reminded prominently that this is a **non-production demo** and data may be lost It is explicitly NOT: - A billing / onboarding surface (deferred per meeting decision) - A deployer admin tool (that's `hero_os_tfgrid_deployer`, separate repo) - A heavy productivity suite (Office is out, Books / Slides / Whiteboard / Agent / Voice are in) ## 2. Architecture Scaffold from [`hero_template`](https://forge.ourworld.tf/lhumina_code/hero_template) so cockpit follows the canonical Hero service workspace shape. Final crate layout: | Crate | Binary | Socket | Purpose | |---|---|---|---| | `hero_cockpit` | `hero_cockpit` | — | Lifecycle CLI (`--start` / `--stop` / `--status` / `--info`) | | `hero_cockpit_server` | `hero_cockpit_server` | `hero_cockpit/rpc.sock` | OpenRPC backend — service state, BYO-key writes, upgrade trigger, feedback proxy | | `hero_cockpit_sdk` | (lib) | — | Auto-generated typed client (used by future deployer + other services) | | `hero_cockpit_admin` | `hero_cockpit_admin` | `hero_cockpit/admin.sock` | Admin dashboard (lower-level config, secret rotation, log viewer) per `/hero_ui_dashboard_admin` skill | | `hero_cockpit_web` | `hero_cockpit_web` | `hero_cockpit/web.sock` | The actual user-facing UI — landing page + service menu + settings + feedback + manual | All `_admin` + `_web` use `hero_admin_lib` per the `/hero_ui_dashboard` skill (shared Bootstrap + unpoly + Chart.js + connection-status widget, base_path_middleware, /health + /.well-known/heroservice.json, axum 0.8, Askama templates). `hero_cockpit_web` integrates the iframe / postMessage protocols (`hero:theme`, `hero:route`) per `/web_embed` so the cockpit pages embed cleanly inside the Hero OS shell when navigated to `/hero_os/ui/cockpit`. ## 3. Page inventory (the `_web` crate) ### 3.1 Landing — `/` - Hero header + "This is a non-production demo. Your data may be lost at any time." banner (always visible, sticky) - Status tiles: total services installed / running / stopped / disk free / memory free - Big primary buttons: Manage Services, Settings, Feedback, Manual - Quick-links to running services (deep-link into `/hero_os/ui/<service>` via hero_proxy) ### 3.2 Services — `/services` - Table of all installed services from the per-user manifest (see §6) - Per-row: name, description, state (running / stopped / failed), CPU, RSS, action buttons (start / stop / restart / disable) - Action buttons call `hero_cockpit_server` RPC → server invokes `hero_proc service <action> <name>` and the underlying `lab service` flow - Bulk actions: "Enable selected", "Disable selected" → updates the per-user manifest + applies via hero_proc - "Reset to default profile" button (reloads the deployer-written defaults) ### 3.3 Settings — `/settings` - BYO AI keys form: GROQ_API_KEY, OPENROUTER_API_KEY, SAMBANOVA_API_KEY, FORGE_TOKEN - Each field reads from hero_proc secret store (masked) + writes back via `hero_cockpit_server` → `hero_proc secret set` - "Test connection" button per key — server calls a minimal API on the provider to confirm validity (avoids polluting logs with bad-key noise downstream) - VM info: gateway URL, mycelium IP, node, OS, embedder model in use, hero_proc version - "Upgrade now" button → `hero_cockpit_server` → `lab update` (covered separately in §5) ### 3.4 Feedback — `/feedback` - Initial iteration: iframe pointing at `https://forge.ourworld.tf/lhumina_public/feedback/issues/new?template=feedback` - User signs in via Forge directly through the iframe; submits their issue; cockpit listens for postMessage confirmation - Future iteration: native form (text + voice when Scott's voice integration lands) that POSTs via Forge REST from cockpit_server, no iframe - No Rhai scripts (meeting decision — keep it simple) ### 3.5 Manual — `/manual` - Static markdown pages describing each supported service: what it does, how to use it - Generated at build time from `docs/manual/*.md` in the cockpit crate (we curate this content) - Index page + per-service pages - Embed-friendly (`hero:theme` / `hero:route` integration) ### 3.6 About / Warning — `/about` - Repeats the non-production warning prominently - Lists data locations + how to backup (`hero_proc secret export`, etc.) - Lists the components currently installed + their versions - Link to the public feedback repo - Credits / OSS ## 4. API surface — `hero_cockpit_server` (OpenRPC) The server is the integration point. It owns: | Method | Purpose | Calls into | |---|---|---| | `cockpit.list_services` | Enumerate per-user manifest + live state | hero_proc `service list` | | `cockpit.start_service(name)` | Start a single service | hero_proc `service start` | | `cockpit.stop_service(name)` | Stop | hero_proc `service stop` | | `cockpit.restart_service(name)` | Restart | hero_proc `service restart` | | `cockpit.enable_service(name)` | Add to per-user manifest + start | manifest write + hero_proc | | `cockpit.disable_service(name)` | Remove from manifest + stop | manifest write + hero_proc | | `cockpit.get_byok_keys()` | Read masked BYO keys | hero_proc `secret get` | | `cockpit.set_byok_key(name, value)` | Write a BYO key | hero_proc `secret set` | | `cockpit.test_byok_key(name)` | Test a key against its provider | provider HTTP API | | `cockpit.upgrade()` | Trigger full upgrade | hero_proc → `lab update` for each installed service | | `cockpit.system_info()` | Gateway URL, mycelium IP, versions, RAM/disk | system facts | | `cockpit.submit_feedback(title, body, category)` | Native feedback flow | Forge REST | | `cockpit.expose_service(service, subdomain)` | Add a URL mapping in hero_proxy | hero_proxy `domain.add` | | `cockpit.unexpose_service(domain_id)` | Remove mapping | hero_proxy `domain.delete` | ## 5. Upgrade flow User clicks "Upgrade now" on `/settings`: 1. `hero_cockpit_web` → POST `cockpit.upgrade` on cockpit_server 2. cockpit_server iterates the per-user manifest's `[enabled]` list 3. For each enabled component: `hero_proc job submit lab build <name> --download --install --force` 4. Stream job logs back via SSE to the cockpit web UI 5. On completion, restart affected services via `hero_proc service restart` 6. Confirm UI gets fresh `cockpit.system_info()` showing new versions ## 6. Per-user component manifest The contract between the deployer (which writes it once at provisioning) and cockpit (which reads + edits it). Path on VM: `~driver/hero/cfg/cockpit/services.toml` Format: ```toml # Generated by hero_os_tfgrid_deployer for <user>. # Cockpit edits this file when user enables/disables services. profile = "demo" # one of: demo, lightweight, books-only, custom [enabled] hero_proxy = true hero_router = true hero_proc = true # always-on (synthetic — not actually toggleable) hero_embedder = true # uses small model on demo profile hero_db = true hero_books = true hero_cockpit = true # synthetic — itself hero_slides = false hero_whiteboard = false hero_collab = false hero_agent = false hero_voice = false [embedder] model = "small" # vs "default" — keeps RAM down on 8 GB VMs [byok] # Written by cockpit. Values stored in hero_proc secrets, never here. groq = "<set>" openrouter = "<set>" sambanova = "<unset>" forge = "<set>" [deployer] provisioned_at = "2026-05-20T20:00:00Z" provisioned_by = "hero_os_tfgrid_deployer/v0.1.0" user_forge_id = "demo_user_42" ``` Default profile = demo: - Always-on: hero_proxy, hero_router, hero_proc, hero_cockpit - Default-enabled: hero_embedder (small), hero_db, hero_books Lightweight profile: only always-on + hero_books. Books-only profile: like lightweight + nothing else. Custom: nothing default-enabled; user picks via cockpit. The setup-binaries.sh refactor (separate issue) reads this file to pick which `lab build $repo --download --install` calls to make. ## 7. Auth — hero_proxy + Forge OAuth End-user accesses cockpit via `https://herolab.<node>.grid.tf/` → hits hero_proxy → proxy enforces OAuth-via-Forge → on success, request is forwarded to cockpit_web's web.sock. `hero_proxy domain.add` is called once at deployer-time: - `domain = "herolab.<node>.grid.tf"` - `target_type = "socket"` - `target = "/home/driver/hero/var/sockets/hero_cockpit/web.sock"` - `auth_mode = "oauth"` - `oauth_provider = "forge.ourworld.tf"` - `allowed_pubkeys = [<user's forge id>]` - `strip_prefix = false` - `https_redirect = true` Cockpit reads the authenticated forge user from request headers (`X-Hero-Claims` per `hero_admin_lib::middleware::HeroClaims`) and uses that to gate write operations. ## 8. Dynamic URL mapping — "share a service" From the meeting notes: "user wants to expose a channel on the whiteboard — we have whiteboard — can map url to their proxy — e.g. mik.<vm>.grid.tf — can invite other people to play with it". User flow: 1. User in `hero_whiteboard_web` shares a channel publicly 2. Whiteboard widget shows "Get a public URL" button 3. Button posts to `cockpit.expose_service(service: "hero_whiteboard", subdomain: "mik")` 4. cockpit_server → hero_proxy `domain.add` with `domain = "mik.herolab.gent02.grid.tf"` (or wildcard / dynamic subdomain pattern TBD with hero_proxy) 5. cockpit shows the shareable URL + a copy button + an `unexpose` button ## 9. Boundary with deployer - Deployer (separate repo) provisions VM + writes initial per-user manifest at `~/hero/cfg/cockpit/services.toml` - Deployer runs `setup-binaries.sh` (from this PR/the `hero_demo` repo) which reads the manifest - After bootstrap, cockpit owns the manifest (deployer doesn't touch it again unless user requests a reset) - Cockpit reports state back to the deployer via Forge metadata (the deployer admin UI calls `cockpit.system_info` over the VM's gateway to surface state in its dashboard) ## 10. Boundary with hero_compute None. Cockpit lives inside the VM after hero_compute is done. Cockpit has no need to call hero_compute's API. ## 11. Out of scope (initial) - Billing / payment - Self-service onboarding (admin-driven for now) - Multi-user-per-VM (one demo user owns one VM) - Office / OnlyOffice (too heavy for 8 GB) - Voice (waits for Scott's hero_voice end-to-end work) - AI inference paid for by the team (BYO keys only) ## 12. Implementation plan — proposed session map | Session | Focus | |---|---| | A1 (~s134) | Scaffold from `hero_template`. Build empty crates that pass `cargo check`, register service.toml, /health + /.well-known endpoints. | | A2 | Service-list page (`/services`) + cockpit_server RPCs: list_services, start/stop/restart_service. | | A3 | Settings page (`/settings`) + cockpit_server RPCs: get/set/test_byok_key, system_info. | | A4 | Feedback iframe + Manual pages. | | A5 | Per-user manifest read/write. Profile switching. | | A6 | Upgrade button + cockpit.upgrade flow + SSE job log streaming. | | A7 | Dynamic URL mapping (`cockpit.expose_service` / `unexpose_service`) + hero_proxy admin integration. | ## 13. References - Meeting notes: [`hero_os_tfgrid_deployer#1`](https://forge.ourworld.tf/lhumina_code/hero_os_tfgrid_deployer/issues/1) - VM bootstrap: [`hero_demo` deploy/single-vm/scripts/setup-binaries.sh](https://forge.ourworld.tf/lhumina_code/hero_demo/src/branch/development/deploy/single-vm/scripts/setup-binaries.sh) (s132) - Canonical service workspace template: [`hero_template`](https://forge.ourworld.tf/lhumina_code/hero_template) - Skills: `/hero_website` · `/hero_ui_dashboard` · `/hero_ui_dashboard_admin` · `/hero_admin_lib` · `/web_embed` · `/hero_proc` · `/hero_log` · `/hero_service` - Sister repos: [hero_proxy](https://forge.ourworld.tf/lhumina_code/hero_proxy) (OAuth + URL mapping) · [hero_compute](https://forge.ourworld.tf/lhumina_code/hero_compute) (VM lifecycle, Mahmoud) · [hero_os_tfgrid_deployer](https://forge.ourworld.tf/lhumina_code/hero_os_tfgrid_deployer) (admin tool) · [lhumina_public/feedback](https://forge.ourworld.tf/lhumina_public/feedback) (user feedback target)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_cockpit#1
No description provided.