mirror of
https://github.com/luxfi/work-board.git
synced 2026-07-27 06:45:00 +00:00
work-board: gate FIXTURE/RPC-error chrome out of prod, wrap bounty titles, per-brand favicon
Three prod UI fixes from the 4-res audit (work.lux.network + work.hanzo.network):
1. FIXTURE debug bar in prod. The LIVE/FIXTURE data-source pill (and the raw
RPC error string it surfaces, e.g. "Missing or invalid parameters") was
pinned bottom-left for every visitor and clipped past the mobile viewport.
Gate it to `vite dev` (import.meta.env.DEV) or an explicit VITE_SHOW_STATUS
flag, and cap the pill at calc(100vw-1.5rem) + overflow-hidden so it can
never overflow even in dev.
2. Bounty-card title truncation. Space-card names ("Ecosystem &
Partnerships", "Community & Social", "Content Creation") were ellipsis-
clipped at tablet/laptop. Drop `truncate`; let the title wrap.
3. Favicon. Replace the generic emerald green-check placeholder with each
brand's own square mark, injected per-build by the brand-html Vite plugin
as an inline data: URI (CSP-safe, no extra request). New purpose-built
16px-legible favicon.svg per brand (lux/hanzo/zoo/pars).
Board still reads $0 / 0 tasks because the work-market contracts are not yet
deployed on 96369/36963 (owner-gated) — untouched here.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
+8
-2
@@ -19,11 +19,17 @@ import { IconMenu, IconSearch } from './ui';
|
||||
import type { Task } from './types';
|
||||
import type { Workspace } from './chain';
|
||||
|
||||
// The LIVE/FIXTURE data-source pill (and the raw RPC error it surfaces) is a
|
||||
// developer diagnostic, never production chrome — a production visitor must never
|
||||
// see "FIXTURE" or an RPC error string. Gated to `vite dev`; force it on in a
|
||||
// prod-like build for debugging with VITE_SHOW_STATUS=true.
|
||||
const SHOW_STATUS = import.meta.env.DEV || import.meta.env.VITE_SHOW_STATUS === 'true';
|
||||
|
||||
function StatusPill({ source, updatedAt, error }: { source: Source | null; updatedAt: number | null; error: string | null }) {
|
||||
if (source === null) return null;
|
||||
const live = source === 'live';
|
||||
return (
|
||||
<div className="fixed bottom-3 left-3 z-40 flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-neutral-400 ring-1 ring-inset ring-white/10 backdrop-blur">
|
||||
<div className="fixed bottom-3 left-3 z-40 flex max-w-[calc(100vw-1.5rem)] items-center gap-1.5 overflow-hidden rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-neutral-400 ring-1 ring-inset ring-white/10 backdrop-blur">
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${live ? 'animate-pulse bg-emerald-400' : 'bg-amber-400'}`} />
|
||||
<span className={live ? 'text-emerald-300' : 'text-amber-300'}>{live ? 'LIVE' : 'FIXTURE'}</span>
|
||||
<span className="text-neutral-600">· chain {CHAIN_ID}</span>
|
||||
@@ -159,7 +165,7 @@ export function App() {
|
||||
{modalTask && ws && <TaskDetail task={modalTask} ws={ws} />}
|
||||
{paletteOpen && <CommandPalette tasks={tasks} onClose={() => setPaletteOpen(false)} />}
|
||||
<ConnectSheetHost />
|
||||
<StatusPill source={source} updatedAt={updatedAt} error={error} />
|
||||
{SHOW_STATUS && <StatusPill source={source} updatedAt={updatedAt} error={error} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#000000"/>
|
||||
<g transform="translate(7 7) scale(0.269)" fill="#FFFFFF">
|
||||
<path d="M22.21 67V44.6369H0V67H22.21Z"/>
|
||||
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z"/>
|
||||
<path d="M22.21 0H0V22.3184H22.21V0Z"/>
|
||||
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z"/>
|
||||
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#000000"/>
|
||||
<path d="M11 8h4.2v11.8H23V24H11z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 177 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#1C3879"/>
|
||||
<path transform="translate(16 16) scale(0.16)" fill="#D4AF37" d="M0,-70 L22,-42 L70,-28 L42,0 L70,28 L22,42 L0,70 L-22,42 L-70,28 L-42,0 L-70,-28 L-22,-42 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="7" fill="#6c5efb"/>
|
||||
<path d="M9 8H23V12L16 20H23V24H9V20L16 12H9Z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 189 B |
@@ -113,7 +113,7 @@ function SpaceCard({ space, rollup }: { space: Space; rollup: SpaceRollup }) {
|
||||
{space.emoji}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-semibold text-neutral-100">{space.name}</div>
|
||||
<div className="text-sm font-semibold leading-snug text-neutral-100">{space.name}</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs text-neutral-500">
|
||||
<span className="tabular-nums">{plural(rollup.open, 'open task')}</span>
|
||||
<span aria-hidden>·</span>
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_RPC_URL?: string;
|
||||
readonly VITE_BRAND?: string;
|
||||
readonly VITE_SHOW_STATUS?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
+14
-1
@@ -1,7 +1,9 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { defineConfig, type Plugin, type IndexHtmlTransformContext } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { resolveBrand, brandTitle } from './src/brands';
|
||||
import { resolveBrand, resolveBrandKey, brandTitle } from './src/brands';
|
||||
|
||||
// Dev proxy target. Defaults to the kubectl port-forward; override to read a
|
||||
// remote node, e.g. `RPC_TARGET=https://api.pars.network npm run dev`.
|
||||
@@ -17,6 +19,16 @@ const RPC_TARGET = process.env.RPC_TARGET || 'http://127.0.0.1:9631';
|
||||
// CSP can never drift from the endpoint the board calls.
|
||||
function brandHtml(): Plugin {
|
||||
const brand = resolveBrand(process.env.VITE_BRAND);
|
||||
const brandKey = resolveBrandKey(process.env.VITE_BRAND);
|
||||
// Favicon = the brand's own square mark, inlined as a data: URI so it needs no
|
||||
// separate request and satisfies the strict CSP (img-src 'self' data:). Kept as
|
||||
// a purpose-built 16px-legible favicon.svg per brand (never the generic
|
||||
// placeholder, never cross-brand).
|
||||
const faviconSvg = readFileSync(
|
||||
fileURLToPath(new URL(`./src/assets/brands/${brandKey}/favicon.svg`, import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
const faviconHref = `data:image/svg+xml,${encodeURIComponent(faviconSvg)}`;
|
||||
const rpcOrigin = new URL(process.env.VITE_RPC_URL || brand.rpcUrl).origin;
|
||||
// The board fetches the brand's Hanzo IAM tenant (OIDC token/userinfo + web3
|
||||
// SIWE) — its origin must be in connect-src alongside the RPC host. The
|
||||
@@ -38,6 +50,7 @@ function brandHtml(): Plugin {
|
||||
const policy = ctx.server ? dev : prod;
|
||||
return html
|
||||
.replace(/<title>[^<]*<\/title>/, `<title>${brandTitle(brand)}</title>`)
|
||||
.replace(/<link rel="icon"[^>]*>/, `<link rel="icon" type="image/svg+xml" href="${faviconHref}" />`)
|
||||
.replace(
|
||||
'</title>',
|
||||
`</title>\n <meta http-equiv="Content-Security-Policy" content="${policy}" />`,
|
||||
|
||||
Reference in New Issue
Block a user