Compare commits

8 Commits
Author SHA1 Message Date
fabiodalez-dev c072300969 v2.9.0: Sync upstream 41d3b9c (unified playback API, native OAuth deep link)
- Drop patch #53 (storage.js streaming fallback: upstream removed the hardcoded
  instance list, all old HiFi proxies are down) and #56 (HiFi.ts
  tracks.albums.coverArt — merged upstream). 12/12 remaining patches apply.
- AndroidManifest: add monochrome://auth-callback intent-filter for the native
  OAuth flow (js/accounts/auth.js via @capacitor/browser).
- build-web.sh: recreate dist-web.zip from scratch, prune stale assets/ on deploy.
- versionCode 13, versionName 2.9.0.
- Verified on emulator (Pixel 6 / API 35): search + playback + next track OK.
2026-08-16 15:47:35 +02:00
fabiodalez-devandClaude Sonnet 4.6 bfab3e8f90 v2.8.1: Fix Tidal proxy (tidal-proxy.monochrome.tf)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:40:42 +02:00
fabiodalez-devandClaude Sonnet 4.6 b03298725e v2.8.0: Sync upstream 8cf6740 (Qobuz proxy, theme FOUC fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 00:48:34 +02:00
fabiodalez-devandClaude Sonnet 4.6 3e8c9e7d98 v2.7.0: Sync upstream 3936c07 (enrichment fallbacks, playlist exports, download fix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 18:17:28 +02:00
fabiodalez-devandClaude Sonnet 4.6 b3730f7a69 v2.6.1: Fix Picsum images on track rows in artist page
Add tracks.albums.coverArt to artist endpoint include in HiFi.ts.
Album items were landing in includedMap without coverArt relationship,
so resolveArtworkId returned null and every track row showed Picsum.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:49:18 +02:00
fabiodalez-devandClaude Sonnet 4.6 a83ed74219 Add build-web.sh for music.fabiodalez.it deployment
Builds patched web app (no Android-specific changes) and packages
dist/ as dist-web.zip for manual upload via cPanel File Manager.

Patches applied: artists.profileArt, debounce 800ms, album enrichment,
cache TTL/normalization, search limit=100, streaming instances fallback,
workbox NetworkOnly for audio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:40:15 +02:00
fabiodalez-devandClaude Sonnet 4.6 a935cfbf68 v2.6.0: Sync upstream eb3558a (Tidal API proxy, AAC fix, lyrics)
Upstream changes included:
- All Tidal API calls now routed through tidal-api.geeked.wtf proxy,
  fixing CORS blocks on api.tidal.com in the Android WebView
- AAC 320KBPS download bitrate corrected (HIGH: 256k -> 320k)
- am-lyrics dependency bump
- Parties button can now be disabled from settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 16:58:50 +02:00
fabiodalez-devandClaude Sonnet 4.6 baaca85f5d v2.5.1: Reduce search debounce 3000ms -> 800ms
3 seconds after the last keystroke was too slow for mobile UX.
800ms is the standard for search bars and still avoids firing on
every keystroke. The 'partial query' behavior is normal for search
(same as Spotify/Google) and not a regression.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 11:53:09 +02:00
5 changed files with 451 additions and 25 deletions

No files matched your search

+35 -1
View File
@@ -500,8 +500,42 @@ Studio e guarda l'errore reale. Tipicamente:
## Cronologia delle patch
- **2026-08-16** — v2.9.0: Sync upstream `41d3b9c` (249 commit dopo `abbc2a6`).
Upstream ha riscritto lo stack di riproduzione ("unified API": query TIDAL
native via `HiFiClient` + `music-api.geeked.wtf` con Turnstile + fallback
Deezer; account con better-auth, `@capacitor/browser` per l'OAuth nativo).
Patch rimosse: **#53** `storage.js` fallback istanze streaming (upstream ha
tolto la lista hardcoded, `INSTANCES_URLS` è vuoto e i vecchi proxy HiFi
sono tutti 503/irraggiungibili) e **#56** `HiFi.ts` `tracks.albums.coverArt`
(merged upstream). Le altre 12/12 patch matchano. Aggiunto al manifest del
wrapper l'intent-filter `monochrome://auth-callback` (login nativo).
`build-web.sh`: `rm -f dist-web.zip` prima dello zip (`zip -r` aggiornava
l'archivio vecchio) e prune di `assets/` sul server dopo il deploy.
**Nota importante**: sul dominio `music.fabiodalez.it` la riproduzione NON
funziona più — le API upstream (Turnstile sitekey, CORS del fallback Deezer)
sono bloccate ai soli domini ufficiali (issue #725, risposta del maintainer:
"self-hosted instances won't be able to use the API"). Servono un App API
Key personale, oppure dev-mode + estensione con account TIDAL. L'APK usa
`hostname: monochrome.tf` come origin del WebView e resta nel perimetro:
**verificato su emulatore** (Pixel 6 / API 35, via CDP sul WebView) — ricerca
100 brani, play di "Creep" e brano successivo con MediaSession `PLAYING` e
notifica del foreground service.
- **2026-04-10** — Initial release: 50 + 1 ottimizzazioni implementate
(escluso #31 battery opt su richiesta esplicita dell'utente). Aggiunti
`fm-build.sh`, `verify-patches.sh`, `PATCHES.md`. Rimosso il sed obsoleto
`viewport-fit=cover` perché upstream già lo ship.
- Future update: riservato per quando upstream mergerà le PR candidate
- **2026-05-11** — v2.8.1: Sync upstream `abbc2a6`. Fix proxy Tidal:
`tidal-api.geeked.wtf` → `tidal-proxy.monochrome.tf` (ufficiale su CDN
Cloudflare MXP). Confermato funzionante via test streaming su monochrome.tf.
Rimozione preconnect `fonts.coollabs.io`. Bump am-lyrics.
- **2026-05-08** — v2.8.0: Sync upstream `8cf6740`. Nuovo proxy Qobuz primario
(`trypt-hifi-dl-456461932686.us-west1.run.app`), fix FOUC tema (preload
script in `<head>`), nuova istanza ufficiale `if-it-runs-ship-it.lol`.
Tutti i patch invariati (11/11 pass).
- **2026-05-06** — v2.7.0: Sync upstream `3936c07`. 14 nuovi commit upstream
inclusi senza conflitti (11/11 patch pass). Novità upstream rilevanti:
`enrichArtistsWithPicture` + `enrichTracksWithAlbumCover` come fallback lazy
in `api.js` (complementari ai nostri fix #55/#56 su `HiFi.ts`); fix download
quality Qobuz (LOSSLESS→`'6'`); playlist exports; rimozione browser extension.
Manifest Android già aggiornato (POST_NOTIFICATIONS, READ_MEDIA_AUDIO,
WAKE_LOCK presenti da prima nel wrapper).
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.monochrome.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 9
versionName "2.5.0"
versionCode 13
versionName "2.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+8
View File
@@ -25,6 +25,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Native OAuth return (js/accounts/auth.js: monochrome://auth-callback via @capacitor/browser + App.appUrlOpen) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="monochrome" android:host="auth-callback" />
</intent-filter>
</activity>
<service
+20 -22
View File
@@ -11,6 +11,7 @@ set -euo pipefail
# js/app.js — search debounce 3000→500 ms
# js/api.js — search result limit = 100 (was backend default ≈25)
# js/cache.js — per-type TTL + query normalization (trim/lowercase/diacritics)
# js/app.js — search debounce 3000ms → 800ms
# js/HiFi.ts — add artists.profileArt to unified search include (fix Picsum covers)
#
# All reverted automatically on exit. Upstream repo stays clean.
@@ -192,33 +193,27 @@ def patch(path, before, after, label):
print(" + " + label)
return True
# ── #53: Add live streaming instances to hardcoded fallback list ──
# The uptime worker returns streaming:[] so the default list is the only fallback
# when the network fetch fails on a fresh install. We add the same 3 live instances
# used in the web app (index.html) and android-service.js localStorage bootstrap.
# frankfurt-2 was previously here but is now DOWN (504).
patch(
"js/storage.js",
""" streaming: [
{ url: 'https://hifi.geeked.wtf', version: '2.7' },""",
""" streaming: [
{ url: 'https://eu-central.monochrome.tf', version: '2.10' },
{ url: 'https://us-west.monochrome.tf', version: '2.10' },
{ url: 'https://hifi-api.kennyy.com.br', version: '2.10' },
{ url: 'https://hifi.geeked.wtf', version: '2.7' },""",
"storage.js: add live streaming instances to fallback",
)
# ── #53: storage.js streaming fallback — REMOVED 2026-08-16 ──
# Upstream (41d3b9c) dropped the hardcoded instance list: INSTANCES_URLS is
# empty, the offline fallback is `api: [lol.samidy.workers.dev], streaming: []`
# and playback goes through the "unified playback" API (music-api.geeked.wtf)
# plus native TIDAL queries (HiFiClient). The old HiFi proxies are all 503/down.
# ── #54: REMOVED — was forcing native HiFiClient for streaming, which gives
# only preview (1:40) because the browser client credentials aren't premium.
# Streaming MUST go through proxy instances (they have premium credentials).
# ── #1 + #2: debounce + min-chars REMOVED ──
# The upstream 3000ms debounce works fine in practice — it lets users finish
# typing before navigating. Our attempts to reduce it (500ms, 700ms) all
# caused the search page to render for partial queries.
# Keeping upstream behavior as-is.
#
# ── #1: debounce 3000ms → 800ms ──
# 3 seconds after the last keystroke is too slow for mobile. 800ms is the
# standard for search bars and still avoids firing on every keystroke.
# "Renders partial queries" is normal search behavior (like Spotify/Google).
patch(
"js/app.js",
"}, 3000);",
"}, 800);",
"app.js: search debounce 3000ms -> 800ms",
)
# The limit=100 and cache normalization patches below still apply.
# ── #52: Enrich albums missing artist/cover from track data ──
@@ -431,6 +426,9 @@ patch(
"HiFi.ts: add artists.profileArt to unified search include (fixes Picsum artist covers)",
)
# ── #56: HiFi.ts artist page tracks.albums.coverArt — UPSTREAM MERGED (2026-08-16) ──
# Upstream now ships 'albums,albums.coverArt,tracks,tracks.albums,tracks.albums.coverArt,biography,profileArt'.
print(" ✓ Upstream JS optimizations applied.")
PYEOF
Executable
+386
View File
@@ -0,0 +1,386 @@
#!/bin/bash
set -euo pipefail
# ─────────────────────────────────────────────────────────
# build-web.sh — Fabiodalez Music web build + deploy to music.fabiodalez.it
#
# Applies the same functional patches as build-android.sh
# (no Android-specific changes: no android-service.js, no fm-logger,
# no brand rename, no viewport-fit).
#
# Output: dist/ directory + dist-web.zip ready for cPanel upload.
# ─────────────────────────────────────────────────────────
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
DEPLOY_HOST="fabiodalez.it"
# cPanel subdomain document root (standard cPanel layout)
DEPLOY_PATH="music.fabiodalez.it"
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
cd "$PROJECT_DIR"
cleanup() {
echo ""
echo "▶ Cleaning up patched files..."
for f in .gitignore; do
if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
git checkout -- "$f" 2>/dev/null || true
fi
done
rm -rf \
index.html package.json package-lock.json bun.lock bun.lockb \
vite.config.ts vite-plugin-auth-gate.js vite-plugin-blob.ts \
vite-plugin-svg-use.ts vite-plugin-upload.js \
styles.css stream-stub.js tsconfig.json tsconfig-eslint.json \
eslint.config.js lhci.yml nginx.conf \
js/ src/ public/ assets/ functions/ images/ dist/ node_modules/ \
.npmrc .prettierrc .stylelintrc.json .htmlhintrc .gitmodules \
.dockerignore .vscode/ .wrangler/ \
android/build.gradle android/capacitor.settings.gradle \
android/gradle.properties android/settings.gradle android/variables.gradle \
android/gradlew android/gradlew.bat android/gradle/ android/.gitignore
rm -rf node_modules/@capgo/capacitor-media-session 2>/dev/null || true
echo " ✓ Upstream web app files removed."
}
trap cleanup EXIT
echo "══════════════════════════════════════════"
echo " Fabiodalez Music — Web Build"
echo "══════════════════════════════════════════"
# ── 1. Pull latest upstream ──
echo ""
echo "▶ Pulling latest from upstream/main..."
cleanup 2>/dev/null || true
git fetch upstream
UPSTREAM_SHA=$(git rev-parse upstream/main)
SYNC_FILE="$PROJECT_DIR/.upstream-sync-sha"
LAST_SHA=$(cat "$SYNC_FILE" 2>/dev/null || echo "")
if [ "$UPSTREAM_SHA" = "$LAST_SHA" ] && [ -f index.html ]; then
echo " Already up to date ($(git rev-parse --short upstream/main))."
read -p " Build anyway? (y/N) " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
else
echo " Extracting web app from upstream/main ($(git rev-parse --short upstream/main))..."
git archive upstream/main | tar -x \
--exclude='capacitor.config.ts' \
--exclude='README.md' \
--exclude='android/' \
--exclude='ios/' \
--exclude='extension/' \
--exclude='docker/' \
--exclude='.devcontainer' \
--exclude='.github' \
--exclude='CONTRIBUTING.md' \
--exclude='DOCKER.md' \
--exclude='INSTANCES.md' \
--exclude='THEME_GUIDE.md' \
--exclude='license'
echo "$UPSTREAM_SHA" > "$SYNC_FILE"
echo " ✓ Updated to $(git rev-parse --short upstream/main)."
fi
# ── 2. Fix upstream broken npm override ──
if grep -q '"sourcemap-codec": "\^1.4.14"' package.json; then
sed -i '' 's|"sourcemap-codec": "\^1.4.14"|"sourcemap-codec": "^1.4.8"|' package.json
echo " ✓ package.json: sourcemap-codec override fixed."
fi
# ── 3. Install deps + shim media-session ──
echo ""
echo "▶ Installing dependencies..."
npm install 2>&1 | tail -3
echo " ✓ Done."
SHIM_DIR="node_modules/@capgo/capacitor-media-session"
[ -d "$SHIM_DIR" ] && rm -rf "$SHIM_DIR"
mkdir -p "$SHIM_DIR"
cat > "$SHIM_DIR/index.js" <<'SHIMEOF'
export const MediaSession = {
setActionHandler: async () => {},
setMetadata: async () => {},
setPlaybackState: async () => {},
setPositionState: async () => {},
};
SHIMEOF
cat > "$SHIM_DIR/package.json" <<'SHIMEOF'
{"name":"@capgo/capacitor-media-session","version":"0.0.0-shim","main":"index.js","module":"index.js","type":"module"}
SHIMEOF
echo " ✓ @capgo/capacitor-media-session shimmed."
# ── 4. Apply functional patches ──
echo ""
echo "▶ Patching upstream sources..."
python3 <<'PYEOF'
import sys, os, re
PROJECT_DIR = os.environ.get("PROJECT_DIR", os.getcwd())
def patch(path, before, after, label):
full = os.path.join(PROJECT_DIR, path)
with open(full, "r", encoding="utf-8") as f:
src = f.read()
if before not in src:
print(" ! " + label + ": pattern not found, skipping")
return False
src = src.replace(before, after, 1)
with open(full, "w", encoding="utf-8") as f:
f.write(src)
print(" + " + label)
return True
# ── storage.js streaming fallback: REMOVED 2026-08-16 ──
# Upstream (41d3b9c) dropped the hardcoded instance list: INSTANCES_URLS is
# empty, the offline fallback is `api: [lol.samidy.workers.dev], streaming: []`
# and playback goes through the "unified playback" API (music-api.geeked.wtf)
# plus native TIDAL queries (HiFiClient). The old HiFi proxies are all 503/down.
# ── search debounce 3000ms → 800ms ──
patch(
"js/app.js",
"}, 3000);",
"}, 800);",
"app.js: search debounce 3000ms -> 800ms",
)
# ── enrich albums missing artist/cover from tracks ──
patch(
"js/ui.js",
""" if (finalAlbums.length === 0 && finalTracks.length > 0) {
const albumMap = new Map();
finalTracks.forEach((track) => {
if (track.album && !albumMap.has(track.album.id)) {
albumMap.set(track.album.id, track.album);
}
});
finalAlbums = Array.from(albumMap.values());
}""",
""" if (finalAlbums.length === 0 && finalTracks.length > 0) {
const albumMap = new Map();
finalTracks.forEach((track) => {
if (track.album && !albumMap.has(track.album.id)) {
albumMap.set(track.album.id, track.album);
}
});
finalAlbums = Array.from(albumMap.values());
}
// Enrich albums that have no artist or cover from track data
if (finalAlbums.length > 0 && finalTracks.length > 0) {
const trackInfoMap = new Map();
finalTracks.forEach((track) => {
if (track.album && track.album.id && !trackInfoMap.has(track.album.id)) {
trackInfoMap.set(track.album.id, {
artist: track.artist || (track.artists && track.artists[0]) || null,
cover: (track.album && track.album.cover) || null,
});
}
});
finalAlbums.forEach((album) => {
const info = trackInfoMap.get(album.id);
if (!info) return;
if (!album.artist && !album.artists?.length && info.artist) {
album.artist = info.artist;
album.artists = [info.artist];
}
if (!album.cover && info.cover) {
album.cover = info.cover;
}
});
}""",
"ui.js: enrich albums missing artist/cover from tracks",
)
# ── cache: per-type TTL ──
patch(
"js/cache.js",
" this.ttl = options.ttl || 1000 * 60 * 30;",
""" this.ttl = options.ttl || 1000 * 60 * 30;
this.ttlByType = {
search_all: 1000 * 60 * 10,
search_tracks: 1000 * 60 * 10,
search_artists: 1000 * 60 * 60,
search_albums: 1000 * 60 * 30,
search_playlists: 1000 * 60 * 60,
search_videos: 1000 * 60 * 15,
};""",
"cache.js: per-type TTL map",
)
# ── cache: query normalization ──
patch(
"js/cache.js",
""" generateKey(type, params) {
const paramString = typeof params === 'object' ? JSON.stringify(params) : String(params);
return `${type}:${paramString}`;
}""",
""" generateKey(type, params) {
let normalized = params;
if (typeof params === 'string' && typeof type === 'string' && type.startsWith('search')) {
normalized = params
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\\u0300-\\u036f]/g, '');
}
const paramString = typeof normalized === 'object' ? JSON.stringify(normalized) : String(normalized);
return `${type}:${paramString}`;
}""",
"cache.js: query normalization (trim/lowercase/NFD)",
)
# ── cache: per-type TTL lookup in get() ──
patch(
"js/cache.js",
""" async get(type, params) {
const key = this.generateKey(type, params);
if (this.memoryCache.has(key)) {
const cached = this.memoryCache.get(key);
if (Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
this.memoryCache.delete(key);
}
if (this.db) {
try {
const cached = await this.getFromIndexedDB(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {""",
""" async get(type, params) {
const key = this.generateKey(type, params);
const effectiveTtl = (this.ttlByType && this.ttlByType[type]) || this.ttl;
if (this.memoryCache.has(key)) {
const cached = this.memoryCache.get(key);
if (Date.now() - cached.timestamp < effectiveTtl) {
return cached.data;
}
this.memoryCache.delete(key);
}
if (this.db) {
try {
const cached = await this.getFromIndexedDB(key);
if (cached && Date.now() - cached.timestamp < effectiveTtl) {""",
"cache.js: per-type TTL lookup in get()",
)
# ── api.js: search limit=100 ──
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?q=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?q=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: search() unified — limit=100")
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?s=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: searchTracks — limit=100")
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?al=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: searchAlbums — limit=100")
patch("js/api.js",
"const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}`, options);",
"const response = await this.fetchWithRetry(`/search/?p=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, options);",
"api.js: searchPlaylists — limit=100")
patch("js/api.js",
"""const response = await this.fetchWithRetry(`/search/?v=${encodeURIComponent(query)}`, {
...options,
});""",
"""const response = await this.fetchWithRetry(`/search/?v=${encodeURIComponent(query)}&limit=${(options && options.limit) || 100}`, {
...options,
});""",
"api.js: searchVideos — limit=100")
try:
with open(os.path.join(PROJECT_DIR, "js/api.js"), "r", encoding="utf-8") as f:
api_src = f.read()
new_src, n = re.subn(
r"(`/search/\?a=\$\{encodeURIComponent\(query\)\})(`)",
r"\1&limit=${(options && options.limit) || 100}\2",
api_src,
)
if n > 0:
with open(os.path.join(PROJECT_DIR, "js/api.js"), "w", encoding="utf-8") as f:
f.write(new_src)
print(" + api.js: searchArtists — limit=100")
else:
print(" ! api.js: searchArtists pattern not found, skipping")
except Exception as e:
print(" ! api.js: searchArtists patch failed: " + str(e))
# ── HiFi.ts: add artists.profileArt to unified search include ──
patch(
"js/HiFi.ts",
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,playlists,videos'",
"'albums,albums.coverArt,albums.artists,tracks,tracks.artists,tracks.albums,tracks.albums.coverArt,artists,artists.profileArt,playlists,videos'",
"HiFi.ts: add artists.profileArt to unified search include (fixes Picsum artist covers)",
)
# ── HiFi.ts: tracks.albums.coverArt on artist page — UPSTREAM MERGED (2026-08-16) ──
# Upstream now ships 'albums,albums.coverArt,tracks,tracks.albums,tracks.albums.coverArt,biography,profileArt'.
# ── Workbox: CacheFirst → NetworkOnly for audio/video ──
patch(
"vite.config.ts",
"handler: 'CacheFirst',\n options: {\n cacheName: 'media',",
"handler: 'NetworkOnly',\n options: {\n cacheName: 'media',",
"vite.config.ts: workbox audio/video CacheFirst -> NetworkOnly",
)
print(" ✓ All web patches applied.")
PYEOF
echo " ✓ Patches done."
# ── 5. Build ──
echo ""
echo "▶ Building web app..."
npx vite build 2>&1 | tail -3
echo " ✓ Build complete."
# ── 6. Package for upload ──
echo ""
echo "▶ Packaging dist/ for upload..."
# `zip -r` UPDATES an existing archive (stale entries from older builds survive) — start clean.
rm -f dist-web.zip
(cd dist && zip -r ../dist-web.zip . -x "*.map") 2>&1 | tail -3
SIZE=$(du -h "$PROJECT_DIR/dist-web.zip" | awk '{print $1}')
echo " ✓ dist-web.zip (${SIZE}) ready."
# ── 7. Deploy via rsync over SSH (if accessible) ──
echo ""
echo "▶ Attempting deploy to ${DEPLOY_HOST}:${DEPLOY_PATH}/..."
if rsync -az --no-perms --omit-dir-times \
-e "ssh -o ConnectTimeout=10 -o BatchMode=yes" \
dist/ "${DEPLOY_HOST}:~/${DEPLOY_PATH}/" 2>&1; then
echo " ✓ Deployed to ${DEPLOY_HOST}:~/${DEPLOY_PATH}/"
# assets/ is 100% build output (hashed chunks + public/assets) → safe to prune
# stale files from previous builds there. Root is left untouched (.htaccess, cgi-bin, …).
rsync -az --delete --no-perms --omit-dir-times \
-e "ssh -o ConnectTimeout=10 -o BatchMode=yes" \
dist/assets/ "${DEPLOY_HOST}:~/${DEPLOY_PATH}/assets/" 2>&1 \
&& echo " ✓ Pruned stale files in assets/"
echo ""
echo "══════════════════════════════════════════"
echo " Live at: https://${DEPLOY_PATH}/"
echo "══════════════════════════════════════════"
else
echo " ! rsync failed — use cPanel File Manager instead:"
echo ""
echo " 1. Open cPanel → File Manager → music.fabiodalez.it/"
echo " 2. Upload: $PROJECT_DIR/dist-web.zip"
echo " 3. Extract in place"
echo " 4. Delete dist-web.zip"
echo ""
echo "══════════════════════════════════════════"
echo " ZIP: $PROJECT_DIR/dist-web.zip (${SIZE})"
echo "══════════════════════════════════════════"
fi
# cleanup() runs automatically via trap EXIT