Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions client/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ server {
# Theme stylesheet plugins link as /index.css (see the plugin development
# guide). Exact match, so it wins over the static-asset regex without
# shadowing the hashed stylesheets under /assets/.
#
# Served from this image, not proxied to the backend. The backend builds with
# server/ as its Docker context, so it never contains client/src/index.css
# and answered every one of these requests with a 500. This is a client
# asset; it lives where the other client assets live.
location = /index.css {
proxy_pass http://${BACKEND_SERVICE}:${BACKEND_PORT}/index.css;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
try_files $uri =404;
add_header Cache-Control "no-cache";
}

# Cache static assets from build - but NOT from /Uploads/ or /uploads/
Expand Down
31 changes: 30 additions & 1 deletion client/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,35 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

const INDEX_CSS = fileURLToPath(new URL('./src/index.css', import.meta.url));

// Plugins run in iframes and link `/index.css` for the theme variables — the
// plugin development guide documents it. Vite bundles src/index.css into a
// hashed asset, so nothing answers that path; emit an unhashed copy at the site
// root for nginx to serve.
//
// Emitted from src/index.css rather than kept as a second file in public/, so
// there is one source and the copy cannot drift from it.
//
// Build only, deliberately. A plugin iframe is loaded from API_BASE_URL, so its
// `/index.css` resolves against the *backend* origin whenever that differs from
// the page's — which is exactly `npm run dev`. The dev server never sees the
// request, and the backend's own handler already answers it there by reading
// the file out of the source tree. In containers API_BASE_URL is empty, the
// iframe is same-origin, and the request lands here instead.
const emitPluginThemeStylesheet = () => ({
name: 'homeglow-plugin-theme-stylesheet',
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'index.css',
source: readFileSync(INDEX_CSS, 'utf8'),
});
},
});

const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const inPackage = (id, pkgName) => {
Expand All @@ -11,7 +40,7 @@ const inPackage = (id, pkgName) => {
};

export default defineConfig({
plugins: [react()],
plugins: [react(), emitPluginThemeStylesheet()],
build: {
rollupOptions: {
output: {
Expand Down
113 changes: 10 additions & 103 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,109 +677,16 @@ fastify.get('/index.css', async (request, reply) => {
} catch (error) {
console.error('Error serving index.css:', error);

// Fallback: serve minimal CSS for widgets
const fallbackCSS = `
:root {
--background: #f4f4f9;
--card-bg: rgba(255, 255, 255, 0.8);
--card-border: rgba(255, 255, 255, 0.2);
--text-color: #1a1a2e;
--text-color-rgb: 26, 26, 46;
--accent: #6e44ff;
--accent-rgb: 110, 68, 255;
--shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
--backdrop-blur: blur(10px);
--dynamic-text-size: 16px;
--dynamic-card-width: 300px;
--dynamic-card-padding: 20px;
--error-color: #ff4444;
--light-gradient-start: #00ddeb;
--light-gradient-end: #ff6b6b;
--dark-gradient-start: #2e2767;
--dark-gradient-end: #620808;
--light-button-gradient-start: #00ddeb;
--light-button-gradient-end: #ff6b6b;
--dark-button-gradient-start: #2e2767;
--dark-button-gradient-end: #620808;
--gradient: linear-gradient(45deg, var(--light-gradient-start), var(--light-gradient-end));
}

[data-theme="dark"] {
--background: #0a0a1a;
--card-bg: rgba(30, 30, 50, 0.7);
--card-border: rgba(100, 100, 150, 0.3);
--text-color: #a6a6d1;
--text-color-rgb: 166, 166, 209;
--accent: #00ddeb;
--accent-rgb: 0, 221, 235;
--shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
--gradient: linear-gradient(45deg, var(--dark-gradient-start), var(--dark-gradient-end));
}

html, body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--background);
color: var(--text-color);
transition: background 0.3s ease, color 0.3s ease;
touch-action: manipulation;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
font-size: var(--dynamic-text-size);
}

.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: var(--dynamic-card-padding);
backdrop-filter: var(--backdrop-blur);
box-shadow: var(--shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease;
width: 100%;
max-width: var(--dynamic-card-width);
touch-action: manipulation;
}

.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
}

h1, h2, h3, h4, h5, h6 {
font-weight: 700;
letter-spacing: 0.5px;
color: var(--text-color);
}

button {
background: linear-gradient(45deg, var(--light-button-gradient-start), var(--light-button-gradient-end));
color: var(--text-color);
border: none;
border-radius: 8px;
padding: 10px 20px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background 0.3s ease;
touch-action: manipulation;
}

[data-theme="dark"] button {
background: linear-gradient(45deg, var(--dark-button-gradient-start), var(--dark-button-gradient-end));
}

button:hover {
filter: brightness(1.1);
}
`;

console.log('Serving fallback CSS');
reply.header('Content-Type', 'text/css');
reply.header('Access-Control-Allow-Origin', '*');
return fallbackCSS;
// No substitute stylesheet. This used to answer with a hardcoded palette
// that defined *different* variable names than src/index.css and inverted
// the theme model — light :root with a dark override, where the real sheet
// is dark :root with a light override. Because the containers could never
// find the real file, that substitute was what every plugin actually
// received, and plugin authors wrote against its vocabulary. 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.
return reply.status(404).send({ error: 'index.css not found' });
}
});

Expand Down