Skip to content

fix(client): serve /index.css from the frontend instead of the backend - #175

Merged
jherforth merged 2 commits into
jherforth:mainfrom
mrramam:fix/serve-index-css-from-frontend
Sep 15, 2026
Merged

jherforth merged 2 commits into
jherforth:mainfrom
mrramam:fix/serve-index-css-from-frontend

Conversation

@mrramam

@mrramam mrramam commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #174.

Plugin iframes have never received the real theme stylesheet in a container
deployment. The backend serves /index.css by reading client/src/index.css off
disk, trying four candidate paths; server/Dockerfile builds with server/ as
its Docker context, so the image contains server/ and nothing else and none of
those paths can resolve. It works natively from a full checkout, which is how it
went unnoticed.

The request did not fail after the four ENOENTs the handler served a hardcoded
substitute, so plugins got a valid 200 with the wrong content, under different
variable names
(--text-color, --error-color where the real sheet has
--text, --error) and an inverted theme model.

Changes

Serve it from the frontend. Vite emits an unhashed index.css at the site
root from src/index.css, and nginx serves that file instead of proxying. It is
emitted rather than copied into public/, so there is one source and no second
file to drift.

Drop the substitute. A stylesheet that silently supplies different values
under different names is worse than none: plugins carry their own
var(--x, default) fallbacks and those only work if this path stays quiet. It
now 404s.

The backend handler stays, deliberately. A plugin iframe loads from
API_BASE_URL, so its root-relative /index.css resolves against the backend
origin whenever that differs from the page's which is exactly npm run dev.
There the file is present and served correctly. In containers API_BASE_URL is
empty, the iframe is same-origin, and the request reaches nginx.

Depends on jherforth/HomeGlowPlugins#13

