Context
In `bin/base-demo-services`, `service_rows()` maps any non-healthy non-required service to the single state `"stopped"`:
```python
state = "healthy" if ok else "unhealthy"
if not ok and not service.get("required", False):
state = "stopped"
```
This means a service that was never started, a service whose process crashed, and a service with a misconfigured health check all display identically as `stopped`. A user watching the status table after attempting to start a service that failed to bind its port will see `stopped` with no indication that something went wrong.
The `cmd_check` output at least separates `fail` from `skip optional`, but `cmd_status` (the table) offers no differentiation.
Scope
- Introduce a distinct `error` state for optional services that have been started (PID or compose state exists) but whose health check is failing.
- Keep `stopped` for services that have never been started (no state file, no compose container).
- Update the status table, the state column, and `cmd_check` output to surface the `error` state clearly.
- Update `tests/services_test.bats` to cover the new state distinction.
Validation
- Start a process service, kill the process externally, run `services status`: should show `error`, not `stopped`.
- A service that was never started should still show `stopped`.
- `BASE_DEMO_SERVICES_DRY_RUN=1 ./bin/base-demo-services status` should not regress.
- `./tests/validate.sh`
Context
In `bin/base-demo-services`, `service_rows()` maps any non-healthy non-required service to the single state `"stopped"`:
```python
state = "healthy" if ok else "unhealthy"
if not ok and not service.get("required", False):
state = "stopped"
```
This means a service that was never started, a service whose process crashed, and a service with a misconfigured health check all display identically as `stopped`. A user watching the status table after attempting to start a service that failed to bind its port will see `stopped` with no indication that something went wrong.
The `cmd_check` output at least separates `fail` from `skip optional`, but `cmd_status` (the table) offers no differentiation.
Scope
Validation