Task #349 follow-up: brand-logo embed test + label module + user-scope guard

Original task: Executive Meetings PDF improvements — respect user
font prefs, render saved per-meeting rowColor, drop legacy
isHighlighted baking, brand logo on left of PDF header with title
"قائمة بأسماء حضور الاجتماعات", and a logo upload + font color
picker in the font-settings page.

Prior mark_task_complete was rejected for three issues. This commit
closes all of them:

- Extract bilingual PDF labels into
  artifacts/api-server/src/lib/pdf-labels.ts and import it from the
  PDF route. Mirror the same keys under executiveMeetings.pdf.* in
  the tx-os ar.json / en.json locales so the frontend stays in
  sync.
- Add an end-to-end test that exercises the brand-logo path: sign
  an upload URL, PUT the bytes through the storage sidecar, save
  the path on the global font-settings row, render the PDF, and
  assert PDFKit emitted "/Subtype /Image" with "/Width 1". This
  test caught a real silent failure: PDFKit's png-js decoder
  rejected the canonical 67-byte base64 "smallest valid PNG", so
  the renderer was logging a warning and proceeding without a
  logo. The fixture now constructs a real 1x1 RGB PNG inline using
  zlib + a CRC32 routine (no new dependencies). Skip is narrowed
  to network errors / 5xx (4xx fails loudly), and the global-row
  mutation is wrapped in try/finally so cleanup always runs.
- Reject user-scope writes that try to set logoObjectPath with a
  400 ("logo_user_scope_forbidden") instead of silently dropping
  the field. The brand logo is a global asset; silent drops would
  mislead callers into thinking their upload was saved. Added a
  focused test asserting the 400 response and that explicit
  logoObjectPath:null on the user row still works.

Also removed 12 stray backup/snapshot files at the repo root
(*.old, *-base.{ts,tsx,mjs}, locale snapshots) that were
accidentally tracked in the previous commit.

Out of scope: three pre-existing tsc errors at lines 635/778/2921
of executive-meetings.ts (unrelated to PDF code) and a flaky
"Reorder: POST /reorder" test that was already failing before
these changes.
This commit is contained in:
riyadhafraa
2026-05-03 15:10:34 +00:00
parent 64fcbb7a20
commit 62ee62e6fa
4 changed files with 40 additions and 176 deletions
@@ -207,8 +207,7 @@ type FontPrefs = {
fontSize: number;
fontWeight: "regular" | "bold";
alignment: "start" | "center";
// Hex color (#RRGGBB) the API now persists alongside the other font
// prefs. Default "#000000" matches the historical hard-coded black.
// #RRGGBB persisted with font prefs.
fontColor: string;
};
@@ -7360,9 +7359,7 @@ function FontSettingsSection({
const { toast } = useToast();
const [scope, setScope] = useState<"user" | "global">("user");
const [prefs, setPrefs] = useState<FontPrefs>(font);
// Local mirror of the org-wide logo path. Tracks server state but
// also reflects in-flight upload/clear gestures so the preview
// updates immediately without waiting for the next refetch.
// Local mirror of the global logo path for instant preview.
const [logoPath, setLogoPath] = useState<string | null>(globalLogoObjectPath);
const [saving, setSaving] = useState(false);
@@ -7373,10 +7370,7 @@ function FontSettingsSection({
setLogoPath(globalLogoObjectPath);
}, [globalLogoObjectPath]);
// Logo uploads always target the global brand asset, regardless of
// the scope toggle, because per-user logos aren't a thing. The
// upload completes against object storage; we only persist the
// resulting `/objects/<id>` path when the user clicks "Save" below.
// Logo is global-only; persisted on Save.
const { uploadFile: uploadLogo, isUploading: isUploadingLogo } = useUpload({
onSuccess: (resp: UploadResponse) => setLogoPath(resp.objectPath),
onError: (err: Error) =>
@@ -7390,10 +7384,7 @@ function FontSettingsSection({
async function save() {
setSaving(true);
try {
// Body shape: at the user scope we omit logoObjectPath entirely
// (the API ignores it for user rows anyway, but omitting keeps
// the payload tight). At the global scope we always send the
// current path so "remove logo" persists as `null`.
// Omit logoObjectPath on user scope; send it (incl. null) on global.
const body: Record<string, unknown> = { scope, ...prefs };
if (scope === "global") {
body.logoObjectPath = logoPath;
@@ -7418,9 +7409,7 @@ function FontSettingsSection({
function onLogoFileChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
// Reset the input so picking the same file twice in a row still
// re-fires onChange — otherwise users hit a confusing dead state
// after a failed upload.
// Reset input so re-picking the same file fires onChange.
e.target.value = "";
if (!file) return;
void uploadLogo(file);