diff --git a/SPEC.md b/SPEC.md index 2aa7cb17..a4d5d0f3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1359,8 +1359,11 @@ If implemented: - The HTTP server is an extension and is not required for conformance. - The implementation may serve server-rendered HTML or a client-side application for the dashboard. -- The dashboard/API must be observability/control surfaces only and must not become required for - orchestrator correctness. +- The dashboard and `/api/v1/*` contract must remain observability/control surfaces only and must + not become required for orchestrator correctness. +- Implementations may expose additional explicitly optional routes (for example a self-contained + local demo at `/pomodoro`) as long as those routes stay separate from the dashboard/API contract + and are never required for orchestration. Enablement (extension): @@ -1383,6 +1386,8 @@ Enablement (extension): retry delays, token consumption, runtime totals, recent events, and health/error indicators). - It is up to the implementation whether this is server-generated HTML or a client-side app that consumes the JSON API below. +- Additional implementation-specific HTML pages may exist at other routes, but they are optional + and outside the conformance surface defined in this section. #### 13.7.2 JSON REST API (`/api/v1/*`) diff --git a/elixir/README.md b/elixir/README.md index 9ad35355..a8632241 100644 --- a/elixir/README.md +++ b/elixir/README.md @@ -125,8 +125,9 @@ codex: ``` - If `WORKFLOW.md` is missing or has invalid YAML, startup and scheduling are halted until fixed. -- `server.port` or CLI `--port` enables the optional HTTP dashboard and JSON API at `/`, - `/api/v1/state`, `/api/v1/`, and `/api/v1/refresh`. +- `server.port` or CLI `--port` enables the optional HTTP dashboard plus pomodoro demo at `/` + and `/pomodoro`, alongside the JSON API at `/api/v1/state`, `/api/v1/`, + and `/api/v1/refresh`. ## Project Layout diff --git a/elixir/lib/symphony_elixir/http_server.ex b/elixir/lib/symphony_elixir/http_server.ex index 8e1f8625..eb13666b 100644 --- a/elixir/lib/symphony_elixir/http_server.ex +++ b/elixir/lib/symphony_elixir/http_server.ex @@ -231,6 +231,9 @@ defmodule SymphonyElixir.HttpServer do defp route(%{method: "GET", path: "/"} = request, orchestrator, snapshot_timeout_ms), do: html_response(200, render_dashboard(request, orchestrator, snapshot_timeout_ms)) + defp route(%{method: "GET", path: "/pomodoro"}, _orchestrator, _snapshot_timeout_ms), + do: html_response(200, render_pomodoro_app()) + defp route(%{method: "GET", path: "/api/v1/state"}, orchestrator, snapshot_timeout_ms), do: json_response(200, state_payload(orchestrator, snapshot_timeout_ms)) @@ -260,6 +263,9 @@ defmodule SymphonyElixir.HttpServer do defp route(%{path: "/"}, _orchestrator, _snapshot_timeout_ms), do: error_response(405, "method_not_allowed", "Method not allowed") + defp route(%{path: "/pomodoro"}, _orchestrator, _snapshot_timeout_ms), + do: error_response(405, "method_not_allowed", "Method not allowed") + defp route(%{path: "/api/v1/" <> _issue_identifier}, _orchestrator, _snapshot_timeout_ms), do: error_response(405, "method_not_allowed", "Method not allowed") @@ -420,18 +426,647 @@ defmodule SymphonyElixir.HttpServer do #{title}

#{title}

+

+ Optional observability endpoints for the local Symphony runtime. + For a focused demo experience, open the standalone pomodoro app. +

+
#{body}
""" end + defp render_pomodoro_app do + ~S""" + + + + + + Pomodoro Timer + + + +
+
+ ← Back to Symphony dashboard +

Standalone focus timer

+
+ +
+

Pomodoro web app

+

Pomodoro Timer

+

+ Configure a focus session, start the countdown, and let the page roll into your + break automatically. Changing the duration inputs resets the timer so every run stays + predictable for demos and daily use. +

+
+ +
+
+ Focus session +
+

25:00

+

+ Set your durations, then start a focus session. +

+
+ +
+
+ Progress + 0% +
+ +
+ +
+ + + +
+ +
+
+ Current phase + Focus +
+
+ Next up + Break +
+
+ Completed focus sessions + 0 +
+
+ Current duration + 25:00 +
+
+
+ + +
+
+ + + + + """ + end + defp parse_host({_, _, _, _} = ip), do: {:ok, ip} defp parse_host({_, _, _, _, _, _, _, _} = ip), do: {:ok, ip} diff --git a/elixir/test/symphony_elixir/extensions_test.exs b/elixir/test/symphony_elixir/extensions_test.exs index 2c40990e..484f1582 100644 --- a/elixir/test/symphony_elixir/extensions_test.exs +++ b/elixir/test/symphony_elixir/extensions_test.exs @@ -337,6 +337,15 @@ defmodule SymphonyElixir.ExtensionsTest do assert status == 200 assert Map.fetch!(headers, "content-type") =~ "text/html" assert body =~ "Symphony Dashboard" + assert body =~ "Open pomodoro app" + + {status, headers, body} = http_request(port, "GET", "/pomodoro") + assert status == 200 + assert Map.fetch!(headers, "content-type") =~ "text/html" + assert body =~ "Pomodoro Timer" + assert body =~ "Focus duration" + assert body =~ "Start" + assert body =~ "Break duration" {status, headers, body} = http_request(port, "GET", "/api/v1/state") assert status == 200 @@ -407,6 +416,10 @@ defmodule SymphonyElixir.ExtensionsTest do assert status == 405 assert %{"error" => %{"code" => "method_not_allowed"}} = Jason.decode!(body) + {status, _headers, body} = http_request(port, "POST", "/pomodoro", "") + assert status == 405 + assert %{"error" => %{"code" => "method_not_allowed"}} = Jason.decode!(body) + {status, _headers, body} = http_request(port, "POST", "/api/v1/MT-1", "") assert status == 405 assert %{"error" => %{"code" => "method_not_allowed"}} = Jason.decode!(body)