fix: overhaul PWA icons with v2 suffix to bust all cache layers

- Redesign favicon SVG with gradient border matching splash screen
- Rename all icon files with -v2 suffix to bypass browser/SW/PWA caches
- Delete 9 old/duplicate icon files (~13MB removed)
- Add nginx cache-control headers for icons and manifest
- Rename assets-cache to assets-cache-v2 to orphan stale SW cache
- Update all HTML, manifest, and component icon references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-03-06 01:11:00 +00:00
parent dfb81c0348
commit 2818fe90fc
31 changed files with 90 additions and 96 deletions

View File

@ -10,7 +10,7 @@ server {
location /aiui/ { location /aiui/ {
alias /opt/archipelago/web-ui/aiui/; alias /opt/archipelago/web-ui/aiui/;
index index.html; index index.html;
try_files $uri $uri/ =404; try_files $uri $uri/ /aiui/index.html;
} }
# AIUI Claude API proxy — routes through claude-proxy service (port 3141) # AIUI Claude API proxy — routes through claude-proxy service (port 3141)
@ -54,6 +54,12 @@ server {
return 503 '{"error":"SearXNG is not running"}'; return 503 '{"error":"SearXNG is not running"}';
} }
# Icons, favicon, manifest — always revalidate (no heuristic caching)
location ~* ^/(favicon\.ico|manifest\.webmanifest|assets/icon/) {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
# Serve static files (Vue.js SPA) # Serve static files (Vue.js SPA)
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
@ -385,6 +391,12 @@ server {
proxy_send_timeout 120s; proxy_send_timeout 120s;
} }
# Icons, favicon, manifest — always revalidate (no heuristic caching)
location ~* ^/(favicon\.ico|manifest\.webmanifest|assets/icon/) {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri =404;
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }

View File

