From d35f93e1ef5d680c7208e9458e873c82c05eae36 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 16 Jun 2026 22:29:07 -0700 Subject: [PATCH] FN-6512: refresh mobile PWA icons Refresh the dashboard launcher artwork so installed mobile PWAs use the Fusion brand mark. - Generate 192px and 512px dashboard PWA icons from the canonical logo in the desktop icon generator. - Bump the service-worker cache name so installed PWAs refresh cached icon assets. - Strengthen PWA tests to validate icon wiring, PNG dimensions, opacity, and non-blank brand content. - Document the PWA icon regeneration workflow and add a patch changeset. Files changed: .changeset/fn-6512-dashboard-pwa-icons.md | 5 + docs/dashboard-guide.md | 4 + packages/dashboard/app/__tests__/pwa.test.ts | 178 ++++++++++++++++++++--- packages/dashboard/app/public/icons/icon-192.png | Bin 5466 -> 4634 bytes packages/dashboard/app/public/icons/icon-512.png | Bin 17687 -> 14826 bytes packages/dashboard/app/public/sw.js | 2 +- packages/desktop/scripts/generate-icons.ts | 75 +++++++--- 7 files changed, 227 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-6512 Fusion-Task-Lineage: a7a46e0d-fa7d-4ca1-b49c-f083aed92b17 --- .changeset/fn-6512-dashboard-pwa-icons.md | 5 + docs/dashboard-guide.md | 4 + packages/dashboard/app/__tests__/pwa.test.ts | 172 ++++++++++++++++-- .../dashboard/app/public/icons/icon-192.png | Bin 5466 -> 4634 bytes .../dashboard/app/public/icons/icon-512.png | Bin 17687 -> 14826 bytes packages/dashboard/app/public/sw.js | 2 +- packages/desktop/scripts/generate-icons.ts | 75 ++++++-- 7 files changed, 224 insertions(+), 34 deletions(-) create mode 100644 .changeset/fn-6512-dashboard-pwa-icons.md diff --git a/.changeset/fn-6512-dashboard-pwa-icons.md b/.changeset/fn-6512-dashboard-pwa-icons.md new file mode 100644 index 0000000000..61a689ee6a --- /dev/null +++ b/.changeset/fn-6512-dashboard-pwa-icons.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Refresh dashboard mobile and PWA home-screen icons from the canonical Fusion logo and bump the service-worker cache for installed app updates. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index feb04e561a..ab2ee06873 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -8,6 +8,10 @@ The Fusion dashboard is the main control plane for tasks, agents, missions, sett When Fusion detects a newer `@runfusion/fusion` release, the Settings modal footer shows the available version with **Learn more** and **Update now** actions. **Update now** installs the latest global package with npm; after it succeeds, restart Fusion to apply the new version because the already-running dashboard server is unchanged until restart. +## Mobile/PWA app icons + +The installed mobile/PWA home-screen icons are generated from `packages/dashboard/app/public/logo.svg` by the desktop icon generator. When the Fusion brand mark changes, run `pnpm --filter @fusion/desktop generate:icons` so `packages/dashboard/app/public/icons/icon-192.png` and `packages/dashboard/app/public/icons/icon-512.png` stay aligned with the canonical logo. Also bump `CACHE_NAME` in `packages/dashboard/app/public/sw.js` whenever those icon assets change so installed PWAs refresh the cached launcher images. + ## Browser Navigation The dashboard now handles browser back navigation consistently on desktop and mobile. diff --git a/packages/dashboard/app/__tests__/pwa.test.ts b/packages/dashboard/app/__tests__/pwa.test.ts index 522dc5fa5d..45fe05fac2 100644 --- a/packages/dashboard/app/__tests__/pwa.test.ts +++ b/packages/dashboard/app/__tests__/pwa.test.ts @@ -1,8 +1,16 @@ -import { readFileSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { resolve } from "node:path"; +import { inflateSync } from "node:zlib"; import { describe, expect, it } from "vitest"; import { loadAllAppCss } from "../test/cssFixture"; +type DecodedPng = { + width: number; + height: number; + colorType: number; + pixels: Buffer; +}; + function getStandaloneDisplayModeBlock(css: string): string { const match = /@media\s*\(\s*display-mode:\s*standalone\s*\)\s*\{/.exec(css); expect(match).toBeTruthy(); @@ -21,6 +29,89 @@ function getStandaloneDisplayModeBlock(css: string): string { return css.slice(start, i); } +function decodeRgbaPng(filePath: string): DecodedPng { + const buffer = readFileSync(filePath); + const signature = buffer.subarray(0, 8).toString("hex"); + expect(signature).toBe("89504e470d0a1a0a"); + + let offset = 8; + let width = 0; + let height = 0; + let bitDepth = 0; + let colorType = 0; + const idatChunks: Buffer[] = []; + + while (offset < buffer.length) { + const length = buffer.readUInt32BE(offset); + const type = buffer.subarray(offset + 4, offset + 8).toString("ascii"); + const dataStart = offset + 8; + const dataEnd = dataStart + length; + const data = buffer.subarray(dataStart, dataEnd); + + if (type === "IHDR") { + width = data.readUInt32BE(0); + height = data.readUInt32BE(4); + bitDepth = data.readUInt8(8); + colorType = data.readUInt8(9); + } else if (type === "IDAT") { + idatChunks.push(data); + } else if (type === "IEND") { + break; + } + + offset = dataEnd + 4; + } + + expect(bitDepth).toBe(8); + expect(colorType).toBe(6); + + const bytesPerPixel = 4; + const stride = width * bytesPerPixel; + const inflated = inflateSync(Buffer.concat(idatChunks)); + const pixels = Buffer.alloc(width * height * bytesPerPixel); + let inputOffset = 0; + let outputOffset = 0; + + for (let y = 0; y < height; y += 1) { + const filter = inflated[inputOffset]; + inputOffset += 1; + + for (let x = 0; x < stride; x += 1) { + const raw = inflated[inputOffset + x]; + const left = x >= bytesPerPixel ? pixels[outputOffset + x - bytesPerPixel] : 0; + const up = y > 0 ? pixels[outputOffset + x - stride] : 0; + const upLeft = y > 0 && x >= bytesPerPixel ? pixels[outputOffset + x - stride - bytesPerPixel] : 0; + let value: number; + + if (filter === 0) { + value = raw; + } else if (filter === 1) { + value = raw + left; + } else if (filter === 2) { + value = raw + up; + } else if (filter === 3) { + value = raw + Math.floor((left + up) / 2); + } else if (filter === 4) { + const predictor = left + up - upLeft; + const pa = Math.abs(predictor - left); + const pb = Math.abs(predictor - up); + const pc = Math.abs(predictor - upLeft); + const paeth = pa <= pb && pa <= pc ? left : pb <= pc ? up : upLeft; + value = raw + paeth; + } else { + throw new Error(`Unsupported PNG filter ${filter} in ${filePath}`); + } + + pixels[outputOffset + x] = value & 0xff; + } + + inputOffset += stride; + outputOffset += stride; + } + + return { width, height, colorType, pixels }; +} + describe("PWA configuration", () => { it("manifest defines required PWA fields and icon sizes", () => { const manifestPath = resolve(__dirname, "../public/manifest.json"); @@ -29,7 +120,7 @@ describe("PWA configuration", () => { short_name?: string; start_url?: string; display?: string; - icons?: Array<{ sizes?: string }>; + icons?: Array<{ src?: string; sizes?: string; type?: string; purpose?: string }>; }; expect(manifest.name).toBe("Fusion"); @@ -37,8 +128,18 @@ describe("PWA configuration", () => { expect(manifest.start_url).toBe("/"); expect(manifest.display).toBe("standalone"); expect(Array.isArray(manifest.icons)).toBe(true); - expect(manifest.icons?.some((icon) => icon.sizes?.includes("192"))).toBe(true); - expect(manifest.icons?.some((icon) => icon.sizes?.includes("512"))).toBe(true); + expect(manifest.icons).toContainEqual({ + src: "/icons/icon-192.png", + sizes: "192x192", + type: "image/png", + purpose: "any", + }); + expect(manifest.icons).toContainEqual({ + src: "/icons/icon-512.png", + sizes: "512x512", + type: "image/png", + purpose: "any", + }); }); it("index.html includes required PWA meta tags", () => { @@ -93,7 +194,7 @@ describe("PWA configuration", () => { expect(swSource).toContain('addEventListener("install"'); expect(swSource).toContain('addEventListener("fetch"'); expect(swSource).toContain('addEventListener("activate"'); - expect(swSource).toMatch(/fusion-cache-v\d+/); + expect(swSource).toContain('const CACHE_NAME = "fusion-cache-v4";'); }); it("service worker bypasses SSE requests instead of trying to cache them", () => { @@ -162,21 +263,60 @@ describe("PWA configuration", () => { expect(logoSvg).not.toContain("r=\"20\""); }); - it("PWA icon files exist with correct sizes", async () => { - const fs = await import("node:fs"); + it("PWA icon files exist, decode to expected sizes, and are opaque non-blank PNGs", () => { + const icons = [ + { path: resolve(__dirname, "../public/icons/icon-192.png"), size: 192 }, + { path: resolve(__dirname, "../public/icons/icon-512.png"), size: 512 }, + ]; - const icon192Path = resolve(__dirname, "../public/icons/icon-192.png"); - const icon512Path = resolve(__dirname, "../public/icons/icon-512.png"); + for (const icon of icons) { + expect(existsSync(icon.path)).toBe(true); + expect(statSync(icon.path).size).toBeGreaterThan(icon.size * 12); - expect(fs.existsSync(icon192Path)).toBe(true); - expect(fs.existsSync(icon512Path)).toBe(true); + const png = decodeRgbaPng(icon.path); + expect(png.width).toBe(icon.size); + expect(png.height).toBe(icon.size); + expect(png.colorType).toBe(6); - // Verify PNG files have reasonable size (not empty) - const stats192 = fs.statSync(icon192Path); - const stats512 = fs.statSync(icon512Path); + let opaquePixels = 0; + let transparentPixels = 0; + let brandMarkPixels = 0; + const brandBackground = [0x1a, 0x1a, 0x2e]; - expect(stats192.size).toBeGreaterThan(100); - expect(stats512.size).toBeGreaterThan(100); + for (let index = 0; index < png.pixels.length; index += 4) { + const alpha = png.pixels[index + 3]; + if (alpha === 255) opaquePixels += 1; + else transparentPixels += 1; + + const colorDistance = + Math.abs(png.pixels[index] - brandBackground[0]) + + Math.abs(png.pixels[index + 1] - brandBackground[1]) + + Math.abs(png.pixels[index + 2] - brandBackground[2]); + if (colorDistance > 8) brandMarkPixels += 1; + } + + expect(transparentPixels).toBe(0); + expect(opaquePixels).toBe(icon.size * icon.size); + expect(brandMarkPixels).toBeGreaterThan(icon.size * icon.size * 0.1); + } + }); + + it("wires the same PWA icons through manifest, apple touch, and service-worker precache", () => { + const manifest = JSON.parse(readFileSync(resolve(__dirname, "../public/manifest.json"), "utf8")) as { + icons?: Array<{ src?: string; sizes?: string; purpose?: string }>; + }; + const indexHtml = readFileSync(resolve(__dirname, "../index.html"), "utf8"); + const swSource = readFileSync(resolve(__dirname, "../public/sw.js"), "utf8"); + const iconSources = ["/icons/icon-192.png", "/icons/icon-512.png"]; + + for (const iconSource of iconSources) { + expect(manifest.icons?.some((icon) => icon.src === iconSource && icon.purpose === "any")).toBe(true); + expect(swSource).toContain(`"${iconSource}"`); + } + + expect(indexHtml).toContain(''); + expect(indexHtml).toContain(''); + expect(swSource).toContain('const CACHE_NAME = "fusion-cache-v4";'); }); }); }); diff --git a/packages/dashboard/app/public/icons/icon-192.png b/packages/dashboard/app/public/icons/icon-192.png index 0790c874195e953033d0a377331706a89d50d879..7233652e1eae06211f89fbc69f23c95aaea9b8d0 100644 GIT binary patch literal 4634 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 literal 5466 zcma)=1y>V}!-Y2y7%)n@q(eZuyJUbMC0&k`ZYc#u$LNsm7##vq(jhG!5+jukl^omq z`+tM?o^$X03eS0Bb>2V-@oDh^005!7nu^|k-}k?QaQ<7pbr;fRu*EGT2zo_UY4+gncJy^V~0;Qfp)kZJn3UF zPb%`?h* z2|YIFx2grd_u-iefLLbx?e7Bg+}Vq4LL`HwWq9W=zFg8GTV*ZDwy4_tUEcp%{!>0a z1N8Y74hbd+U50{Be0##2y--yb0&v*!+0MXp!)1cayck%NHa=r}Q9#8I0qnWK9k|(s zeIt0jQUN^MnYvAE{^#^5hsrgs2%?Q&WK|&c;|7&#)rWNnK6&PVl#DzV`RNp{!+dT? z^t4Fx36g(;=y#BeJa2gPwwwPCVx+*>6GzdKAseALU;>f$@^Tt|Jc+1jzIOThKo*zHEypCAK!%|iHuz$#O;)RO;Q*RUz!dO7JeHzP*O}WzcxUqBY zYT@>HJ%0RrWuc7}UAy#pff&P&CX)G1)PvhY)#ISEuF1cHrf38(i34>E zsizD4bp*PIEjGT#mH~;2rBk^Ld}n>< zp&X1|;0ep_?m&TnTgF!tgfE*@G@}SU5VrTEg+({f)@K>VFPV=?XOU(ly9)n(bcbqjm!K zGmR6uWn{&u`IG@n$;X5x;J`&i4{Ql+L>A}!~ zAxv3zR8Yz0uRYR{FKKMI7j99sa>HNy9vu!QBf1<)6JjWg(h)R>dZ7fsz^U8LXt0-m zPOLqBQYHdrD1dK=$}k9f!`Hdm&h*TzBqDlfJgYDrdAsKmHD+eJ$RE5`F)JODN6YJN z3=M7watFJccW6;5WJst3sGyKyV_Gs?pBiWLVGzU^A;yM>Y^4Y^*g$x#Avfg0Z#v7I z|7Fw#4OWXt#7%fil~m{VJ)BPQ)HJhG?K3y}7pnSj1-)Em@!K{q=*D$>==(mv@_lXJ z+#S^5^y%_O5@QdYXC9l{A+4NB#%m9w?Zw*tJ61K{HfC?ShKeez=C4L8VlF4hjcJpW(X%Yf(D=xT z+Aubi89}-irQuyU85b^x{-`vQ9A*q_ncS}{OfQOGz}SGz8RXEr$IDj&-5 zxCPDT6q0`%Uh0i}bcl+%W3-vk2BY>Pe?IZwGEweU4si%!ork*mMi~Wp79g_I;YlrXe=Ds{;aizW_ZfzRs62}3S>=`5pn*x z$I;^6lRBh8JHDO4 z=xZdnbjtRMJI5uomkT!8R*s_U3Dq`E4xigKT8>VC=Xe8zfpEr5Co91p-0TPiKw z`6CGTnP7fukam8GDK8j#6^;#^!>3eHb4(z}yOd-YVHfEmg~c&DjS6*&%OrAFe?CJ( zKGxj7D3~8zOTr>WnGBV30M5Az0?>3mF;3Yv!mDyGyK(<I40^g`TH#QeUu) zwMm{DKn}P+cf@bW%mIe*o0o4T4C=@${Z+O$pvI|sR%DC2LM($7Z?sT@Iit<|@Xwysq>jQP$5wo~-)19z+F>Ri!@bRQ*StAn-eaE!vU; zp_eC6S4;yuQOMV|Az@gQd=&=YR;w@fpZXfV7sZjzf879{xh=0~S^NMZA?@>BFY(_H zN`bRn{X-B-JUj@i*tn|7v`DX|O6?vJ;g7{~hZn82a4+2e$x~Nc9=`lN`OfbW&9B3)X&tC9*wSM~?Eu!95=ci0 zDT^jzHCQdtDjf;MZx~;Fc(&jlL!QQ?n!D>Err&WBjpw$z5~+=+D6MW-0E+jc{S_L_ z!QW^i*oC}Y_^duqeQ%QG$)c&V$EL@N92RW+Qcr#Al!zhIqOc@4ZIbl|beA3mLqxY_ z`&KA$h%LT-!4>#vX71vOHqZ@a%v&iPBEBvi3uZLP@Nf%>0i$+VfW5~dIs8GA<6CVy z>Cs$gfVzXuYjP@?jJ=95N&{v?N7g6u*AFNdil*}Isw#s6&Re_2qYCqCdjh=R0EjIz z8p`(eSQGL|xoL4kMT&~giE9Rp@BL&USs{i5RpkWH&8LVu|4YICDf)F(0~*yU|Bsz?Du)xct7pKUaKNmS-NM z9v?1~4!-maXOuOfM~=!!sdLy-&_Y@niZ+J&Rn+g(?Y}ao@Z!RmmR{@CtRA12ZAu~{ zg9xpx8P(be-e>73P)GaQ1m-!M6T5z zJ0FoBWxP)X({m@HV8B$-#XC z!gY)xXaygAniu)$t$~SBnJoWg-X8$ztNn!rm~fv+s*v;$7@I@WAH7C9Gs$wwM%2y~ zrI6$y-BQG?{*-wvI!XrfE0PrL#*%B>%St|9e9B{YY3-*seysM7Bl*0@;Kpjd@`37B<=V@Flyp{lF=jXCSNn$ zDx7-4co}H8p6&!>3QDt}(IaFZ@yIXWaY@e6Gk_1(=8mnnR(0?2y zkbdi~npOk!x%_CvEth{|SFh60PN2GQc*mF5-@{W&g3`}a?_4jssR4gAV8Z%?!lU=E z(Nle4=SO0yx#HBtb3(0E#_mFEYHPz0XU?IY?_41}Lp6|TaySmySBYP(c z%9_eeJaJg8&qKltkjo_FI1V!0v93;j`*fzY^vP}NeEKh^Wc@MH5q3CUoyzH>3a`;j ze~j?+gpW+2hhtD&q`1iBoU-Zpr@YwKQ=m>}*Z|l@4+T+!)CC&Te1!hGccgxYJ0mZy zID=uI)CIQ@qQb}X=@5oX+Zc@O*q}z*{fAdwFIhY6|~DwxlFUv zqWA!&vk%(EIxX*)F1%Q=1xwb-`iZ4y96Crh3kLKC-`aEQ55X!juF{TYiU-W$U8JFy zf3~vnJ=~ReU6FkTv4feZj^K#+>@r<43GQPQ3V*e!D{{D^TGkZ~R4SJK3rNUC!_^A> zt|*=$LpVdM?QC)x0gJ0!Ou4`BAAa27kW9ZXCnBRgs;?64Ntd{_!0eh=+2H2(6WjJY zI8DBqWL~x;7CfOQB$3G;%4U~Sl-d{YXm4C^oHwvpmPuNtqCxU)f@O1>L%;OjE#;8E zz`GdqHFuD{ZQ`olew0_V#;FdUTy`!~DHLzYhmJ5&k~&RU8_}97?GWcK4(fQ&FF$)u z?(VLWp(Dh6M8cIIafL@)uvO^b+~hp=MPe8vNU$mChNSspnQ|^6C52l&H|QB)}NY2daSwIfb3Rc!mP@fUI|e-53g~O-6^= zd9UU|#9&~ub4phXs3u-&nY8qVaqLIKC`F^pf6oYC`yn(1Dw&j+AXQMX#{ayGNtCm;&ura(4;ruDynuuAtKELID>Q`C{%Q`rf1|45K6QJS zE@d+@Kjt5->8BLLKNw7raz)d593xa^ViG~;E9>a4uXnMTc!ot8f5Sg6Omki18~jhN z0Cv+WCv6G1-`I{tD|k~~z<6J5Df65P)RmZh+{Da)ANX7aDeO4trnT~IYssKb(Kicz0DE*+0-RK3q!uaC)g94$ zb<-5m zCq=>%U+neCX)b1_nx9nYxOgk%TnXeibIIC`0js>o%O?{K-2N&SFD!j7?ycVRcBkHd zeXKW1eOyk3%GyvK=zo_N(&H66MJZ*uZt{Lf&tjDbVgO(v-&C$zfR z&}FEswT-xieEk}hGON}sJo~)NBvq|?-nu-KHgb<5#dmEW&})*>KG`%}kyiz7+9Wo^ z_**XMrEa}=g_Pt^Ru%-A#1$dgDPjBR&+$3>R?8Cx%RNEJn~~K8ks#rd`BA0kdP~kt z%`ZGUO@$|whCO`hrEKWW-x1mhN+D;=!oscb4}v`{LSNZ{%>UH2IwyHk24YY9UdZ@d z(ZGqdD8BdQzIioJf207$E>~j0igJFDj?&-0e=f45jZNkm@YRcnHCZE*i)l+p+CFwr z@FM$$wNx~4<6~8`)=87hN1W?D-@D{Tpg+sv8siP)2My+(_w2zc`0x7j$ zk>k&<`^e4R`Qd4o0&(;Zi?NWV`#5=Jj>EoO<$ZE|FGyby98hFsy1xY8^o9=n<^OUK zBrpduV~T%!HDPd`w^JOX$KL^J4Y<^q0=?E%3*TSWb=TZY`aFzqh5wrE7Sp$ngVQ!pK|2KWJDp zlDxS8#})PPShaFFj?F8kYo$F^q6GsS`M>2_Hjq-M{(H(L0;0*^ZPc6gOV^4xEG9-M z(qr<9{=w{1w4SU~_K7Brw|=*f=!V9N=SH{^)^Cx6ys@N(rh94gEZhjGvlY|84X`ib zF17<4^iLp4H)Pq&F#2RJwrlUIYQDp%*Ekh=nH7o1g;Hd+#6+ItU1% z2c$PcQ6MBFd-L0!@1LFdcE8>IZ^sEZ!_9fmJ@=gFp7OjOo@=R4US+%rfj}tLRG;cX zAjIIm#E>hO!LPsmzs|uga(7h|F9_t)7U4rw7pUtDf!u|tJyq2ApW9zV!;GdrpBlPQKjjt{@v?KVrQc$IYarEolbr_=5Xv~DWy^Jt?!zlBSkVOfi7 zK^aBvH=En3$89KQop_Fy=&Y9Z5PvzABu8N>ty|I&v_(%IvmavxWgvSfk@9U$NK0 zYDIX1UO$SXBNH2U)G~<5GH)^09BrS#HV73EzD@DwZGOp2t)Zy##;9Cj@$@4XxiA_2 zPVJ;tiz_Y{pV4pG+WskvbHeJc#`bS861D~-!JI9m^E#hjH)ua1nrg(@O$B51#z-;a z39y!!uoxx7Q@>$J77=B0KJb8Lrx%}^c&lD!qhDIpY^N4`EwdvpV{~BZLhvnYCUF2C zHH%qK9SJzr>r`hqO`M$2#O(fyUm1Pd@|v|mw4UeOW$1=m2xgFX{i-J2p!X^x;(&zi ze=8szhR)qCVb%Y9;XppujJ&ac(>``pKZ!B>wN~m@t_V)JQuS5wp()X`f+I1{J5ehpu`QhdpV@;Y$nw0Lb zmpCAuq?^aw`I#!Cc~`VgKa5?;V3E_g%XCGL3E`OmA~ZComqW5Je95->U%w-99#ZZm zeKNU5d52l=_l7E35UOWgx8FM6MigZ*X&3!Tw7PLESgV|Y(Ig?+suz3MI~IdRNB| zs9_v?;fvN_^1Nlemp0)9bV&BOR)_6p10miWzNUlOL$A^NK%Ec~-;@Vn-pdu~gvp%n^qo zS=_BgwBzj@nZ&xQ)R4PrOxU$dKA-cIw;`7%B8Ax6W9lfVA=V3z^$e!}-U-@d{o|kE z@%yXC=}__ORoh~hUhQEHo`y}s0%PBKG=GWWnK;1ahJCtmqVCAs;p2$O!Ve4j;e zqPG_GuVVCFfd2>tFZF2>iO zzWYwcnAPV) zlhC(MfVq@wT97k3{B863XHgXy6U_KuTY3lIc&$P@Oq%_H*WX?b`yu-2gMpJN`tdPR zzy3GT)GI90I&!DsoG1XnZ1{DBq0|F--IEVvyE>XJR)>AN7+ei+ZpzCX<+i8+c(&}Mw%}y-NyJTx`&J|3I<;xzx^II z`Vqv0GmVnsV%V1~ikhM-KCyL>ZBu)GPVq)*LkI;tbu8!H%f9e?TSefJygAGDx%=aiWTS@W`yPdTpkZ-c?FL2e)6a-OeVZc+aD$3&VoUDo}0%~a2f&N7S! zqc84*HZd>|Mzh3tZP3=jH5iqlo%D(2K77?k#7|dWePbfFPdPu7`uGYpq%p2mIoa0S z4I?rCvX&u*GMH|9@Lh` zi~JBCh#kHI_Og)aWQLR}Z~S_>9%so(dny7ouTgI3wQ91rQg}Shyg|tUaZg$6ygDUy zR&{)-L&sam@{*`SW4su*=MxWJo)C^Yu{6s;%_6M6S%0N8#C+ISWfBhlfueriJ{^L%OCTQgfuNfEv~4q@;` z!IFNS+ru=#zf@iZHR(*^IaQV*`6q!-&3TSV+Q=(CVw3q>#?lQQ#EwMkB$Wf-eJti} z=4BI>0}!-0ZwKZOXT9%8l&b|B3hR)mu>Kj=kv`Ip#vTt*AT4RmnVc&f)VV*q)_3{- zOw-Nj1&*ys_R4BQTiuM9!7xg~bA`H=VQaNDZtm~2CwVC2cKo+%#V11cp4`q6jX)ET zL*VEU`KmdO3aJuFS+_q*)*;KaZA1pUEWQhwp<9kb8)O_1`c+Aot*B1TI3t2bQj|yZ zf$Qtpm5(>bjCzyw?ti|-D&_Do=2NE%=d_UgU82sdkLOZF%ue0qbg|ku`G+aZ6`h_x z+^&sD>e_g7O%>vfW|JQ$Ee`4Pifal;t_V_yR9X{~bWg7AXI`DVD9TNE#k1VI0KBmK zkCU%@q&&|$pEx$1t}z|0w%jAtr+;nj7eNZSW6EmM9M1LKlA+$!a(l~Va&jdvW-s+G zuUBcrZV;101eRL$oBvv>EPNrJ&;FLjBimF||7WA%P8D1|1sg8~cHW*OgZU!bep-P` zKWvkamI{+ZR!2G%9<86s*p#gg&-G=6l60aHE|oCr43xTesjZ%>z0g`lH@Ir4HrkC8 zNR+?OA|zZL38+1QGBfS7g1NyI$3#|Y&P&#qjn48eFW#!gRP}fE&bn^fcCs*2L;TH`rrz!C8@VlPy?Rc} zVKw2ek=^6^h~cMKXcIw=ck?N}pRo(m8Ec>50k1ZQRavUZ-X-nTx$1rKfLd!`Cx^)1 zmo8e`y6J}eF25~!gT?5FIV5#9XVQC!@5Eq5OyaT%_mnUlP~xiNcfF2d(!dD$%$QJ} z*8CPNpCw#XSq|}zpPaU$08m9~yP9?WbPjepB#XY38?W0iC>$F>h&`!$M4|Kf5<&Cr zlrRkLnFY-HRvvxIXN=cdHga;WC&UoMp9kgDWi#4CWq6@D$te-4PCM24-IZybzA7q% zp)N@mlWyI-`@B_erPzormQCEQJfuG6Zi*QJVE zqt&+j2)AI}$T#0btN_~3NKHCxIF93}3J=I)*c1huaDTw!^|^PyB5z-kIhU_3kn1!E zOcw;O*}Jy4UGKt`kdtmKog)QNrcSYUsc!LGTlEh{6~8~Lj>ehWY&Q&VnD~nAMU8zP zX#E7~q*TFWoV3~xB@$a)gbtOBmFgt_W^ub@X?m<2DDjuE?a2=|ukkcTFR-%#_O z5&b#yeP@^0N%8&61~uEi?(ZDQi~L6F{R|vV5x1;8(AxM!0?`uZMDd!%Q72cNYkNJR z@q%3Am#u$J>K5|aTLvYlCtx;hIvbTUqs!|LF5rzs5=dNN^OdpM^(mP3ydJNZY=ji# ztxOz~mD~;^{wNAoyBjP<9vSz;veXag@eG$WUn>8{?@VkYI5hrKpnL5e9;s8tjh1!q zrKh2gi6aBadH+HJq?2cD=JQkw&d$O8@rrniyiiQqFWM>TK{h#n0@GEA<^jm( zhv;ClQDpRi){O<8t0QT*da8NT1|2TVzIR4fA>&+!CCMRsY>|&k%dc}~rsqvVR;f6s zCPz#rIyq$LT5!PnGJAeZ}+(4=oaG1CXiAgxno zlb_l=7*G+qC5R*P(k6xsN#g8$O+q&0Yc9CpEcB0+=Qo5a`p!QK7XGaK`vu2Ro!YrG z2T81HZn~@rvCg04@2KL23^+kCW91|Y9}xN>_LJBo1t}-3gs=) z9%KS9nP>S-j=DX#e|_L=OTlJWD~=5YDxC~JGH2p|Tp6E!p4UHN3*#rlX;D01CjOc@ zVSwJri4D|De$docjr7sg&vs(4-GW>fN zgSM5!LsY&n@1?hWv}Ts6q4mpmiQF-h9Xjs0HTY5={*YAUSmUKlgq3&LRhNs!H`Dqc z{V@wU1&N*MDXsN#X%qP;=eHyfMg7|4Jf9XEX36g6CAz)h3M3l)=HvL>2}WDMU_TgT z;){=$Og5JUnv+|~d+_lE0ldicAj8$2fXYm)>T1TsHq^9p8m3};d|89*e>TAeUZFZ8 zYuwSWCjvQQ2D%)Ls|&ENO(oL_u`rb}usBxW8_`C0wYCW0JsH8{ahKoj? z_WP@IY($-D(Q$7a3hSRh&8k(IPFr`#}LYolT$niN7}xL{`bG3&bu zC@kT;Z9a)UxACt|F0m%=TE&%>^ZGfzUu`Uy4H}I`=@yHC$)Xq@>Zio<;U;p$Q zE zxIV{0)Jb}@d18_f=1`05@$aUqcG#P4PRTQRS0I<@?NYPGlH~XAUb!|9%+GnhtxvGU zWMIpamhsbwe>Yhdo^~HP(Q3FV_kc}>&^x%0$cdt zU>d1S39WnJYEoB1EarFI==(6H|I+66D&_h*3nS_AM`tUI^&!;hAIrQq>c^H}1FuEX zX02s#5{07A@dsvebmri0q<8jzeFvb03P##fuO{cP*!1*0sW{P+ji?AYggUy6r+0e| zx7PX8uQFAj4Uyop_T657m9y_n8T}9sX-$)_8U$PBe@OM$VIF?@NrN~K(PL$kM>W6S z=9o}dF3jvXnE#m22bcRl*kxmXZiEndXh)7Y@aKB#W8@1s;}Qi9{(+X~fvwVkWah=% z@UpRIyvC(t>&L{)NCch}VXEhE>%_lGId;dSrr2{MbtFVfH|?2*%0!d-7zadd@dDaP zJlWRSs#|>Pp*ADR*q~*JVk87!vH{oQFa$Y#egMj4SA1cx5gE##>P8u0g{`IqC4-KF}F}cYg9a;UM?rPp` z+)cwYk#UNjnplQPV78m4PHhEqZUj8PN2KuFPspKYbRJ)Hv2Ca8Ew@VP2jkUntzUcA z%$ji#Z!I>I@Pcv+P4%VETi7p%iAu6Ixc9eWqTS~tWmJ;JUX)^hG&a+IJiFv3E%iHH5b=<$LFEnWE` z;2k|OQsPUBZ@x38n;Ak++Sd0Mb?N>EEzmjsSz;`C)Y$He(iL8weBGJ5Id)MSKWk%1AT$Oo>kY4WQ&=C_)*rttU7=-g&E$8Ked0WKNI|J#bVmh=kS3c5 z3?-=gmu-jnOm&xy*XmzRjQ9c#R;Jz&X)TdaUt2Bt4oz>;ODT`AqzZjUM-3TEFp6o0 z+8>Ga8h2CXO=N!#!NDbHP0(~1pKtcQ5Tti_D1Bz8L+bUb&n}imYFOilJ~QpnS0AmsMNz`a zHp!2hqU70E9OW>6Ees%zrI`GToF}i*%G9AoRL$y<8y^zpbkoH6bi4s;@Xo*mHXdcSXCka0}N3v~M}B12Eu+@}rMW>JUiSIa%Q73(Fi zjlyNCrj2_W_E{jMH2{6ahslb>p;h;@_pFC#~nYnSDDEDTD~P!&=|^1r7S9x9};PmjE)j#m#RpBcE=12bhzvlskZnz3K_ z@vxhr;zO*qj(Db)l)#zfen2dxy<)uAlw})}7*+egL$D7Of0rtprsg^YgY%4*X*1|J zEO~Sx_auxKhL|Hs3hZkBG<_2jB@cDz*&P1aZp}u+3l?T>Dbp2pj%dq>oTc)&e3ZJH z0HXAQngX(glp~hjlTL^ zvzB2;_=05rX+YV(VM%=1<0x?!-13rU?xIGFTExS11*i9yo8K`wP(wc68l5>VUP9UV@G&?TGht(Gq(?CL zotfn^y*x`0JmPc`fge18dEsxYw zpcrKxTJvAok3P7YpX>n)J56Z=tk^rw@0*Hm#X{z)GhUQlM>j@{@3 z-L^yWw#-cDRMAlm$e$F@b=_s-u{g1XGnhue9PlPaB>0p|FiEDH!qDmaW?viltd%je z@N|;SMbJ!|3jF2Rr3RLA+_GHF0Jz&zY(Cs-Quu1+w06|NZ7k%!g;!%CWnQ1w{xVx< z-SpNQqJ4#2Q(B=II(PXm&BW`Hyx; z=qjb~Xqx+h3SB|9h3F7TfsfK?t*|tDWbIS*|r+lV#AAZ?6WC>R-cMK+X zS-qanI~| z?KC4*CU+2S6OsCU8V&>lPf5qMSNME-7Bvbqs$YiwirbPyNAg8<%YP#7BW<38bY}O9??(^vl zDEdd~llK@}Ebd_K_&xTf6k`HE{z2OVW2l0ELMzL#=#q$z`)RPOCJrKyJ?B8qNJ4MC z6LYP4xr$tAm_hyE1_^#8dAw#QeOaq>T%+7s=54%Fig1n#F$p=O4{WvJ%H$N;} zDe2M25rvb4K32Fz{9|1$VBs4n_oJKGbsEMg0Ui<(YKRh#wK|dQtuENgsq9Ux(kSRN z7`=}3Y6e;nF!9ypr=oXbw4!(|v8>tTxN6_W-1(vv_rxv{lZSr0BeWkXj7@|c zeru7X!RG6}u<;P|+O-DNsUz7l5H2J!gLcw%YBmG1@N{mNLG{`@AmW?%cH3}CH)qNy zV*XFSydD8~S_ZD$m-XfD7>bO_QQ`76U~5IoNKl$bz2~!YyCaYvQnxh8y36tcD6~)p zrW&;Z3D0t?C|372mthqJ;T`dvKNh5|>^DbDDc|kqM_o;{;?0up|IbCW#)#UYdsKz!9q^>E0O=#s0~Gzkl5gKVOQ*!CBq>m9SMGxf zBS_P}^&8?mWB9D6^C5=+47u5~EGQyfFWe!Z3nqSTh$r**|DA8bCBDDyB8zAAt9u0q z+)&8gz_MV0-ImL?z!X!?&z7#H6dV-?#7&-8Z=%HK6G&syJ-E(VOcBdcHHFuX_o~?( z0xc(BTX5VO`EQr}0yR$p%l`7wJD@sF5J4F|TGi<>0lro%MWbCG>n!Q2wp|J6-lEi# z>evycZX^JoYw79$QJ+nTiw(3i027konxUug)ZLB>9g1+0PMY+iP8Cx+^`Jk`;Bz(F zI4XaMGr!MR^WM#WLSq8Aa^y8IA8rtKLbJy6&MA~W`e&uLK|nVwK;jG*Fp44Z+MnYM zmHc_-JqaXtj=!Xd*&USgF*SYRFp!pDoazI9T&7-y0AJ zLp*^C5GuL#XTq@cj4Y!&*az8%VQ-l==%)Qk_*!hLpwI5 z;QqfSTW^dBPd99jukv+1u-d&?7Q9;C_F3vQG%u=mt|Iv$jToLm(kU}{y=_Ddawc(h zypux%$(`~%pd?t%R)t7Xw%li2weBH&)}qTQP8xmhEVBZ49Pp3zce+5R{M7vN@I#3a zLT(V%iu+}%>w}1n5!J?x2K|$Vh6bF*e_yQFYO{O|E*B2SQo?kt;qJF>Ib6}z=)B6P z)rbzA({`st_I7gIM489~*~F&P_i8!;#h4xk(+Y+nk$&Su$bF~}>E1+0jplPrv+jFp5cXy>w@c}23pSJPx5r8uM!e+ zsfp#syBq0*Yfh_d9~E&GMwg)OF)h&tSdW)ISmFK+-psz?&N@^_3tkKmmGWn4IGi_>sN4g zd{{c{d>l3>D$RTl8<_M*+vI5bKF5Ptf~fMjk&fwhDg|mv_#B)Ywkq=-8QwAb@te)f zW7Nt!x~7I#uFmb-se~YESO>FeOl(?piG*;rrFW{DPjSL7${zRV!Pi&iC(if%P812p zKL0&M@Mn8GD3OJyC1la50+=en2-xHjB9=F)$^4RKi2L?F!m7V?&Gf(3KXNICH}0E` zP5kkX19mZ++;TXesTt%%gdv>MS_>PJnshAvK>v~T;yUF*zkKeKm>Ut9ejNeZ*2Q1OgS6G)X%GV+{EXbFs=e!nG9%OPC05i zGdSPrf{mXBGhvmwT8YX@b5xr|!C?b@vhde7T^;USciO3N zoEjnh|86`+CVXMVi^e?_o#ueFbEsI=8aas;Usu*Fvuo`u`f{3vihy;Or|Q3YDuIaj z_*9$XRfRDx=w=Bmfoi#o4e|P{_CzVgZ{!91HFI7;I5MJxc|1zZb>;Pe#Xdgwd|rk1 z^?WOt2w2^2(7GjXp=5TwLn&upc(7&TJt{&Td65`AKUKZHoJre?>xclJsd^HSIU<#^ z(4WXlE4q}+;i^WC8r z!^1^nfzsggqi+}IPEu!Y15gCh+!_h{ixNro%>)DUm{ZlR4vY*5{&@33FSCj3anKxk ztkLLI1(y{5AvisVFzD=YO8TP9NBrlF(#IbmFA(mUXFkVB&~y*D06c?3E}~AO93r=i zcQ}iNVAPSRU*R$|iEU~TwIGhXsOZRaX6vcY9Q4!=L><;IGEUU+}2s7$@c z(mlEpX{7Z)fP-Wq65Tpml&~A#U@bksWd_eAfzX8nW;$uE#BN-`*7x>1@+8XH&+yT* z=mGJ5pA}>VhhY+zBt;^a+yQG|srq`H62MXUx(L-LEmip=VMNRAuH<@=9ffg(oE4R! zx$~4VlQ%)?6aF7_>nRU@fXe|Bl$B~Su|CZ2Sw1&(e%#cYnNz>DvWjGYQbVpOotHMS zNNL`@{#i*Yrh%0NAHDme^4`#9Z~xVbsx{NZ>dmh&wqv9~dreOW+L_R6T(g~og8KqG z!j$+Kc^E#3Z@R6bL~R^h+H#{AXX4SOX3I}_KKpw$$J#wrLct0uN0D&6Qg4VGz5o!O zY(^r_+Ss@2Qp};A+RRF`qoQ5!__~j{3dBW^Z2b< zO>12nlbt!5zk(sV?3tP-4hyzB4f3`owmcFU2@vaR6J`>ow70$+2%NkGY0ljmu$zSY(>mxF-AueKJ*JdRucwt zI}d#Kb&kX2U+4HzbIY>YcBe^T54o1DN-G_Lo!txG6{IfgU7M%RYZ$uV&c53ay9Ht*# zFeOW_=Mk-f0`zX8Z-(LJM8{-=jN7%BdYJ~C&lbPG?q1~umtGz5Kc6^AL*O9n!^}1?G?oo_5_AO}G{i2JXij==_~D2YGSR1Wq1lMYH#A?htue4uIGpc$uv?8s9y!D-;OlC+xQN^5fj{ zdNH4>>KGT$`=57CH3D=DYx!T22%1fOFo2uBEWoz!XSZ+ab4P+f#iQ+)HW=o_C7ZV_ z%yAF1;~oBRfKJ@I{F1>ss+)W7?pd&#&@{e6ba;gK?Ig?qJfsh#h8XAD4?#;8t?Lhk zcPU6BzcUux3{n5Wstg8&#uSZt%O9tpLr!k8y)%iRr<^D%JrXXM{m4q73x>WRIe(#F z8uDCcl@*yJ9$c`lRPbA5jseJgiNE8gdRC^Q zRZ{py-6#b9rG<4tK%{E0bo|A?fA=Um!XzEq18+_5{W=5etPq%hWMA?_YcGET6-l=U45&isH-+%@^VJ0@RjwAI3fx0aoIl4AJePSZ5b>@Z#^Gk(3~Z-uz75 zUg4*E&f;@#$0KQmzLaDw?&4QHsD-H*oM zxEH7Qz5l{gzp#GYWs3|EN@^Syn3{!Z|IPyKKEm!AZeJ;0#5$X~(R<1YapiCSW=b1> z@}&*+i1_0D2R%9jzra1U7XT$)n*=VIWs5O+34`L!ok1!W>H9Z^UosBRWeN+6!UUn# zJh3UK+@f%g_^!eC0A28a;f89AeJfCQx_F~FC%!i?yC`K`TZxpWg`D{?$q14+x}@Wwrf9cHTOq2|M}k?ecGfo^Vf{oN0Ce<}_IoTPf68R?I z(Mx6UPBq4)wC$TWfsr6^?57^ z$QS&UxHCLaL*j6;;}$aaDlehi=Uw#aTGFT_pTeAJNHfafn3Cie5c_h{b%ip^gEGYT zu!&bceyw3m5ZvBDIC{liD^f^*E=HigghKg9r_1ygxjXI}JO6yOw{`s^|Aut8QlPu1 z18Z#uaemc?9#i{BRy;)LN=WLYS7>P2N{bU{ts>~!1^RDie;qWEQ@F_sv@*&2%N-`8 z+66=H@_MS1B5waeXa*PSUz|8FdjzLEXnIGW^$7S&lWR!&7UL5ja^sg(;oDKFxfJY( zTkUguMhm-Xlnql%ERqQ6V`6HEIR`-KDYxJR1-T{Kelna}twQ;_=?^0$Mzp1VzI_(e z8XCaJnc4)TzUA5s%V)Sk^m+LsImaZtY7CS_(i`WcfcG`)5K35~eCwuBG2>~gx;3lo zf&AH96QeL^GJB7l7+*6pw52Cwukldz_jIOsh_!cSN(^d76N6U|?Ql7OP2SI&=oG(n zoD6%t8%tm)JG7;XoL~EoOhxY}U{nG!KK7qfMoRlX=Xts6BArQj35+vSd|__+m|=jZ zlCu>M+G?Ylp}+fSk^~EtGhrAp0|(fo{N=lWB{b<*aGyyR(zr&rXk?fQ)HQE7I{h>5 z9>6Y4lcBhH${NB2{vv9a20t%kj|QA+g<2X{`+*G6uEZ~jGP z9opFk2U^jgg?XDj_|GtAu-R%%)@5`$Tr_T2omD?XwlT)yt&BCuS(M1!>cVZ&ql&pIm~Qzn7GL?(}V29L`q z$OZH}TWu`lRABNwv=9ShcLIYAp+Uo?()|Qx-t<)sz24|{)aUPckdyE+^Y0G0 zv2J(A>J}!hT`oU@J2n^~iy0V%7y<7)&_75uYES%jlxCF^ys2GmxpOutP(4iu+$hz#?9?9VxQ*!s<0mgA|1e0k!*XO{n2PW z2Drm3Jb?zbvW zxuMHSw7&J^iHZ1uMKuEr$Ae6`A&c1H7)d%FO=r++=U3u1cwiV-*5QZXF7lx&Id~O^ z25Jh-Jtq)Jrh2Uo4zD@!cr<+oCuBpnV5mciaE|Aq(KA zT(mAI488C9OtM2J>#$86HtL#W=p&rof^7=6?Z9OnC=V7sOala`0MO1{(A?QXKwe8Ya~d#S*BcgP4+H;aG~wW9sPBC#yMvy(k$l+Z{PxcJmE4`Bs~ z&$m3LFUP8Z_UW0^-O#3U#>Byhtj8ccT1^D+R;t&r%B=2W)}jrU;d*etbxq~`PPUCH zF5P^c_I-pKE_s(ir|!-p+jct}O8_ooFlzUq+1(+xUm_AxV3;$y@Q@yG(y%+%&*5(V z+k@OPt1{YI$35a4zkScz)@;T0}MS8_H@`5$3_Y>$)7J)lWv|sqtiY$mBadXUuMb^*C_mg}J}>)^!2ec$<^LAFgU~>}R%uDEB(Tkc*0o34ZUVMY5f}!Y(ygYO zI&(cdf+oN+u}q7GcSPv#U_qj?E_ws-;m6-=Ivapm2hIEZ8hX3)p$G#QLIuc~hZoj{ zCrfgEI$R%}&2zvC9LWrzQbe6|&@G@=()D2uMxGvXLj!DdGfhBTLLS3sT9w+~b9c7ES8-s3|EqZX zGN;H*GQrx?hwAoX&pv5->`{e&W)fS!pPOa=LK6fD2u|xr^=i`-0TLO~tFPwJhinDK5&f?*(l~KQt2cUNCtLbS!^=5#8Sr#{<|DG%;raqf}^fD(Priwh# zgR;NwE~4jlDHV_`SM(rl!Mc6Nwdo%#$&-$@4f&usY&&b_65JGc*|OQ!BEoww#l{z3 zcI0q8L=)XU=lLIWJoi0sGb%e00z_`=s}t+C4bqb93jKmw{L%VR=UAR27G2H zF{P3EVaB%1a=Q^0UPKkpE6)j&K%RiNKdf7IhZWd}^8X!}kr=;o+6_dw^z!9+y&P?H z1Ao2Sh#`#%9ASIn=@QfFd_3OvsG^9CU0CCO#*?2wFk}~Wdq2v zDXK0zq%7wMxt%ySEPe&-Z6NNIPL{D}85kWD0;0Q`PtIA)mq+7X=WzOIK+Is)V=yyC zfx9G>!u0lLS|Hh)W1Z7tw>{oJ{CwSPpMVrDTS@v=X(=rPQxLsybn-m$u32zFA7MtS zdq9U4ed^PG2!#s472uEIFFg$#r2dchg`)Em;+WJu7^OV#L$7AU*uJym(^~);Ci8TPeaRWcs}AHG8nZEOT;75cr9EE)&woHhDzwr!?~duj%wB%2FFs6iXn6x zB*5YSpS*jgK-_s3^1twg9yrDS>v#SB|MVIkFvtJQCfw(EO7`q47F46aw;>QUC9S9B IkFCT1H|k32kN^Mx literal 17687 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 diff --git a/packages/dashboard/app/public/sw.js b/packages/dashboard/app/public/sw.js index dd28a0ff50..9f53c68c68 100644 --- a/packages/dashboard/app/public/sw.js +++ b/packages/dashboard/app/public/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = "fusion-cache-v3"; +const CACHE_NAME = "fusion-cache-v4"; const APP_SHELL_URLS = [ "/", "/index.html", diff --git a/packages/desktop/scripts/generate-icons.ts b/packages/desktop/scripts/generate-icons.ts index 4e44d9bb27..bad5586aff 100644 --- a/packages/desktop/scripts/generate-icons.ts +++ b/packages/desktop/scripts/generate-icons.ts @@ -9,20 +9,52 @@ const repoRoot = resolve(packageDir, "../.."); const sourceSvgPath = resolve(repoRoot, "packages/dashboard/app/public/logo.svg"); const outputDir = resolve(packageDir, "src/icons"); +const dashboardPwaIconDir = resolve(repoRoot, "packages/dashboard/app/public/icons"); const iconSizes = [16, 32, 48] as const; +const dashboardPwaIconSizes = [192, 512] as const; const APP_ICON_SIZE = 1024; const APP_ICON_PADDING = 128; const APP_ICON_BG = "#0d1117"; const APP_ICON_FG = "#58a6ff"; +const DASHBOARD_PWA_ICON_BG = "#1a1a2e"; +const DASHBOARD_PWA_ICON_FG = APP_ICON_FG; + +async function renderIconTile(options: { + tintedSvg: string; + size: number; + padding: number; + background: string; + outputPath: string; +}): Promise { + const markSize = options.size - options.padding * 2; + const mark = await sharp(Buffer.from(options.tintedSvg), { density: 2048 }) + .resize(markSize, markSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png() + .toBuffer(); + + await sharp({ + create: { + width: options.size, + height: options.size, + channels: 4, + background: options.background, + }, + }) + .composite([{ input: mark, top: options.padding, left: options.padding }]) + .png({ compressionLevel: 9 }) + .toFile(options.outputPath); +} async function main(): Promise { const sourceSvg = await readFile(sourceSvgPath, "utf8"); const trayTintedSvg = sourceSvg.replaceAll("currentColor", "#333333"); const appTintedSvg = sourceSvg.replaceAll("currentColor", APP_ICON_FG); + const dashboardPwaTintedSvg = sourceSvg.replaceAll("currentColor", DASHBOARD_PWA_ICON_FG); await mkdir(outputDir, { recursive: true }); + await mkdir(dashboardPwaIconDir, { recursive: true }); await Promise.all( iconSizes.map(async (size) => { @@ -34,25 +66,34 @@ async function main(): Promise { }), ); - const markSize = APP_ICON_SIZE - APP_ICON_PADDING * 2; - const mark = await sharp(Buffer.from(appTintedSvg), { density: 2048 }) - .resize(markSize, markSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) - .png() - .toBuffer(); + await renderIconTile({ + tintedSvg: appTintedSvg, + size: APP_ICON_SIZE, + padding: APP_ICON_PADDING, + background: APP_ICON_BG, + outputPath: resolve(outputDir, "icon.png"), + }); - await sharp({ - create: { - width: APP_ICON_SIZE, - height: APP_ICON_SIZE, - channels: 4, - background: APP_ICON_BG, - }, - }) - .composite([{ input: mark, top: APP_ICON_PADDING, left: APP_ICON_PADDING }]) - .png({ compressionLevel: 9 }) - .toFile(resolve(outputDir, "icon.png")); + /* + FNXC:DashboardPWAIcons 2026-06-16-21:10: + Dashboard mobile home-screen icons must be derived from packages/dashboard/app/public/logo.svg so the installed PWA, iOS apple-touch tile, and Android launcher glyph stay aligned with the in-app Fusion brand mark. + Regenerate these assets and bump the dashboard service-worker cache whenever the canonical logo.svg brand mark changes. + */ + await Promise.all( + dashboardPwaIconSizes.map(async (size) => { + await renderIconTile({ + tintedSvg: dashboardPwaTintedSvg, + size, + padding: Math.round(size * (APP_ICON_PADDING / APP_ICON_SIZE)), + background: DASHBOARD_PWA_ICON_BG, + outputPath: resolve(dashboardPwaIconDir, `icon-${size}.png`), + }); + }), + ); - console.log(`Generated ${iconSizes.length} tray icons and app icon in ${outputDir}`); + console.log( + `Generated ${iconSizes.length} tray icons and app icon in ${outputDir}; ${dashboardPwaIconSizes.length} dashboard PWA icons in ${dashboardPwaIconDir}`, + ); } void main();