From 76daaa6d961fe5ac4b0c5aaf1b77e364fd160171 Mon Sep 17 00:00:00 2001 From: ClaydeCode Date: Mon, 20 Jul 2026 08:07:49 +0000 Subject: [PATCH 1/3] feat: route Sundial at /sundial alongside legacy web-terminal Add the sundial container (ghcr.io/freeshardbase/sundial:0.1.0) to the shard compose stack and route it at PathPrefix(/sundial) with priority 2 so it wins over web-terminal's PathPrefix(/) priority-1 catch-all. The router chains redirect-sundial-slash (RedirectRegex forcing bare /sundial to /sundial/) before strip-sundial (StripPrefix /sundial), because the Sundial image serves at root and derives its base path client-side from the browser URL: stripping lets the root-serving image work behind /sundial, and the trailing-slash redirect makes base detection resolve to /sundial/ instead of / (which would break asset links and fall through to web-terminal). Like web-terminal, the route is unauthenticated: Sundial is a public UI shell that itself calls the authed /core/protected API. Co-Authored-By: Claude Fable 5 --- docker-compose.yml | 7 +++++ shard_core/service/traefik_dynamic_config.py | 29 ++++++++++++++++++++ tests/test_traefik_dyn_spec.py | 25 +++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index f25170bc..4eb20fb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,3 +75,10 @@ services: restart: always networks: - portal + + sundial: + image: ghcr.io/freeshardbase/sundial:0.1.0 + container_name: sundial + restart: always + networks: + - portal diff --git a/shard_core/service/traefik_dynamic_config.py b/shard_core/service/traefik_dynamic_config.py index bab844e9..8915c3a7 100644 --- a/shard_core/service/traefik_dynamic_config.py +++ b/shard_core/service/traefik_dynamic_config.py @@ -69,6 +69,14 @@ def _add_http_section(model: t.Model, portal: SafeIdentity): service="web-terminal", tls=make_http_cert_resolver(portal), ), + "sundial": t.HttpRouter( + rule="PathPrefix(`/sundial`)", + priority=2, + entryPoints=[http_entrypoint], + service="sundial", + middlewares=["redirect-sundial-slash", "strip-sundial"], + tls=make_http_cert_resolver(portal), + ), "traefik": t.HttpRouter( rule=f"HostRegexp(`traefik.{portal.domain}`)", entryPoints=[http_entrypoint], @@ -84,6 +92,20 @@ def _add_http_section(model: t.Model, portal: SafeIdentity): stripPrefix=t.StripPrefixMiddleware(prefixes=["/core/"]) ) ), + "strip-sundial": t.HttpMiddleware( + root=t.HttpMiddlewareItem21( + stripPrefix=t.StripPrefixMiddleware(prefixes=["/sundial"]) + ) + ), + "redirect-sundial-slash": t.HttpMiddleware( + root=t.HttpMiddlewareItem16( + redirectRegex=t.RedirectRegexMiddleware( + regex="^(https?://[^/]+)/sundial$", + replacement="${1}/sundial/", + permanent=True, + ) + ) + ), "auth-private": t.HttpMiddleware( root=t.HttpMiddlewareItem9( forwardAuth=t.ForwardAuthMiddleware( @@ -147,6 +169,13 @@ def _add_http_section(model: t.Model, portal: SafeIdentity): ) ) ), + "sundial": t.HttpService( + root=t.HttpServiceItem( + loadBalancer=t.HttpLoadBalancerService( + servers=[t.Server(url="http://sundial:80/")] + ) + ) + ), } model.http = t.Http(routers=_routers, middlewares=_middlewares, services=_services) diff --git a/tests/test_traefik_dyn_spec.py b/tests/test_traefik_dyn_spec.py index d379a764..04249e45 100644 --- a/tests/test_traefik_dyn_spec.py +++ b/tests/test_traefik_dyn_spec.py @@ -23,20 +23,34 @@ async def test_template_is_written(api_client): "app-error", "auth", "strip", + "strip-sundial", + "redirect-sundial-slash", "auth-public", "auth-private", "auth-management", } assert "authResponseHeadersRegex" in out_middlewares["auth"]["forwardAuth"] + assert out_middlewares["strip-sundial"]["stripPrefix"]["prefixes"] == [ + "/sundial" + ] + assert out_middlewares["redirect-sundial-slash"]["redirectRegex"] == { + "regex": "^(https?://[^/]+)/sundial$", + "replacement": "${1}/sundial/", + "permanent": True, + } out_services_http: dict = output["http"]["services"] assert set(out_services_http.keys()) == { "shard_core", "web-terminal", + "sundial", "filebrowser_http", "paperless-ngx_http", "immich_http", } + assert out_services_http["sundial"]["loadBalancer"]["servers"] == [ + {"url": "http://sundial:80/"} + ] assert out_services_http["filebrowser_http"]["loadBalancer"]["servers"] == [ {"url": "http://filebrowser:80"} ] @@ -47,6 +61,7 @@ async def test_template_is_written(api_client): "shard_core_public", "shard_core_management", "web-terminal", + "sundial", "traefik", "filebrowser_http", "paperless-ngx_http", @@ -54,6 +69,16 @@ async def test_template_is_written(api_client): } assert out_routers_http["filebrowser_http"]["service"] == "filebrowser_http" + sundial_router = out_routers_http["sundial"] + assert sundial_router["rule"] == "PathPrefix(`/sundial`)" + assert sundial_router["service"] == "sundial" + assert sundial_router["middlewares"] == [ + "redirect-sundial-slash", + "strip-sundial", + ] + assert sundial_router["priority"] > out_routers_http["web-terminal"]["priority"] + assert out_routers_http["web-terminal"]["rule"] == "PathPrefix(`/`)" + def _write_app_meta(app_name: str): app_path = get_installed_apps_path() / app_name From a84e040bb686f71543f20a669db850aa9f9f368a Mon Sep 17 00:00:00 2001 From: ClaydeCode Date: Mon, 20 Jul 2026 08:08:10 +0000 Subject: [PATCH 2/3] set version to 0.41.0 --- docker-compose.yml | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4eb20fb9..af7f68cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,7 @@ services: condition: service_healthy shard_core: - image: ghcr.io/freeshardbase/freeshard:0.40.0 + image: ghcr.io/freeshardbase/freeshard:0.41.0 container_name: shard_core restart: always stop_grace_period: 30s diff --git a/pyproject.toml b/pyproject.toml index 1757ddad..644aaa9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "shard_core" -version = "0.40.0" +version = "0.41.0" description = "Core software stack that manages all aspects of a Shard" readme = "README.md" license-files = ["LICENSE.md"] diff --git a/uv.lock b/uv.lock index 98a31e01..8fc52f3f 100644 --- a/uv.lock +++ b/uv.lock @@ -1813,7 +1813,7 @@ wheels = [ [[package]] name = "shard-core" -version = "0.39.5" +version = "0.41.0" source = { editable = "." } dependencies = [ { name = "aiofiles" }, From 8ce1da43d3dfca19ea005b0d94d87ec7fb27abf3 Mon Sep 17 00:00:00 2001 From: ClaydeCode Date: Mon, 20 Jul 2026 08:57:16 +0000 Subject: [PATCH 3/3] chore: scope PR to the /sundial traefik route only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the compose sundial service + the version bump. The deployed shard compose is owned by the controller (data/core-versions//docker-compose.yml, generated by new_core_version.py) — freeshard's docker-compose.yml is not shipped, so adding the service here is a no-op. The version is bumped at release, not during feature work. Only the traefik route (which ships in the shard_core image and is generated at runtime) belongs here. Co-Authored-By: Claude Opus 4.8 (1M context) --- docker-compose.yml | 9 +-------- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index af7f68cd..f25170bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,7 @@ services: condition: service_healthy shard_core: - image: ghcr.io/freeshardbase/freeshard:0.41.0 + image: ghcr.io/freeshardbase/freeshard:0.40.0 container_name: shard_core restart: always stop_grace_period: 30s @@ -75,10 +75,3 @@ services: restart: always networks: - portal - - sundial: - image: ghcr.io/freeshardbase/sundial:0.1.0 - container_name: sundial - restart: always - networks: - - portal diff --git a/pyproject.toml b/pyproject.toml index 644aaa9b..1757ddad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "shard_core" -version = "0.41.0" +version = "0.40.0" description = "Core software stack that manages all aspects of a Shard" readme = "README.md" license-files = ["LICENSE.md"] diff --git a/uv.lock b/uv.lock index 8fc52f3f..98a31e01 100644 --- a/uv.lock +++ b/uv.lock @@ -1813,7 +1813,7 @@ wheels = [ [[package]] name = "shard-core" -version = "0.41.0" +version = "0.39.5" source = { editable = "." } dependencies = [ { name = "aiofiles" },