@ -82,11 +82,12 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.0ddc43l70qk" "revision": "0.qqvn6d3mva8"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/] allowlist: [/^\/$/],
denylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//]
})); }));
workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({ workbox.registerRoute(/^https:\/\/fonts\.googleapis\.com\/.*/i, new workbox.CacheFirst({
"cacheName": "google-fonts-cache", "cacheName": "google-fonts-cache",

View File

@ -2,15 +2,15 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" sizes="48x48" /> <link rel="icon" type="image/svg+xml" href="/assets/icon/favico-black-v2.svg" />
<link rel="icon" type="image/svg+xml" href="/assets/icon/favico-black.svg" /> <link rel="icon" href="/favicon-v2.ico" sizes="48x48" />
<link rel="icon" type="image/png" sizes="64x64" href="/assets/icon/pwa-64x64.png" /> <link rel="icon" type="image/png" sizes="64x64" href="/assets/icon/pwa-64x64-v2.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon/pwa-192x192.png" /> <link rel="icon" type="image/png" sizes="192x192" href="/assets/icon/pwa-192x192-v2.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon/pwa-512x512.png" /> <link rel="icon" type="image/png" sizes="512x512" href="/assets/icon/pwa-512x512-v2.png" />
<link rel="apple-touch-icon" href="/assets/icon/apple-touch-icon-180x180.png" /> <link rel="apple-touch-icon" href="/assets/icon/apple-touch-icon-180x180-v2.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icon/apple-touch-icon-180x180.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/assets/icon/apple-touch-icon-180x180-v2.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/assets/icon/pwa-192x192.png" /> <link rel="apple-touch-icon" sizes="192x192" href="/assets/icon/pwa-192x192-v2.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/assets/icon/pwa-512x512.png" /> <link rel="apple-touch-icon" sizes="512x512" href="/assets/icon/pwa-512x512-v2.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content" />
<meta name="description" content="Archipelago - Your sovereign personal server" /> <meta name="description" content="Archipelago - Your sovereign personal server" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
@ -20,7 +20,7 @@
<meta name="apple-mobile-web-app-title" content="Archipelago" /> <meta name="apple-mobile-web-app-title" content="Archipelago" />
<meta name="application-name" content="Archipelago" /> <meta name="application-name" content="Archipelago" />
<meta name="msapplication-TileColor" content="#000000" /> <meta name="msapplication-TileColor" content="#000000" />
<meta name="msapplication-TileImage" content="/assets/icon/pwa-192x192.png" /> <meta name="msapplication-TileImage" content="/assets/icon/pwa-192x192-v2.png" />
<title>Archipelago OS</title> <title>Archipelago OS</title>
</head> </head>
<body> <body>

View File

@ -235,7 +235,7 @@ async function getDockerContainers() {
const metadata = appMetadata[appId] || { const metadata = appMetadata[appId] || {
title: appId, title: appId,
icon: '/assets/img/favico.png', icon: '/assets/icon/pwa-192x192-v2.png',
description: `${appId} application` description: `${appId} application`
} }
@ -342,7 +342,7 @@ async function installPackage(id, manifestUrl) {
title: 'Amin', title: 'Amin',
shortDesc: 'Administrative interface for Archipelago', shortDesc: 'Administrative interface for Archipelago',
longDesc: 'Amin provides administrative tools and monitoring.', longDesc: 'Amin provides administrative tools and monitoring.',
icon: '/assets/img/favico.png' icon: '/assets/icon/pwa-192x192-v2.png'
} }
} }
@ -350,7 +350,7 @@ async function installPackage(id, manifestUrl) {
title: id.charAt(0).toUpperCase() + id.slice(1), title: id.charAt(0).toUpperCase() + id.slice(1),
shortDesc: `${id} application`, shortDesc: `${id} application`,
longDesc: `${id} application for Archipelago`, longDesc: `${id} application for Archipelago`,
icon: '/assets/img/favico.png' icon: '/assets/icon/pwa-192x192-v2.png'
} }
// Determine port // Determine port

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 363 B

View File

@ -0,0 +1,31 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<radialGradient id="bgGrad" cx="50%" cy="45%" r="55%">
<stop offset="0%" stop-color="#1a1a1a"/>
<stop offset="100%" stop-color="#050505"/>
</radialGradient>
<linearGradient id="borderGrad" x1="0" y1="0" x2="1024" y2="1024" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.55"/>
<stop offset="50%" stop-color="#888888" stop-opacity="0.25"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.7"/>
</linearGradient>
<radialGradient id="glassGlow" cx="35%" cy="30%" r="50%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.06"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0"/>
</radialGradient>
</defs>
<!-- Background circle -->
<circle cx="512" cy="512" r="484" fill="url(#bgGrad)"/>
<!-- Glass glow highlight (top-left) -->
<circle cx="512" cy="512" r="460" fill="url(#glassGlow)"/>
<!-- Gradient border ring (white-to-dark, like logo-gradient-border CSS) -->
<circle cx="512" cy="512" r="484" fill="none" stroke="url(#borderGrad)" stroke-width="16"/>
<!-- The "A" logo — pixel-art style, centered and scaled -->
<g transform="translate(160, 168) scale(0.69)">
<path d="M357.614 388.936V318H428.621V388.936H357.614ZM436.152 388.936V318H508.234V388.936H436.152ZM515.766 388.936V318H587.848V388.936H515.766ZM595.379 388.936V318H666.386V388.936H595.379ZM595.379 468.471V396.46H666.386V468.471H595.379ZM673.917 468.471V396.46H746V468.471H673.917ZM278 548.006V475.994H350.083V548.006H278ZM357.614 548.006V475.994H428.621V548.006H357.614ZM436.152 548.006V475.994H508.234V548.006H436.152ZM515.766 548.006V475.994H587.848V548.006H515.766ZM595.379 548.006V475.994H666.386V548.006H595.379ZM673.917 548.006V475.994H746V548.006H673.917ZM278 626.465V555.529H350.083V626.465H278ZM357.614 626.465V555.529H428.621V626.465H357.614ZM595.379 626.465V555.529H666.386V626.465H595.379ZM673.917 626.465V555.529H746V626.465H673.917ZM357.614 706V633.989H428.621V706H357.614ZM436.152 706V633.989H508.234V706H436.152ZM515.766 706V633.989H587.848V706H515.766ZM595.379 706V633.989H666.386V706H595.379Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,4 +0,0 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1024" height="1024" fill="#030202"/>
<path d="M357.614 388.936V318H428.621V388.936H357.614ZM436.152 388.936V318H508.234V388.936H436.152ZM515.766 388.936V318H587.848V388.936H515.766ZM595.379 388.936V318H666.386V388.936H595.379ZM595.379 468.471V396.46H666.386V468.471H595.379ZM673.917 468.471V396.46H746V468.471H673.917ZM278 548.006V475.994H350.083V548.006H278ZM357.614 548.006V475.994H428.621V548.006H357.614ZM436.152 548.006V475.994H508.234V548.006H436.152ZM515.766 548.006V475.994H587.848V548.006H515.766ZM595.379 548.006V475.994H666.386V548.006H595.379ZM673.917 548.006V475.994H746V548.006H673.917ZM278 626.465V555.529H350.083V626.465H278ZM357.614 626.465V555.529H428.621V626.465H357.614ZM595.379 626.465V555.529H666.386V626.465H595.379ZM673.917 626.465V555.529H746V626.465H673.917ZM357.614 706V633.989H428.621V706H357.614ZM436.152 706V633.989H508.234V706H436.152ZM515.766 706V633.989H587.848V706H515.766ZM595.379 706V633.989H666.386V706H595.379Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,17 +0,0 @@
<svg width="285" height="273" viewBox="0 0 285 273" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_145_9)">
<path d="M67.6045 215.234H154.772V240.824H28L77.7109 127.559L131.736 76.2861L67.6045 215.234ZM198.893 146.688H256.594L213.508 240.824H171.984V172.604H114.283L156.588 76.7314L177.74 50.3652L198.893 24V146.688ZM198.893 214.361L217.207 172.604H198.893V214.361ZM153.67 146.688H171.984V104.929L153.67 146.688Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_145_9" x="0" y="0" width="284.594" height="272.824" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="14"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_145_9"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_145_9" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 B

View File

@ -7,7 +7,7 @@
> >
<div class="glass-card p-4 flex items-center gap-4 shadow-xl"> <div class="glass-card p-4 flex items-center gap-4 shadow-xl">
<img <img
src="/assets/icon/pwa-192x192.png" src="/assets/icon/pwa-192x192-v2.png"
alt="Archipelago" alt="Archipelago"
class="w-14 h-14 rounded-xl shrink-0" class="w-14 h-14 rounded-xl shrink-0"
/> />

View File

@ -4,7 +4,7 @@
<div class="text-center mb-8"> <div class="text-center mb-8">
<div class="logo-gradient-border inline-block mb-8"> <div class="logo-gradient-border inline-block mb-8">
<img <img
src="/assets/img/favico.svg" src="/assets/icon/favico-black-v2.svg"
alt="Archipelago" alt="Archipelago"
class="w-20 h-20" class="w-20 h-20"
/> />

View File

@ -9,7 +9,7 @@ export default defineConfig({
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['assets/icon/favico-black.svg', 'assets/icon/pwa-192x192.png', 'assets/icon/pwa-512x512.png', 'assets/icon/apple-touch-icon-180x180.png', 'favicon.ico'], includeAssets: ['assets/icon/favico-black-v2.svg', 'assets/icon/pwa-64x64-v2.png', 'assets/icon/pwa-192x192-v2.png', 'assets/icon/pwa-512x512-v2.png', 'assets/icon/maskable-icon-512x512-v2.png', 'assets/icon/apple-touch-icon-180x180-v2.png', 'favicon-v2.ico'],
manifest: { manifest: {
name: 'Archipelago', name: 'Archipelago',
short_name: 'Archipelago', short_name: 'Archipelago',
@ -25,15 +25,15 @@ export default defineConfig({
categories: ['productivity', 'utilities'], categories: ['productivity', 'utilities'],
prefer_related_applications: false, prefer_related_applications: false,
icons: [ icons: [
{ src: '/assets/icon/pwa-64x64.png', sizes: '64x64', type: 'image/png', purpose: 'any' }, { src: '/assets/icon/pwa-64x64-v2.png', sizes: '64x64', type: 'image/png', purpose: 'any' },
{ src: '/assets/icon/pwa-192x192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, { src: '/assets/icon/pwa-192x192-v2.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
{ src: '/assets/icon/pwa-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, { src: '/assets/icon/pwa-512x512-v2.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
{ src: '/assets/icon/maskable-icon-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' } { src: '/assets/icon/maskable-icon-512x512-v2.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }
], ],
shortcuts: [ shortcuts: [
{ name: 'Dashboard', short_name: 'Dashboard', description: 'Open the dashboard', url: '/dashboard', icons: [{ src: '/assets/icon/pwa-192x192.png', sizes: '192x192' }] }, { name: 'Dashboard', short_name: 'Dashboard', description: 'Open the dashboard', url: '/dashboard', icons: [{ src: '/assets/icon/pwa-192x192-v2.png', sizes: '192x192' }] },
{ name: 'My Apps', short_name: 'Apps', description: 'Manage your apps', url: '/dashboard/apps', icons: [{ src: '/assets/icon/pwa-192x192.png', sizes: '192x192' }] }, { name: 'My Apps', short_name: 'Apps', description: 'Manage your apps', url: '/dashboard/apps', icons: [{ src: '/assets/icon/pwa-192x192-v2.png', sizes: '192x192' }] },
{ name: 'App Store', short_name: 'Store', description: 'Browse and install apps', url: '/dashboard/marketplace', icons: [{ src: '/assets/icon/pwa-192x192.png', sizes: '192x192' }] } { name: 'App Store', short_name: 'Store', description: 'Browse and install apps', url: '/dashboard/marketplace', icons: [{ src: '/assets/icon/pwa-192x192-v2.png', sizes: '192x192' }] }
] ]
}, },
workbox: { workbox: {
@ -44,8 +44,6 @@ export default defineConfig({
'**/*-1.47mb.mp4', '**/*-1.47mb.mp4',
'**/bg-*.mp4', // Exclude large background videos from precache '**/bg-*.mp4', // Exclude large background videos from precache
'**/video-intro.mp4', // Exclude video-intro.mp4 from precache (7MB, cached at runtime) '**/video-intro.mp4', // Exclude video-intro.mp4 from precache (7MB, cached at runtime)
'**/favico.svg', // Exclude large SVG (5.73MB) - use PNG instead
'**/icon/favico.svg' // Exclude duplicate large SVG
], ],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit (increased from 2MB) maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit (increased from 2MB)
skipWaiting: false, // Wait for user to accept update skipWaiting: false, // Wait for user to accept update
@ -95,7 +93,7 @@ export default defineConfig({
urlPattern: /\/assets\/.*/i, urlPattern: /\/assets\/.*/i,
handler: 'CacheFirst', handler: 'CacheFirst',
options: { options: {
cacheName: 'assets-cache', cacheName: 'assets-cache-v2',
expiration: { expiration: {
maxEntries: 100, maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days

View File

@ -153,19 +153,26 @@ if [ "$LIVE" = true ]; then
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo cp -rf $TARGET_DIR/web/dist/neode-ui/* /opt/archipelago/web-ui/" sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo cp -rf $TARGET_DIR/web/dist/neode-ui/* /opt/archipelago/web-ui/"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui" sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui"
# Build and deploy AIUI # Build and deploy AIUI (non-fatal — never delete existing AIUI on failure)
AIUI_DIR="$PROJECT_DIR/../AIUI" AIUI_DIR="$PROJECT_DIR/../AIUI"
AIUI_DIST="$AIUI_DIR/packages/app/dist" AIUI_DIST="$AIUI_DIR/packages/app/dist"
if [ -d "$AIUI_DIR/packages/app" ]; then if [ -d "$AIUI_DIR/packages/app" ]; then
echo "$(timestamp) Building AIUI for /aiui/ base path..." echo "$(timestamp) Building AIUI for /aiui/ base path..."
cd "$AIUI_DIR" && VITE_BASE_PATH=/aiui/ pnpm build 2>&1 | tail -5 AIUI_BUILD_OK=false
if (cd "$AIUI_DIR" && VITE_BASE_PATH=/aiui/ pnpm build 2>&1 | tail -10); then
AIUI_BUILD_OK=true
fi
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
if [ "$AIUI_BUILD_OK" = true ] && [ -d "$AIUI_DIST" ] && [ -f "$AIUI_DIST/index.html" ]; then
echo "$(timestamp) Deploying AIUI..." echo "$(timestamp) Deploying AIUI..."
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /opt/archipelago/web-ui/aiui" sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /opt/archipelago/web-ui/aiui"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo rm -rf /opt/archipelago/web-ui/aiui/*" sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo rm -rf /opt/archipelago/web-ui/aiui/*"
cd "$AIUI_DIST" && tar cf - . | sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo tar xf - -C /opt/archipelago/web-ui/aiui/" cd "$AIUI_DIST" && tar cf - . | sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo tar xf - -C /opt/archipelago/web-ui/aiui/"
cd "$PROJECT_DIR" cd "$PROJECT_DIR"
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui/aiui && sudo chmod 755 /opt/archipelago/web-ui/aiui && sudo find /opt/archipelago/web-ui/aiui -type d -exec chmod 755 {} \;" sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo chown -R 1000:1000 /opt/archipelago/web-ui/aiui && sudo chmod 755 /opt/archipelago/web-ui/aiui && sudo find /opt/archipelago/web-ui/aiui -type d -exec chmod 755 {} \;"
else
echo "$(timestamp) ⚠️ AIUI build failed — keeping existing AIUI on server"
fi
else else
echo "$(timestamp) ⚠️ AIUI not found at $AIUI_DIR, skipping" echo "$(timestamp) ⚠️ AIUI not found at $AIUI_DIR, skipping"
fi fi