This changes which variable names plugins receive, and the two stylesheets are
not subset and superset. chore-metrics.html uses var(--text-color, #333) — a
name only the substitute defined so merging this alone leaves it rendering dark
grey text on the dark --card-bg.

Landing the plugin PR first is safe; landing this one first leaves that
plugin unreadable in the interval. All 24 plugins were checked and
chore-metrics is the only one using a retired name.

Verified

On a container deployment, with the check that discriminates: front
GET /index.css returns the real 5174-byte sheet, and the backend origin
returns 404
. A 200 on the front door alone was true of the broken build too,
since the substitute answered it.

Known limit

Plugins now get the real palette but still the defaults, not a customized
theme. The app overrides roughly fifteen of these variables at runtime from user
settings, and interfaceColors lives only in localStorage, so no server-side
render can be correct. Matching the live theme would need the host to inject
computed values into the iframes a separate, larger change.

mrramam added 2 commits September 14, 2026 13:58
Plugin iframes link /index.css for the theme variables — the plugin development
guide documents it, and all 24 plugins in HomeGlowPlugins use it. In any
container deployment that request has always failed.

nginx proxied /index.css to the backend, and the backend answers it by reading
client/src/index.css off disk, trying four candidate paths. server/Dockerfile
builds with server/ as its context and does COPY . ., so the image contains
server/ and nothing else; none of those paths can resolve. Every request logged
three errors and returned a 500. It works when running natively from a full
checkout, which is how it survived.

The stylesheet is a client asset, so it is now served by the client. Vite emits
an unhashed copy at the site root next to the hashed bundles, and nginx serves
that file instead of proxying. The copy is emitted from src/index.css rather
than duplicated into public/, so there is one source and no second file to
drift.

The backend handler is left in place on purpose. A plugin iframe is loaded from
API_BASE_URL, so its root-relative /index.css resolves against the backend
origin whenever that differs from the page's — which is exactly `npm run dev`.
There the backend can read the file, and does. In containers API_BASE_URL is
empty, the iframe is same-origin, and the request now lands on nginx.

This finishes what jherforth#61 started: that fixed the routing so /index.css reached the
backend rather than the SPA fallback, but nothing behind it could answer.

Plugins still get the default palette rather than a customized theme — the app
overrides about fifteen of these variables at runtime from user settings, and
interfaceColors lives only in localStorage, so no server-side render could be
correct either. Matching the live theme needs the host to inject values into the
iframes and is a separate change.
…missing

The /index.css handler answered a missing file with a hardcoded stylesheet. That
substitute defined different variable names than client/src/index.css
(--text-color, --text-color-rgb, --error-color, where the real sheet has --text,
--text-secondary, --error) and inverted the theme model: light :root with a
[data-theme="dark"] override, where the real sheet is dark :root with a
[data-theme="light"] override.

Because the backend image cannot contain client/src/index.css, that substitute
was what every plugin actually received in every container deployment, for the
whole life of the feature. Plugin authors wrote against its vocabulary —
chore-metrics uses var(--text-color, #333), a name that does not exist in the
real stylesheet.

A stylesheet that silently supplies different values under different names is
worse than none. Plugins already carry their own var(--x, default) fallbacks and
those only work if this path stays quiet, so it now returns 404.

Paired with serving /index.css from the frontend image: in containers the request
no longer reaches this handler at all, and in native development the real file is
present and served. This path is now only reachable on a genuinely broken
install, where failing honestly is the point.
@mrramam mrramam changed the title Fix/serve index css from frontend fix(client): serve /index.css from the frontend instead of the backend Sep 15, 2026
@jherforth

Copy link
Copy Markdown
Owner

Reviewed critically and merging. The dependency is in place and I verified each structural claim rather than taking it on trust.

The diagnosis is correct, and provable from the build config

docker-image.yml builds the backend with context: ./server, and server/Dockerfile does COPY . .. So client/src/index.css cannot exist in that image under any of the four candidate paths — the failure is structural, not a path bug. And it works natively from a checkout, which is exactly why it went unnoticed.

The substitute is also the source of a confusion I ran into separately: it defined --text-color and --error-color, names the real sheet does not have, with an inverted theme model. That is why chore-metrics was written against var(--text-color, #333) and rendered dark grey on the dark --card-bg.

The ordering constraint is satisfied

HomeGlowPlugins#13 is merged, and chore-metrics.html on that repo's main now has zero --text-color references and uses var(--text. Safe to land this.

What I verified

The emitted stylesheet is real, not a copy that can drift. Built the client on this branch:

dist/index.css        5404 bytes, unhashed, at the dist root
identical to src/index.css:  yes
defines --text:              yes        defines --text-color:  no
hashed sheets still emitted: index-CCHwdNXJ.css, layout-Cb7KG80W.css

client/Dockerfile does COPY --from=builder /app/dist /usr/share/nginx/html, which is precisely where root + try_files $uri resolves it. And location = /index.css is an exact match, so it wins over the static-asset regex — nginx resolves exact matches before regex, so that comment is right.

Same-origin in containers holds. getApiUrl() returns '' outside development, and nginx proxies /api/, /uploads/, /widgets/ and /plugin-sdk/ to the backend. A plugin iframe at /widgets/x.html is therefore same-origin with the page and its /index.css reaches nginx, not the backend.

The dev path is not regressed — this was the one I most wanted to check, since breaking it would hit every developer. Backend on this branch:

GET /index.css -> 200 text/css
identical to src/index.css:  true
defines --text: true | --text-color: false | --error-color: false

Server 225/225, client 281/281, translation parity, CI green on both jobs.

One consequence worth stating plainly

For a split-origin deployment — VITE_REACT_APP_API_URL pointed at a different origin — the iframe stays cross-origin, the request lands on the backend, and plugins now get no stylesheet where they previously got the wrong one. That is the right trade and your reasoning for it is sound: plugins carry var(--x, default) fallbacks, and those only work when this path is quiet. Worth being explicit that it is a real behaviour change for that non-default configuration, not only for containers.

Also agreed on the known limit. Plugins getting the real palette but the default values is a genuine improvement over the wrong palette, and injecting the computed theme into iframes is rightly a separate change.

Small thing: the body says "the real 5174-byte sheet" — it measures 5404 now. The number is just stale, the check behind it is the right one.

@jherforth
jherforth merged commit 3e648e0 into jherforth:main Sep 15, 2026
2 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in HomeGlow Kanban Sep 15, 2026
@jherforth jherforth added this to the 1.9 milestone Sep 15, 2026
@mrramam
mrramam deleted the fix/serve-index-css-from-frontend branch September 15, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Plugins never receive the real theme stylesheet in a container; /index.css serves a substitute with different variable names

2 participants