Skip to content

Commit 76daaa6

Browse files
ClaydeCodeclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 9e90fb3 commit 76daaa6

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

docker-compose.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,10 @@ services:
7575
restart: always
7676
networks:
7777
- portal
78+
79+
sundial:
80+
image: ghcr.io/freeshardbase/sundial:0.1.0
81+
container_name: sundial
82+
restart: always
83+
networks:
84+
- portal

shard_core/service/traefik_dynamic_config.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ def _add_http_section(model: t.Model, portal: SafeIdentity):
6969
service="web-terminal",
7070
tls=make_http_cert_resolver(portal),
7171
),
72+
"sundial": t.HttpRouter(
73+
rule="PathPrefix(`/sundial`)",
74+
priority=2,
75+
entryPoints=[http_entrypoint],
76+
service="sundial",
77+
middlewares=["redirect-sundial-slash", "strip-sundial"],
78+
tls=make_http_cert_resolver(portal),
79+
),
7280
"traefik": t.HttpRouter(
7381
rule=f"HostRegexp(`traefik.{portal.domain}`)",
7482
entryPoints=[http_entrypoint],
@@ -84,6 +92,20 @@ def _add_http_section(model: t.Model, portal: SafeIdentity):
8492
stripPrefix=t.StripPrefixMiddleware(prefixes=["/core/"])
8593
)
8694
),
95+
"strip-sundial": t.HttpMiddleware(
96+
root=t.HttpMiddlewareItem21(
97+
stripPrefix=t.StripPrefixMiddleware(prefixes=["/sundial"])
98+
)
99+
),
100+
"redirect-sundial-slash": t.HttpMiddleware(
101+
root=t.HttpMiddlewareItem16(
102+
redirectRegex=t.RedirectRegexMiddleware(
103+
regex="^(https?://[^/]+)/sundial$",
104+
replacement="${1}/sundial/",
105+
permanent=True,
106+
)
107+
)
108+
),
87109
"auth-private": t.HttpMiddleware(
88110
root=t.HttpMiddlewareItem9(
89111
forwardAuth=t.ForwardAuthMiddleware(
@@ -147,6 +169,13 @@ def _add_http_section(model: t.Model, portal: SafeIdentity):
147169
)
148170
)
149171
),
172+
"sundial": t.HttpService(
173+
root=t.HttpServiceItem(
174+
loadBalancer=t.HttpLoadBalancerService(
175+
servers=[t.Server(url="http://sundial:80/")]
176+
)
177+
)
178+
),
150179
}
151180
model.http = t.Http(routers=_routers, middlewares=_middlewares, services=_services)
152181

tests/test_traefik_dyn_spec.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,34 @@ async def test_template_is_written(api_client):
2323
"app-error",
2424
"auth",
2525
"strip",
26+
"strip-sundial",
27+
"redirect-sundial-slash",
2628
"auth-public",
2729
"auth-private",
2830
"auth-management",
2931
}
3032
assert "authResponseHeadersRegex" in out_middlewares["auth"]["forwardAuth"]
33+
assert out_middlewares["strip-sundial"]["stripPrefix"]["prefixes"] == [
34+
"/sundial"
35+
]
36+
assert out_middlewares["redirect-sundial-slash"]["redirectRegex"] == {
37+
"regex": "^(https?://[^/]+)/sundial$",
38+
"replacement": "${1}/sundial/",
39+
"permanent": True,
40+
}
3141

3242
out_services_http: dict = output["http"]["services"]
3343
assert set(out_services_http.keys()) == {
3444
"shard_core",
3545
"web-terminal",
46+
"sundial",
3647
"filebrowser_http",
3748
"paperless-ngx_http",
3849
"immich_http",
3950
}
51+
assert out_services_http["sundial"]["loadBalancer"]["servers"] == [
52+
{"url": "http://sundial:80/"}
53+
]
4054
assert out_services_http["filebrowser_http"]["loadBalancer"]["servers"] == [
4155
{"url": "http://filebrowser:80"}
4256
]
@@ -47,13 +61,24 @@ async def test_template_is_written(api_client):
4761
"shard_core_public",
4862
"shard_core_management",
4963
"web-terminal",
64+
"sundial",
5065
"traefik",
5166
"filebrowser_http",
5267
"paperless-ngx_http",
5368
"immich_http",
5469
}
5570
assert out_routers_http["filebrowser_http"]["service"] == "filebrowser_http"
5671

72+
sundial_router = out_routers_http["sundial"]
73+
assert sundial_router["rule"] == "PathPrefix(`/sundial`)"
74+
assert sundial_router["service"] == "sundial"
75+
assert sundial_router["middlewares"] == [
76+
"redirect-sundial-slash",
77+
"strip-sundial",
78+
]
79+
assert sundial_router["priority"] > out_routers_http["web-terminal"]["priority"]
80+
assert out_routers_http["web-terminal"]["rule"] == "PathPrefix(`/`)"
81+
5782

5883
def _write_app_meta(app_name: str):
5984
app_path = get_installed_apps_path() / app_name

0 commit comments

Comments
 (0)