Compare commits
6
Commits
aa881ec039
...
2e4b91f89b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e4b91f89b | ||
|
|
76730b84cf | ||
|
|
5c035dc7f5 | ||
|
|
304cfb5be7 | ||
|
|
02abf8f88c | ||
|
|
ebd2ff131c |
No files matched your search
@@ -132,14 +132,10 @@ jobs:
|
|||||||
cd "${STAGE}"
|
cd "${STAGE}"
|
||||||
rm -f tsconfig.tsbuildinfo .tsbuildinfo
|
rm -f tsconfig.tsbuildinfo .tsbuildinfo
|
||||||
find . -maxdepth 3 -name '*.tsbuildinfo' -delete 2>/dev/null || true
|
find . -maxdepth 3 -name '*.tsbuildinfo' -delete 2>/dev/null || true
|
||||||
rm -rf .output dist .next .next/types .next/dev
|
rm -rf .output dist .next .next/types .next/dev .next/cache
|
||||||
|
|
||||||
# Restore build cache from last deploy so Turbopack can do
|
# No build-cache restore: every deploy is a fully clean, from-scratch
|
||||||
# incremental compilation (much faster rebuilds).
|
# build so the shipped output is 100% up to date with origin/main.
|
||||||
if [ -d "${LIVE}/.next/cache" ]; then
|
|
||||||
mkdir -p .next/cache
|
|
||||||
cp -r "${LIVE}/.next/cache/." .next/cache/
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stage shares MySQL with the live app + emulator. Keep the stage pool
|
# Stage shares MySQL with the live app + emulator. Keep the stage pool
|
||||||
# tiny so install/test/build cannot exhaust max_connections.
|
# tiny so install/test/build cannot exhaust max_connections.
|
||||||
|
|||||||
+9
-9
@@ -43,8 +43,8 @@
|
|||||||
"jpeg-js": "0.4.4",
|
"jpeg-js": "0.4.4",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lenis": "1.3.25",
|
"lenis": "1.3.26",
|
||||||
"lucide-react": "1.28.0",
|
"lucide-react": "1.30.0",
|
||||||
"lzma": "2.3.2",
|
"lzma": "2.3.2",
|
||||||
"motion": "12.43.0",
|
"motion": "12.43.0",
|
||||||
"music-metadata": "11.14.0",
|
"music-metadata": "11.14.0",
|
||||||
@@ -56,11 +56,11 @@
|
|||||||
"pino": "10.3.1",
|
"pino": "10.3.1",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-hook-form": "7.84.0",
|
"react-hook-form": "7.85.0",
|
||||||
"resend": "6.18.1",
|
"resend": "6.18.1",
|
||||||
"server-only": "0.0.1",
|
"server-only": "0.0.1",
|
||||||
"sharp": "0.35.3",
|
"sharp": "0.35.3",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.8",
|
||||||
"tailwind-merge": "3.6.0",
|
"tailwind-merge": "3.6.0",
|
||||||
"tailwindcss-animate": "1.0.7",
|
"tailwindcss-animate": "1.0.7",
|
||||||
"zod": "4.4.3"
|
"zod": "4.4.3"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.11",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@types/node": "^26.1.2",
|
"@types/node": "^26.2.0",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.4",
|
"@types/react-dom": "19.2.4",
|
||||||
"@vitest/coverage-v8": "4.1.10",
|
"@vitest/coverage-v8": "4.1.10",
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"drizzle-kit": "^0.31.10",
|
"drizzle-kit": "^0.31.10",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"knip": "6.31.0",
|
"knip": "6.32.0",
|
||||||
"lint-staged": "^17.3.0",
|
"lint-staged": "^17.3.0",
|
||||||
"pino-pretty": "^13.1.3",
|
"pino-pretty": "^13.1.3",
|
||||||
"postcss": "^8.5.25",
|
"postcss": "^8.5.26",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
"tsx": "^4.23.5",
|
"tsx": "^4.23.11",
|
||||||
"typescript": "^7.0.2",
|
"typescript": "^7.0.2",
|
||||||
"vite": "8.2.0",
|
"vite": "8.2.1",
|
||||||
"vitest": "4.1.10"
|
"vitest": "4.1.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+79
-80
@@ -36,7 +36,7 @@ importers:
|
|||||||
version: 3.2.2([email protected])
|
version: 3.2.2([email protected])
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: 5.7.1
|
specifier: 5.7.1
|
||||||
version: 5.7.1(@standard-schema/[email protected])([email protected]4.0([email protected]))([email protected])
|
version: 5.7.1(@standard-schema/[email protected])([email protected]5.0([email protected]))([email protected])
|
||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: 3.14.9
|
specifier: 3.14.9
|
||||||
version: 3.14.9([email protected]([email protected]))([email protected])
|
version: 3.14.9([email protected]([email protected]))([email protected])
|
||||||
@@ -57,7 +57,7 @@ importers:
|
|||||||
version: 3.4.13
|
version: 3.4.13
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: 0.45.2
|
specifier: 0.45.2
|
||||||
version: 0.45.2([email protected](@types/node@26.1.2))
|
version: 0.45.2([email protected](@types/node@26.2.0))
|
||||||
hash-wasm:
|
hash-wasm:
|
||||||
specifier: 4.12.0
|
specifier: 4.12.0
|
||||||
version: 4.12.0
|
version: 4.12.0
|
||||||
@@ -74,11 +74,11 @@ importers:
|
|||||||
specifier: 3.10.1
|
specifier: 3.10.1
|
||||||
version: 3.10.1
|
version: 3.10.1
|
||||||
lenis:
|
lenis:
|
||||||
specifier: 1.3.25
|
specifier: 1.3.26
|
||||||
version: 1.3.25([email protected])
|
version: 1.3.26([email protected])
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: 1.28.0
|
specifier: 1.30.0
|
||||||
version: 1.28.0([email protected])
|
version: 1.30.0([email protected])
|
||||||
lzma:
|
lzma:
|
||||||
specifier: 2.3.2
|
specifier: 2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
@@ -90,16 +90,16 @@ importers:
|
|||||||
version: 11.14.0([email protected])
|
version: 11.14.0([email protected])
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: 3.23.2
|
specifier: 3.23.2
|
||||||
version: 3.23.2(@types/node@26.1.2)
|
version: 3.23.2(@types/node@26.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 16.3.0
|
specifier: 16.3.0
|
||||||
version: 16.3.0(@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected])
|
version: 16.3.0(@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected])
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: 5.0.0-beta.32
|
specifier: 5.0.0-beta.32
|
||||||
version: 5.0.0-beta.32([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected])
|
version: 5.0.0-beta.32([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected])
|
||||||
next-intl:
|
next-intl:
|
||||||
specifier: 4.13.5
|
specifier: 4.13.5
|
||||||
version: 4.13.5([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected])
|
version: 4.13.5([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected])
|
||||||
otplib:
|
otplib:
|
||||||
specifier: 13.4.1
|
specifier: 13.4.1
|
||||||
version: 13.4.1
|
version: 13.4.1
|
||||||
@@ -113,8 +113,8 @@ importers:
|
|||||||
specifier: 19.2.8
|
specifier: 19.2.8
|
||||||
version: 19.2.8([email protected])
|
version: 19.2.8([email protected])
|
||||||
react-hook-form:
|
react-hook-form:
|
||||||
specifier: 7.84.0
|
specifier: 7.85.0
|
||||||
version: 7.84.0([email protected])
|
version: 7.85.0([email protected])
|
||||||
resend:
|
resend:
|
||||||
specifier: 6.18.1
|
specifier: 6.18.1
|
||||||
version: 6.18.1
|
version: 6.18.1
|
||||||
@@ -123,10 +123,10 @@ importers:
|
|||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
sharp:
|
sharp:
|
||||||
specifier: ^0.35.3
|
specifier: ^0.35.3
|
||||||
version: 0.35.3(@types/node@26.1.2)
|
version: 0.35.3(@types/node@26.2.0)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: 2.0.7
|
specifier: 2.0.8
|
||||||
version: 2.0.7([email protected]([email protected]))([email protected])
|
version: 2.0.8(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: 3.6.0
|
specifier: 3.6.0
|
||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
@@ -153,8 +153,8 @@ importers:
|
|||||||
specifier: ^0.5.20
|
specifier: ^0.5.20
|
||||||
version: 0.5.20([email protected])
|
version: 0.5.20([email protected])
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^26.1.2
|
specifier: ^26.2.0
|
||||||
version: 26.1.2
|
version: 26.2.0
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.18
|
specifier: 19.2.18
|
||||||
version: 19.2.18
|
version: 19.2.18
|
||||||
@@ -177,8 +177,8 @@ importers:
|
|||||||
specifier: ^9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
knip:
|
knip:
|
||||||
specifier: 6.31.0
|
specifier: 6.32.0
|
||||||
version: 6.31.0
|
version: 6.32.0
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: ^17.3.0
|
specifier: ^17.3.0
|
||||||
version: 17.3.0
|
version: 17.3.0
|
||||||
@@ -192,17 +192,17 @@ importers:
|
|||||||
specifier: ^4.3.3
|
specifier: ^4.3.3
|
||||||
version: 4.3.3
|
version: 4.3.3
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.23.5
|
specifier: ^4.23.11
|
||||||
version: 4.23.5
|
version: 4.23.11
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^7.0.2
|
specifier: ^7.0.2
|
||||||
version: 7.0.2
|
version: 7.0.2
|
||||||
vite:
|
vite:
|
||||||
specifier: 8.2.0
|
specifier: 8.2.1
|
||||||
version: 8.2.0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])
|
version: 8.2.1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected])
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 4.1.10
|
specifier: 4.1.10
|
||||||
version: 4.1.10(@types/node@26.1.2)(@vitest/[email protected])([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))
|
version: 4.1.10(@types/node@26.2.0)(@vitest/[email protected])([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected]))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1734,8 +1734,8 @@ packages:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
'@types/node@26.1.2':
|
'@types/node@26.2.0':
|
||||||
resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==}
|
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
|
resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==}
|
||||||
@@ -2215,9 +2215,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==}
|
resolution: {integrity: sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==}
|
||||||
|
|
||||||
@@ -2324,13 +2321,13 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
[email protected]1.0:
|
[email protected]2.0:
|
||||||
resolution: {integrity: sha512-NbeIEmUS2VUMjAkbiSNOKPJeV9wpCsr0660sUyKyMQbk4Iom0++nTLInVp4MJ+LfR4kORnw67bDi5tvO7YLnzA==}
|
resolution: {integrity: sha512-KDX9OmmOFmlvmxTkrx6Z0GHISMut+pXMSKR8eg84bovaxJKx2NdQD4JYCXveSbvieRe107W6vCD2xCpmz0qBYA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
[email protected]5:
|
[email protected]6:
|
||||||
resolution: {integrity: sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==}
|
resolution: {integrity: sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nuxt/kit': '>=3.0.0'
|
'@nuxt/kit': '>=3.0.0'
|
||||||
react: '>=17.0.0'
|
react: '>=17.0.0'
|
||||||
@@ -2506,8 +2503,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==}
|
resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==}
|
||||||
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'}
|
||||||
|
|
||||||
lucide-react@1.28.0:
|
lucide-react@1.30.0:
|
||||||
resolution: {integrity: sha512-fARAFJULsGuDDydjp6+6blekG/sBIM29TerzLjc9bQUKAcEfrSc4ZQKb25KRz4OMKd87cZTb5dgq0w/T6KufVg==}
|
resolution: {integrity: sha512-tUIr2jXLbWpCkdtH8XP7P7YppM9ueWgTky99lpWDY6z5REs6B+O6ZQ3U5tHkUUY59ANyOv/PBcs8E4Fe3KO3eA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
@@ -2733,8 +2730,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.8
|
react: ^19.2.8
|
||||||
|
|
||||||
[email protected]4.0:
|
[email protected]5.0:
|
||||||
resolution: {integrity: sha512-+hWvQP6GLco56mDwrbU4XnHix8t1z90ltZsDIrREl+jnQFQxYLX8oAzqe/Xn8nHpmoXTY5M6oEXrAhbP1qevNQ==}
|
resolution: {integrity: sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
@@ -2857,11 +2854,15 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
||||||
|
|
||||||
[email protected].7:
|
[email protected].8:
|
||||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
resolution: {integrity: sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
'@types/react': 19.2.18
|
||||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
@@ -2971,8 +2972,8 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
[email protected].5:
|
[email protected].11:
|
||||||
resolution: {integrity: sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==}
|
resolution: {integrity: sha512-Ry2oTEUnhBdeEdWIztY8kf3/nBGnPnjMLVGL0YfdRXMORuPER5NlKmayqxtxRxwB1xBN+RivRaJfe7PM1rtiyw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -3025,8 +3026,8 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
[email protected].0:
|
[email protected].1:
|
||||||
resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==}
|
resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3405,10 +3406,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/fast-memoize': 3.1.7
|
'@formatjs/fast-memoize': 3.1.7
|
||||||
|
|
||||||
'@hookform/[email protected](@standard-schema/[email protected])([email protected]4.0([email protected]))([email protected])':
|
'@hookform/[email protected](@standard-schema/[email protected])([email protected]5.0([email protected]))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.84.0([email protected])
|
react-hook-form: 7.85.0([email protected])
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
@@ -4163,7 +4164,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
'@types/node@26.1.2':
|
'@types/node@26.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 8.3.0
|
undici-types: 8.3.0
|
||||||
|
|
||||||
@@ -4250,7 +4251,7 @@ snapshots:
|
|||||||
obug: 2.1.4
|
obug: 2.1.4
|
||||||
std-env: 4.2.0
|
std-env: 4.2.0
|
||||||
tinyrainbow: 3.1.1
|
tinyrainbow: 3.1.1
|
||||||
vitest: 4.1.10(@types/node@26.1.2)(@vitest/[email protected])([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))
|
vitest: 4.1.10(@types/node@26.2.0)(@vitest/[email protected])([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected]))
|
||||||
|
|
||||||
'@vitest/[email protected]':
|
'@vitest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4261,13 +4262,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.1.1
|
tinyrainbow: 3.1.1
|
||||||
|
|
||||||
'@vitest/[email protected]([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))':
|
'@vitest/[email protected]([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.1.10
|
'@vitest/spy': 4.1.10
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.2.0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])
|
vite: 8.2.1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected])
|
||||||
|
|
||||||
'@vitest/[email protected]':
|
'@vitest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4392,11 +4393,11 @@ snapshots:
|
|||||||
'@drizzle-team/brocli': 0.10.2
|
'@drizzle-team/brocli': 0.10.2
|
||||||
'@esbuild-kit/esm-loader': 2.6.5
|
'@esbuild-kit/esm-loader': 2.6.5
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
tsx: 4.23.5
|
tsx: 4.23.11
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@26.1.2)):
|
[email protected]([email protected](@types/node@26.2.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
mysql2: 3.23.2(@types/node@26.1.2)
|
mysql2: 3.23.2(@types/node@26.2.0)
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -4493,10 +4494,6 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
dependencies:
|
|
||||||
resolve-pkg-maps: 1.0.0
|
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
@@ -4591,11 +4588,11 @@ snapshots:
|
|||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
setimmediate: 1.0.5
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
[email protected]1.0:
|
[email protected]2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0([email protected])
|
fdir: 6.5.0([email protected])
|
||||||
formatly: 0.3.0
|
formatly: 0.3.0
|
||||||
get-tsconfig: 4.14.0
|
get-tsconfig: 4.14.1
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
oxc-parser: 0.142.0
|
oxc-parser: 0.142.0
|
||||||
oxc-resolver: 11.24.2
|
oxc-resolver: 11.24.2
|
||||||
@@ -4607,7 +4604,7 @@ snapshots:
|
|||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
|
|
||||||
[email protected]5([email protected]):
|
[email protected]6([email protected]):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
@@ -4725,7 +4722,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
lucide-react@1.28.0([email protected]):
|
lucide-react@1.30.0([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
@@ -4784,9 +4781,9 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2):
|
[email protected](@types/node@26.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
aws-ssl-profiles: 1.1.2
|
aws-ssl-profiles: 1.1.2
|
||||||
denque: 2.1.0
|
denque: 2.1.0
|
||||||
generate-function: 2.3.1
|
generate-function: 2.3.1
|
||||||
@@ -4804,22 +4801,22 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected]):
|
[email protected]([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/core': 0.41.3
|
'@auth/core': 0.41.3
|
||||||
next: 16.3.0(@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected])
|
next: 16.3.0(@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected])
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected]):
|
[email protected]([email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.8.13
|
'@formatjs/intl-localematcher': 0.8.13
|
||||||
'@parcel/watcher': 2.6.0
|
'@parcel/watcher': 2.6.0
|
||||||
'@swc/core': 1.15.47
|
'@swc/core': 1.15.47
|
||||||
icu-minify: 4.13.5
|
icu-minify: 4.13.5
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
next: 16.3.0(@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected])
|
next: 16.3.0(@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected])
|
||||||
next-intl-swc-plugin-extractor: 4.13.5
|
next-intl-swc-plugin-extractor: 4.13.5
|
||||||
po-parser: 2.1.1
|
po-parser: 2.1.1
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
@@ -4829,7 +4826,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2)([email protected])([email protected]([email protected]))([email protected]):
|
[email protected](@types/node@26.2.0)([email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.3.0
|
'@next/env': 16.3.0
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -4849,7 +4846,7 @@ snapshots:
|
|||||||
'@next/swc-win32-arm64-msvc': 16.3.0
|
'@next/swc-win32-arm64-msvc': 16.3.0
|
||||||
'@next/swc-win32-x64-msvc': 16.3.0
|
'@next/swc-win32-x64-msvc': 16.3.0
|
||||||
babel-plugin-react-compiler: 1.0.0
|
babel-plugin-react-compiler: 1.0.0
|
||||||
sharp: 0.35.3(@types/node@26.1.2)
|
sharp: 0.35.3(@types/node@26.2.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
@@ -5006,7 +5003,7 @@ snapshots:
|
|||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
scheduler: 0.27.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
[email protected]4.0([email protected]):
|
[email protected]5.0([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
@@ -5100,7 +5097,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2):
|
[email protected](@types/node@26.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@img/colour': 1.1.0
|
'@img/colour': 1.1.0
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
@@ -5131,7 +5128,7 @@ snapshots:
|
|||||||
'@img/sharp-win32-arm64': 0.35.3
|
'@img/sharp-win32-arm64': 0.35.3
|
||||||
'@img/sharp-win32-ia32': 0.35.3
|
'@img/sharp-win32-ia32': 0.35.3
|
||||||
'@img/sharp-win32-x64': 0.35.3
|
'@img/sharp-win32-x64': 0.35.3
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -5147,10 +5144,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
atomic-sleep: 1.0.0
|
atomic-sleep: 1.0.0
|
||||||
|
|
||||||
[email protected].7([email protected]([email protected]))([email protected]):
|
[email protected].8(@types/[email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
react-dom: 19.2.8([email protected])
|
react-dom: 19.2.8([email protected])
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.18
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
@@ -5232,7 +5231,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected].5:
|
[email protected].11:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -5296,7 +5295,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]):
|
[email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.33.0
|
lightningcss: 1.33.0
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
@@ -5304,17 +5303,17 @@ snapshots:
|
|||||||
rolldown: 1.2.2
|
rolldown: 1.2.2
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
tsx: 4.23.5
|
tsx: 4.23.11
|
||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
|
|
||||||
[email protected](@types/node@26.1.2)(@vitest/[email protected])([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])):
|
[email protected](@types/node@26.2.0)(@vitest/[email protected])([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.10
|
'@vitest/expect': 4.1.10
|
||||||
'@vitest/mocker': 4.1.10([email protected].0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected]))
|
'@vitest/mocker': 4.1.10([email protected].1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected]))
|
||||||
'@vitest/pretty-format': 4.1.10
|
'@vitest/pretty-format': 4.1.10
|
||||||
'@vitest/runner': 4.1.10
|
'@vitest/runner': 4.1.10
|
||||||
'@vitest/snapshot': 4.1.10
|
'@vitest/snapshot': 4.1.10
|
||||||
@@ -5331,10 +5330,10 @@ snapshots:
|
|||||||
tinyexec: 1.3.0
|
tinyexec: 1.3.0
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
tinyrainbow: 3.1.1
|
tinyrainbow: 3.1.1
|
||||||
vite: 8.2.0(@types/node@26.1.2)([email protected])([email protected])([email protected].5)([email protected])
|
vite: 8.2.1(@types/node@26.2.0)([email protected])([email protected])([email protected].11)([email protected])
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.1.2
|
'@types/node': 26.2.0
|
||||||
'@vitest/coverage-v8': 4.1.10([email protected])
|
'@vitest/coverage-v8': 4.1.10([email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|||||||
@@ -277,82 +277,14 @@ export function autoPriceFurni(classname: string): {
|
|||||||
return { credits: 3, points: 0, pointsType: 0 };
|
return { credits: 3, points: 0, pointsType: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Allocate the next `catalog_items.id` and run the INSERT through a
|
|
||||||
* process-level async mutex so concurrent imports in the same Node process
|
|
||||||
* cannot race on MAX(id). A DB-level lock would require pinning a single
|
|
||||||
* connection for the whole operation; the in-process mutex is enough
|
|
||||||
* for the single-server deployment and avoids pool-connection gymnastics.
|
|
||||||
*/
|
|
||||||
let catalogNextId: number | null = null;
|
|
||||||
let catalogIdLastUsed = 0;
|
|
||||||
let catalogIdSeedChain: Promise<void> = Promise.resolve();
|
|
||||||
const CATALOG_ID_REFRESH_MS = 60_000;
|
|
||||||
|
|
||||||
// In-memory cache for catalog page IDs — avoids N+1 DB lookups during batch imports
|
|
||||||
// where many items belong to the same category.
|
|
||||||
let parentIdCache: number | null = null;
|
|
||||||
let categoryPageCache: Map<string, number> | null = null;
|
|
||||||
let categoryCacheTimestamp = 0;
|
|
||||||
const CATEGORY_CACHE_TTL_MS = 5 * 60_000; // 5 minutes
|
|
||||||
|
|
||||||
/** Drop the in-process catalog page cache. Call after bulk operations
|
|
||||||
* (e.g. the PUT re-organize route) to force a fresh DB lookup. */
|
|
||||||
export function resetCatalogPageCache(): void {
|
|
||||||
parentIdCache = null;
|
|
||||||
categoryPageCache = null;
|
|
||||||
categoryCacheTimestamp = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Allocate a unique catalog_items id. Seeded once with MAX(id)+1, then handed
|
|
||||||
* out from an in-process counter so concurrent imports don't serialize on a
|
|
||||||
* SELECT MAX + INSERT round-trip per item. Re-seeds when idle so rows added
|
|
||||||
* externally don't collide. */
|
|
||||||
export async function allocateCatalogItemId<T>(
|
|
||||||
insertFn: (nextId: number) => Promise<T>,
|
|
||||||
): Promise<T> {
|
|
||||||
const now = Date.now();
|
|
||||||
if (
|
|
||||||
catalogNextId === null ||
|
|
||||||
now - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
|
|
||||||
) {
|
|
||||||
let settle!: () => void;
|
|
||||||
const prev = catalogIdSeedChain;
|
|
||||||
catalogIdSeedChain = new Promise<void>((r) => {
|
|
||||||
settle = r;
|
|
||||||
});
|
|
||||||
await prev.catch(() => {});
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
catalogNextId === null ||
|
|
||||||
Date.now() - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
|
|
||||||
) {
|
|
||||||
const [maxIdResult] = (await db.execute(sql`
|
|
||||||
SELECT MAX(id) as maxId FROM catalog_items
|
|
||||||
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
|
|
||||||
catalogNextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
settle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catalogIdLastUsed = Date.now();
|
|
||||||
const nextId = catalogNextId;
|
|
||||||
catalogNextId += 1;
|
|
||||||
return await insertFn(nextId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Test seam: drop the cached id counter between tests. */
|
|
||||||
export function __resetCatalogIdAllocatorForTests(): void {
|
|
||||||
catalogNextId = null;
|
|
||||||
catalogIdLastUsed = 0;
|
|
||||||
catalogIdSeedChain = Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get or create a category sub-page under the imported parent page.
|
/** Get or create a category sub-page under the imported parent page.
|
||||||
* Caches category page IDs in-memory for CATEGORY_CACHE_TTL_MS to avoid
|
* Caches category page IDs in-memory for CATEGORY_CACHE_TTL_MS to avoid
|
||||||
* redundant DB lookups during batch imports where many items map to the same
|
* redundant DB lookups during batch imports where many items map to the same
|
||||||
* category. */
|
* category. */
|
||||||
|
let categoryPageCache: Map<string, number> | null = null;
|
||||||
|
let categoryCacheTimestamp = 0;
|
||||||
|
const CATEGORY_CACHE_TTL_MS = 5 * 60_000; // 5 minutes
|
||||||
|
|
||||||
export async function getOrCreateCategoryPage(
|
export async function getOrCreateCategoryPage(
|
||||||
classname: string,
|
classname: string,
|
||||||
itemType: string,
|
itemType: string,
|
||||||
@@ -414,6 +346,71 @@ export async function getOrCreateCategoryPage(
|
|||||||
return created.id;
|
return created.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetCatalogPageCache(): void {
|
||||||
|
parentIdCache = null;
|
||||||
|
categoryPageCache = null;
|
||||||
|
categoryCacheTimestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate the next `catalog_items.id` and run the INSERT through a
|
||||||
|
* process-level async mutex so concurrent imports in the same Node process
|
||||||
|
* cannot race on MAX(id). A DB-level lock would require pinning a single
|
||||||
|
* connection for the whole operation; the in-process mutex is enough
|
||||||
|
* for the single-server deployment and avoids pool-connection gymnastics.
|
||||||
|
*/
|
||||||
|
let catalogNextId: number | null = null;
|
||||||
|
let catalogIdLastUsed = 0;
|
||||||
|
let catalogIdSeedChain: Promise<void> = Promise.resolve();
|
||||||
|
const CATALOG_ID_REFRESH_MS = 60_000;
|
||||||
|
|
||||||
|
let parentIdCache: number | null = null;
|
||||||
|
|
||||||
|
/** Allocate a unique catalog_items id. Seeded once with MAX(id)+1, then handed
|
||||||
|
* out from an in-process counter so concurrent imports don't serialize on a
|
||||||
|
* SELECT MAX + INSERT round-trip per item. Re-seeds when idle so rows added
|
||||||
|
* externally don't collide. */
|
||||||
|
export async function allocateCatalogItemId<T>(
|
||||||
|
insertFn: (nextId: number) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
const now = Date.now();
|
||||||
|
if (
|
||||||
|
catalogNextId === null ||
|
||||||
|
now - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
|
||||||
|
) {
|
||||||
|
let settle!: () => void;
|
||||||
|
const prev = catalogIdSeedChain;
|
||||||
|
catalogIdSeedChain = new Promise<void>((r) => {
|
||||||
|
settle = r;
|
||||||
|
});
|
||||||
|
await prev.catch(() => {});
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
catalogNextId === null ||
|
||||||
|
Date.now() - catalogIdLastUsed > CATALOG_ID_REFRESH_MS
|
||||||
|
) {
|
||||||
|
const [maxIdResult] = (await db.execute(sql`
|
||||||
|
SELECT MAX(id) as maxId FROM catalog_items
|
||||||
|
`)) as unknown as [Array<{ maxId: number | bigint | null }>, unknown];
|
||||||
|
catalogNextId = Number(maxIdResult[0]?.maxId ?? 0) + 1;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
settle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catalogIdLastUsed = Date.now();
|
||||||
|
const nextId = catalogNextId;
|
||||||
|
catalogNextId += 1;
|
||||||
|
return await insertFn(nextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test seam: drop the cached id counter between tests. */
|
||||||
|
export function __resetCatalogIdAllocatorForTests(): void {
|
||||||
|
catalogNextId = null;
|
||||||
|
catalogIdLastUsed = 0;
|
||||||
|
catalogIdSeedChain = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Import Single Furni ─────────────────────────────────────────────
|
// ── Import Single Furni ─────────────────────────────────────────────
|
||||||
|
|
||||||
export async function importSingleFurni(params: {
|
export async function importSingleFurni(params: {
|
||||||
@@ -833,9 +830,6 @@ export async function importSingleFurni(params: {
|
|||||||
const categoryPageId = await getOrCreateCategoryPage(classname, itemType);
|
const categoryPageId = await getOrCreateCategoryPage(classname, itemType);
|
||||||
const pageIdStr = String(categoryPageId);
|
const pageIdStr = String(categoryPageId);
|
||||||
const price = autoPriceFurni(classname);
|
const price = autoPriceFurni(classname);
|
||||||
// catalog_items.id has no AUTO_INCREMENT — serialize ID allocation
|
|
||||||
// across concurrent imports via a MySQL named lock to avoid MAX(id)
|
|
||||||
// TOCTOU that would cause duplicate-key failures.
|
|
||||||
catalogItemId = await allocateCatalogItemId(async (nextCatalogId) => {
|
catalogItemId = await allocateCatalogItemId(async (nextCatalogId) => {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)
|
INSERT INTO catalog_items (id, page_id, item_ids, catalog_name, cost_credits, cost_points, points_type, amount, order_number, offer_id, extradata)
|
||||||
|
|||||||
+228
-307
@@ -3,330 +3,251 @@ import {
|
|||||||
contrastRatio,
|
contrastRatio,
|
||||||
deriveAdminPalette,
|
deriveAdminPalette,
|
||||||
derivePublicForegrounds,
|
derivePublicForegrounds,
|
||||||
|
ensureReadableThemeColors,
|
||||||
|
parseHex,
|
||||||
readableColor,
|
readableColor,
|
||||||
} from "@/lib/theme-contrast";
|
relativeLuminance,
|
||||||
import { PRESETS, THEME_COLOR_KEYS } from "@/lib/theme-presets";
|
softLightSurface,
|
||||||
|
} from "./theme-contrast";
|
||||||
|
|
||||||
describe("contrastRatio", () => {
|
describe("theme-contrast", () => {
|
||||||
it("calculates the WCAG ratio for black on white", () => {
|
describe("parseHex", () => {
|
||||||
expect(contrastRatio("#000000", "#ffffff")).toBe(21);
|
it("parses 6-digit hex", () => {
|
||||||
});
|
expect(parseHex("#ff0000")).toEqual([255, 0, 0]);
|
||||||
|
expect(parseHex("#00ff00")).toEqual([0, 255, 0]);
|
||||||
it("returns null for unsupported color formats", () => {
|
expect(parseHex("#0000ff")).toEqual([0, 0, 255]);
|
||||||
expect(contrastRatio("var(--color-text)", "#ffffff")).toBeNull();
|
expect(parseHex("#abcdef")).toEqual([171, 205, 239]);
|
||||||
});
|
});
|
||||||
});
|
it("parses 3-digit hex", () => {
|
||||||
|
expect(parseHex("#f00")).toEqual([255, 0, 0]);
|
||||||
describe("built-in theme presets", () => {
|
expect(parseHex("#0f0")).toEqual([0, 255, 0]);
|
||||||
it("defines every theme color in light and dark mode", () => {
|
expect(parseHex("#00f")).toEqual([0, 0, 255]);
|
||||||
for (const preset of Object.values(PRESETS)) {
|
});
|
||||||
expect(Object.keys(preset).sort()).toEqual(["dark", "light"]);
|
it("handles case insensitivity", () => {
|
||||||
for (const palette of [preset.light, preset.dark]) {
|
expect(parseHex("#FF0000")).toEqual([255, 0, 0]);
|
||||||
expect(Object.keys(palette).sort()).toEqual(
|
expect(parseHex("#F0F")).toEqual([255, 0, 255]);
|
||||||
[...THEME_COLOR_KEYS].sort(),
|
});
|
||||||
);
|
it("returns null for invalid formats", () => {
|
||||||
}
|
expect(parseHex("red")).toBeNull();
|
||||||
}
|
expect(parseHex("#gggggg")).toBeNull();
|
||||||
});
|
expect(parseHex("#12345")).toBeNull();
|
||||||
|
expect(parseHex("#1234567")).toBeNull();
|
||||||
it("derives WCAG-readable public foregrounds for every palette", () => {
|
expect(parseHex("")).toBeNull();
|
||||||
for (const preset of Object.values(PRESETS)) {
|
|
||||||
for (const palette of [preset.light, preset.dark]) {
|
|
||||||
const backgrounds = [palette.color_background, palette.color_surface];
|
|
||||||
const foregrounds = [
|
|
||||||
readableColor(palette.color_text, backgrounds),
|
|
||||||
readableColor(palette.color_text_muted, backgrounds),
|
|
||||||
readableColor(palette.color_primary, backgrounds),
|
|
||||||
readableColor(palette.color_accent, backgrounds),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const foreground of foregrounds) {
|
|
||||||
for (const background of backgrounds) {
|
|
||||||
expect(
|
|
||||||
contrastRatio(foreground, background),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonText = readableColor(palette.button_text_color, [
|
|
||||||
palette.button_primary_color,
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
contrastRatio(buttonText, palette.button_primary_color),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
|
|
||||||
const primaryForeground = readableColor(palette.button_text_color, [
|
|
||||||
palette.color_primary,
|
|
||||||
]);
|
|
||||||
expect(
|
|
||||||
contrastRatio(primaryForeground, palette.color_primary),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(["#22c55e", "#ef4444"])(
|
|
||||||
"derives readable text for default button %s",
|
|
||||||
(background) => {
|
|
||||||
const foreground = readableColor("#ffffff", [background]);
|
|
||||||
expect(contrastRatio(foreground, background)).toBeGreaterThanOrEqual(4.5);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("readableColor", () => {
|
|
||||||
it("keeps the preferred color when it passes on every background", () => {
|
|
||||||
expect(readableColor("#0f172a", ["#ffffff", "#f8fafc"], 4.5)).toBe(
|
|
||||||
"#0f172a",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replaces low-contrast golden text with a readable neutral", () => {
|
|
||||||
const color = readableColor("#f59e0b", ["#ffffff", "#f8fafc"], 4.5);
|
|
||||||
|
|
||||||
expect(color).toBe("#000000");
|
|
||||||
expect(contrastRatio(color, "#ffffff")).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(contrastRatio(color, "#f8fafc")).toBeGreaterThanOrEqual(4.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("chooses white text for a dark button when configured text fails", () => {
|
|
||||||
expect(readableColor("#777777", ["#111827"], 4.5)).toBe("#ffffff");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves unsupported custom colors instead of corrupting CSS", () => {
|
|
||||||
expect(readableColor("rgb(1, 2, 3)", ["#ffffff"], 4.5)).toBe(
|
|
||||||
"rgb(1, 2, 3)",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("deriveAdminPalette", () => {
|
|
||||||
it.each(Object.entries(PRESETS))(
|
|
||||||
"keeps admin text readable for %s",
|
|
||||||
(_name, preset) => {
|
|
||||||
for (const palette of [preset.light, preset.dark]) {
|
|
||||||
const admin = deriveAdminPalette(palette);
|
|
||||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(
|
|
||||||
4.5,
|
|
||||||
);
|
|
||||||
expect(contrastRatio(admin.text, admin.surface)).toBeGreaterThanOrEqual(
|
|
||||||
4.5,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
contrastRatio(admin.muted, admin.surface),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(
|
|
||||||
contrastRatio(admin.accentForeground, admin.accent),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("does not copy hostile public structural colors into admin surfaces", () => {
|
|
||||||
const admin = deriveAdminPalette({
|
|
||||||
color_background: "#ff00ff",
|
|
||||||
color_surface: "#00ff00",
|
|
||||||
color_primary: "#ffff00",
|
|
||||||
});
|
});
|
||||||
expect(admin.canvas).not.toBe("#ff00ff");
|
|
||||||
expect(admin.surface).not.toBe("#00ff00");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("in light mode softens harsh white public surfaces", () => {
|
describe("relativeLuminance", () => {
|
||||||
const admin = deriveAdminPalette(
|
it("calculates luminance for known colors", () => {
|
||||||
{
|
// White
|
||||||
|
expect(relativeLuminance([255, 255, 255])).toBeCloseTo(1.0);
|
||||||
|
// Black
|
||||||
|
expect(relativeLuminance([0, 0, 0])).toBeCloseTo(0.0);
|
||||||
|
// Mid-gray
|
||||||
|
expect(relativeLuminance([128, 128, 128])).toBeCloseTo(0.2159, 3);
|
||||||
|
});
|
||||||
|
it("applies sRGB gamma correction", () => {
|
||||||
|
// Value exactly at threshold 0.04045
|
||||||
|
const thresholdChannel = Math.round(0.04045 * 255);
|
||||||
|
const val = relativeLuminance([
|
||||||
|
thresholdChannel,
|
||||||
|
thresholdChannel,
|
||||||
|
thresholdChannel,
|
||||||
|
]);
|
||||||
|
expect(val).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("contrastRatio", () => {
|
||||||
|
it("returns null for unparseable colors", () => {
|
||||||
|
expect(contrastRatio("invalid", "#fff")).toBeNull();
|
||||||
|
expect(contrastRatio("#fff", "invalid")).toBeNull();
|
||||||
|
});
|
||||||
|
it("calculates correct ratios", () => {
|
||||||
|
// Black on white = 21:1
|
||||||
|
expect(contrastRatio("#000000", "#ffffff")).toBeCloseTo(21, 0);
|
||||||
|
// White on black = 21:1
|
||||||
|
expect(contrastRatio("#ffffff", "#000000")).toBeCloseTo(21, 0);
|
||||||
|
// Gray on white ~4.5:1
|
||||||
|
expect(contrastRatio("#767676", "#ffffff")).toBeCloseTo(4.5, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readableColor", () => {
|
||||||
|
it("returns preferred when ratio is sufficient", () => {
|
||||||
|
expect(readableColor("#000000", ["#ffffff"])).toBe("#000000");
|
||||||
|
expect(readableColor("#ffffff", ["#000000"])).toBe("#ffffff");
|
||||||
|
});
|
||||||
|
it("picks best candidate when preferred fails", () => {
|
||||||
|
// Gray on light gray fails 4.5:1, should pick black
|
||||||
|
expect(readableColor("#999999", ["#cccccc"])).toBe("#000000");
|
||||||
|
});
|
||||||
|
it("returns preferred when background is unparseable", () => {
|
||||||
|
expect(readableColor("#999999", ["invalid"])).toBe("#999999");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("softLightSurface", () => {
|
||||||
|
it("returns default for white", () => {
|
||||||
|
expect(softLightSurface("#fff", "#eef1f4")).toBe("#eef1f4");
|
||||||
|
expect(softLightSurface("#ffffff", "#eef1f4")).toBe("#eef1f4");
|
||||||
|
expect(softLightSurface("white", "#eef1f4")).toBe("#eef1f4");
|
||||||
|
});
|
||||||
|
it("returns default for very light grays", () => {
|
||||||
|
expect(softLightSurface("#f8fafc", "#eef1f4")).toBe("#eef1f4");
|
||||||
|
expect(softLightSurface("#fafafa", "#eef1f4")).toBe("#eef1f4");
|
||||||
|
});
|
||||||
|
it("returns original for non-white colors", () => {
|
||||||
|
expect(softLightSurface("#1e293b", "#eef1f4")).toBe("#1e293b");
|
||||||
|
expect(softLightSurface("#3b82f6", "#eef1f4")).toBe("#3b82f6");
|
||||||
|
});
|
||||||
|
it("handles undefined/empty", () => {
|
||||||
|
expect(softLightSurface(undefined, "#eef1f4")).toBe("#eef1f4");
|
||||||
|
expect(softLightSurface("", "#eef1f4")).toBe("#eef1f4");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deriveAdminPalette", () => {
|
||||||
|
const baseColors = {
|
||||||
|
color_primary: "#f59e0b",
|
||||||
|
color_background: "#0b0f19",
|
||||||
|
color_surface: "#151b27",
|
||||||
|
color_dropdown: "#1e2635",
|
||||||
|
color_text: "#f8fafc",
|
||||||
|
color_text_muted: "#94a3b8",
|
||||||
|
color_success: "#22c55e",
|
||||||
|
color_warning: "#eab308",
|
||||||
|
color_error: "#ef4444",
|
||||||
|
color_info: "#3b82f6",
|
||||||
|
color_navbar: "#080c18",
|
||||||
|
color_navbar_text: "#f8fafc",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("returns dark defaults when no overrides", () => {
|
||||||
|
const p = deriveAdminPalette(baseColors);
|
||||||
|
expect(p.canvas).toBe("#0b0f19");
|
||||||
|
expect(p.accent).toBe("#f59e0b");
|
||||||
|
expect(p.text).toBe("#f8fafc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies overrides in dark mode", () => {
|
||||||
|
const p = deriveAdminPalette(baseColors, { canvas: "#111111" }, "dark");
|
||||||
|
expect(p.canvas).toBe("#111111");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores overrides in light mode", () => {
|
||||||
|
const p = deriveAdminPalette(baseColors, { canvas: "#111111" }, "light");
|
||||||
|
expect(p.canvas).not.toBe("#111111");
|
||||||
|
expect(p.canvas).toMatch(/^#[0-9a-f]{6}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses light defaults when mode=light", () => {
|
||||||
|
const lightColors = {
|
||||||
|
...baseColors,
|
||||||
color_background: "#f8fafc",
|
color_background: "#f8fafc",
|
||||||
color_surface: "#ffffff",
|
color_surface: "#ffffff",
|
||||||
color_dropdown: "#f1f5f9",
|
color_dropdown: "#f1f5f9",
|
||||||
color_text: "#0f172a",
|
color_text: "#0f172a",
|
||||||
color_text_muted: "#64748b",
|
color_text_muted: "#64748b",
|
||||||
color_primary: "#f59e0b",
|
color_navbar: "#eef1f4",
|
||||||
color_navbar: "#ffffff",
|
color_navbar_text: "#0f172a",
|
||||||
color_navbar_text: "#1e293b",
|
};
|
||||||
},
|
const p = deriveAdminPalette(lightColors, {}, "light");
|
||||||
{
|
expect(p.canvas).toBe("#eef1f4"); // softLightSurface kicks in
|
||||||
canvas: "#0b0f19",
|
expect(p.text).toBe("#0f172a");
|
||||||
surface: "#151b27",
|
|
||||||
text: "#f8fafc",
|
|
||||||
},
|
|
||||||
"light",
|
|
||||||
);
|
|
||||||
expect(admin.canvas).toBe("#eef1f4");
|
|
||||||
expect(admin.surface).toBe("#f5f7fa");
|
|
||||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(
|
|
||||||
contrastRatio(admin.sidebarText, admin.sidebar),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores dark admin overrides while in light mode", () => {
|
|
||||||
const admin = deriveAdminPalette(
|
|
||||||
{
|
|
||||||
color_background: "#ffffff",
|
|
||||||
color_surface: "#ffffff",
|
|
||||||
color_text: "#0f172a",
|
|
||||||
color_primary: "#f59e0b",
|
|
||||||
},
|
|
||||||
{ canvas: "#0b0f19", text: "#f8fafc" },
|
|
||||||
"light",
|
|
||||||
);
|
|
||||||
expect(admin.canvas).toBe("#eef1f4");
|
|
||||||
expect(admin.text.toLowerCase()).not.toBe("#f8fafc");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("repairs default light text when admin canvas is forced light", () => {
|
|
||||||
const admin = deriveAdminPalette(
|
|
||||||
{ color_primary: "#f59e0b" },
|
|
||||||
{
|
|
||||||
canvas: "#ffffff",
|
|
||||||
surface: "#f8fafc",
|
|
||||||
surfaceElevated: "#f1f5f9",
|
|
||||||
sidebar: "#ffffff",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(contrastRatio(admin.text, admin.surface)).toBeGreaterThanOrEqual(
|
|
||||||
4.5,
|
|
||||||
);
|
|
||||||
expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual(
|
|
||||||
4.5,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
contrastRatio(admin.sidebarText, admin.sidebar),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(
|
|
||||||
contrastRatio(admin.accentForeground, admin.accent),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("repairs hostile admin text overrides on dark canvas", () => {
|
|
||||||
const admin = deriveAdminPalette(
|
|
||||||
{ color_primary: "#111827" },
|
|
||||||
{
|
|
||||||
canvas: "#0b0f19",
|
|
||||||
surface: "#151b27",
|
|
||||||
text: "#0b0f19",
|
|
||||||
muted: "#111827",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(contrastRatio(admin.muted, admin.surface)).toBeGreaterThanOrEqual(
|
|
||||||
4.5,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
contrastRatio(admin.accentForeground, admin.accent),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ensureReadableThemeColors", () => {
|
|
||||||
it("rewrites saved text colors that fail against their surfaces", async () => {
|
|
||||||
const { ensureReadableThemeColors } = await import("@/lib/theme-contrast");
|
|
||||||
const fixed = ensureReadableThemeColors({
|
|
||||||
color_background: "#0b0f19",
|
|
||||||
color_surface: "#151b27",
|
|
||||||
color_text: "#0b0f19",
|
|
||||||
color_text_muted: "#111827",
|
|
||||||
color_navbar: "#ffffff",
|
|
||||||
color_navbar_text: "#ffffff",
|
|
||||||
button_primary_color: "#f59e0b",
|
|
||||||
button_text_color: "#f59e0b",
|
|
||||||
admin_canvas: "#ffffff",
|
|
||||||
admin_surface: "#f8fafc",
|
|
||||||
admin_text: "#f8fafc",
|
|
||||||
admin_text_muted: "#e2e8f0",
|
|
||||||
});
|
});
|
||||||
expect(
|
|
||||||
contrastRatio(fixed.color_text, fixed.color_surface),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(
|
|
||||||
contrastRatio(fixed.color_navbar_text, fixed.color_navbar),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(
|
|
||||||
contrastRatio(fixed.button_text_color, fixed.button_primary_color),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
expect(
|
|
||||||
contrastRatio(fixed.admin_text, fixed.admin_canvas),
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("derivePublicForegrounds", () => {
|
it("handles missing color keys gracefully", () => {
|
||||||
it.each(Object.entries(PRESETS))(
|
const p = deriveAdminPalette({ color_primary: "#f00" });
|
||||||
"makes every semantic pair readable for %s",
|
expect(p.accent).toBe("#f00");
|
||||||
(_name, preset) => {
|
});
|
||||||
for (const palette of [preset.light, preset.dark]) {
|
|
||||||
const result = derivePublicForegrounds(palette);
|
|
||||||
for (const pair of result.pairs) {
|
|
||||||
expect(
|
|
||||||
contrastRatio(pair.foreground, pair.background),
|
|
||||||
pair.name,
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("covers admin hierarchy and every semantic status", () => {
|
|
||||||
const required = [
|
|
||||||
"subtle",
|
|
||||||
"disabled",
|
|
||||||
"sidebar",
|
|
||||||
"input",
|
|
||||||
"success-text",
|
|
||||||
"success-solid",
|
|
||||||
"warning-text",
|
|
||||||
"warning-solid",
|
|
||||||
"error-text",
|
|
||||||
"error-solid",
|
|
||||||
"info-text",
|
|
||||||
"info-solid",
|
|
||||||
];
|
|
||||||
for (const preset of Object.values(PRESETS)) {
|
|
||||||
for (const palette of [preset.light, preset.dark]) {
|
|
||||||
const result = derivePublicForegrounds(palette);
|
|
||||||
for (const name of required) {
|
|
||||||
const pair = result.pairs.find(
|
|
||||||
(candidate) => candidate.name === name,
|
|
||||||
);
|
|
||||||
expect(pair, `missing ${name}`).toBeDefined();
|
|
||||||
const safePair = pair as NonNullable<typeof pair>;
|
|
||||||
expect(
|
|
||||||
contrastRatio(safePair.foreground, safePair.background),
|
|
||||||
name,
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("repairs hostile white-on-white custom colors", () => {
|
describe("derivePublicForegrounds", () => {
|
||||||
const result = derivePublicForegrounds({
|
const baseColors = {
|
||||||
color_background: "#ffffff",
|
color_background: "#f8fafc",
|
||||||
color_surface: "#ffffff",
|
color_surface: "#ffffff",
|
||||||
color_dropdown: "#ffffff",
|
color_dropdown: "#f1f5f9",
|
||||||
color_navbar: "#ffffff",
|
color_navbar: "#f1f5f9",
|
||||||
color_navbar_text: "#ffffff",
|
color_text: "#0f172a",
|
||||||
color_text: "#ffffff",
|
color_text_muted: "#64748b",
|
||||||
color_text_muted: "#ffffff",
|
color_navbar_text: "#1e293b",
|
||||||
color_primary: "#ffffff",
|
color_primary: "#f59e0b",
|
||||||
color_accent: "#ffffff",
|
color_accent: "#10b981",
|
||||||
button_primary_color: "#ffffff",
|
color_success: "#22c55e",
|
||||||
button_text_color: "#ffffff",
|
color_warning: "#eab308",
|
||||||
button_secondary_color: "#ffffff",
|
color_error: "#ef4444",
|
||||||
|
color_info: "#3b82f6",
|
||||||
|
link_color: "#f59e0b",
|
||||||
|
link_hover_color: "#d97706",
|
||||||
|
button_text_color: "#1e293b",
|
||||||
|
button_primary_color: "#f59e0b",
|
||||||
button_secondary_text_color: "#ffffff",
|
button_secondary_text_color: "#ffffff",
|
||||||
button_danger_color: "#ffffff",
|
button_secondary_color: "#22c55e",
|
||||||
button_danger_text_color: "#ffffff",
|
button_danger_text_color: "#ffffff",
|
||||||
button_outline_color: "#ffffff",
|
button_danger_color: "#ef4444",
|
||||||
button_outline_text_color: "#ffffff",
|
button_outline_text_color: "#1a1a2e",
|
||||||
link_color: "#ffffff",
|
};
|
||||||
link_hover_color: "#ffffff",
|
|
||||||
|
it("returns pairs with readable contrasts", () => {
|
||||||
|
const result = derivePublicForegrounds(baseColors);
|
||||||
|
expect(result.pairs.length).toBeGreaterThan(0);
|
||||||
|
expect(result.byName.body).toBeDefined();
|
||||||
|
expect(result.byName.primary).toBeDefined();
|
||||||
|
expect(result.byName.accent).toBe("#000000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing keys with defaults", () => {
|
||||||
|
const result = derivePublicForegrounds({});
|
||||||
|
expect(result.pairs.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates status pairs", () => {
|
||||||
|
const result = derivePublicForegrounds(baseColors);
|
||||||
|
expect(result.byName["success-text"]).toBeDefined();
|
||||||
|
expect(result.byName["success-solid"]).toBeDefined();
|
||||||
|
expect(result.byName["warning-text"]).toBeDefined();
|
||||||
|
expect(result.byName["error-solid"]).toBeDefined();
|
||||||
|
expect(result.byName["info-solid"]).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ensureReadableThemeColors", () => {
|
||||||
|
it("fixes low-contrast foregrounds", () => {
|
||||||
|
const colors = {
|
||||||
|
color_text: "#999999", // too light on white
|
||||||
|
color_surface: "#ffffff",
|
||||||
|
color_background: "#f8fafc",
|
||||||
|
color_navbar_text: "#999999",
|
||||||
|
color_navbar: "#ffffff",
|
||||||
|
button_text_color: "#999999",
|
||||||
|
button_primary_color: "#3b82f6",
|
||||||
|
button_secondary_text_color: "#999999",
|
||||||
|
button_secondary_color: "#22c55e",
|
||||||
|
button_danger_text_color: "#999999",
|
||||||
|
button_danger_color: "#ef4444",
|
||||||
|
button_outline_text_color: "#999999",
|
||||||
|
link_color: "#999999",
|
||||||
|
link_hover_color: "#999999",
|
||||||
|
};
|
||||||
|
const fixed = ensureReadableThemeColors(colors);
|
||||||
|
// All should be fixed to black or white
|
||||||
|
expect(fixed.color_text).toMatch(/^#(000000|ffffff)$/);
|
||||||
|
expect(fixed.color_navbar_text).toMatch(/^#(000000|ffffff)$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves already-readable colors", () => {
|
||||||
|
const colors = {
|
||||||
|
color_text: "#0f172a",
|
||||||
|
color_surface: "#ffffff",
|
||||||
|
color_background: "#f8fafc",
|
||||||
|
};
|
||||||
|
const fixed = ensureReadableThemeColors(colors);
|
||||||
|
expect(fixed.color_text).toBe("#0f172a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles missing keys", () => {
|
||||||
|
const fixed = ensureReadableThemeColors({});
|
||||||
|
expect(fixed).toEqual({});
|
||||||
});
|
});
|
||||||
expect(result.pairs.length).toBeGreaterThan(10);
|
|
||||||
for (const pair of result.pairs)
|
|
||||||
expect(
|
|
||||||
contrastRatio(pair.foreground, pair.background),
|
|
||||||
pair.name,
|
|
||||||
).toBeGreaterThanOrEqual(4.5);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ export interface AdminPalette {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Pure / near-white public surfaces → soft admin paper (less eye strain). */
|
/** Pure / near-white public surfaces → soft admin paper (less eye strain). */
|
||||||
function softLightSurface(
|
export function softLightSurface(
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
softDefault: string,
|
softDefault: string,
|
||||||
): string {
|
): string {
|
||||||
@@ -262,7 +262,7 @@ export function ensureReadableThemeColors(
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHex(value: string): Rgb | null {
|
export function parseHex(value: string): Rgb | null {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
const short = /^#([0-9a-f]{3})$/i.exec(trimmed);
|
const short = /^#([0-9a-f]{3})$/i.exec(trimmed);
|
||||||
if (short) {
|
if (short) {
|
||||||
@@ -282,7 +282,7 @@ function parseHex(value: string): Rgb | null {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function relativeLuminance(rgb: Rgb): number {
|
export function relativeLuminance(rgb: Rgb): number {
|
||||||
const [red, green, blue] = rgb.map((channel) => {
|
const [red, green, blue] = rgb.map((channel) => {
|
||||||
const value = channel / 255;
|
const value = channel / 255;
|
||||||
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
||||||
|
|||||||
+83
-18
@@ -1323,24 +1323,81 @@ do_permissions() {
|
|||||||
fix_perms
|
fix_perms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Wait for the emulator service to be active and its game port to be listening.
|
||||||
|
wait_for_emulator_health() {
|
||||||
|
local hc=0
|
||||||
|
while [ "$hc" -lt "$HEALTH_RETRIES" ]; do
|
||||||
|
if service_active "$EMULATOR_SERVICE" && emulator_port_open; then
|
||||||
|
ok "Emulator health check passed (service up + port listening)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
hc=$((hc+1))
|
||||||
|
[ "$hc" -lt "$HEALTH_RETRIES" ] && sleep "$HEALTH_INTERVAL"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve the newest built emulator jar filename inside the target directory.
|
||||||
|
emulator_jar_name() {
|
||||||
|
find "$EMULATOR_MODULE/target" -maxdepth 1 -name 'Polaris-*-jar-with-dependencies.jar' \
|
||||||
|
-printf '%T@ %p\n' 2>/dev/null | sort -rn | sed -n '1s/^[0-9.]* //p' | xargs -r basename 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the emulator's built-in Flyway repair against the database. This realigns
|
||||||
|
# flyway_schema_history with the packaged migrations (e.g. clearing "applied
|
||||||
|
# migration not resolved locally" drift) without touching hotel data.
|
||||||
|
flyaway_repair_db() {
|
||||||
|
local jar; jar=$(emulator_jar_name)
|
||||||
|
if [ -z "$jar" ]; then
|
||||||
|
fail "No emulator jar found in %s" "$EMULATOR_MODULE/target"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
command -v java >/dev/null 2>&1 || { fail "Missing: java"; return 1; }
|
||||||
|
cd "$EMULATOR_MODULE/target"
|
||||||
|
spinner_start "Running Flyway repair..."
|
||||||
|
if timeout 300 java -Dfile.encoding=UTF8 -jar "$jar" --migrations=repair >> "$LOG_FILE" 2>&1; then
|
||||||
|
spinner_stop ok
|
||||||
|
ok "Flyway schema history repaired (see: %s)" "$LOG_FILE"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
spinner_stop fail
|
||||||
|
tail -30 "$LOG_FILE" | grep -iE 'repair|migrat|error|exception' || true
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
do_restart() {
|
do_restart() {
|
||||||
step 8 8 "Restart Services"
|
step 8 8 "Restart Services"
|
||||||
if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
|
if systemctl cat "$EMULATOR_SERVICE" &>/dev/null; then
|
||||||
sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" || warn "Restart failed"
|
sudo systemctl restart "$EMULATOR_SERVICE" 2>/dev/null && ok "$EMULATOR_SERVICE restarted" || warn "Restart failed"
|
||||||
local hc=0
|
if ! wait_for_emulator_health; then
|
||||||
while [ "$hc" -lt "$HEALTH_RETRIES" ]; do
|
|
||||||
if service_active "$EMULATOR_SERVICE" && emulator_port_open; then
|
|
||||||
ok "Emulator health check passed (service up + port listening)"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
hc=$((hc+1))
|
|
||||||
[ "$hc" -lt "$HEALTH_RETRIES" ] && sleep "$HEALTH_INTERVAL"
|
|
||||||
done
|
|
||||||
if [ "$hc" -eq "$HEALTH_RETRIES" ]; then
|
|
||||||
warn "Emulator not healthy after %s retries" "$HEALTH_RETRIES"
|
warn "Emulator not healthy after %s retries" "$HEALTH_RETRIES"
|
||||||
sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 20 2>/dev/null || true
|
sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 20 2>/dev/null || true
|
||||||
ERRORS=$((ERRORS + 1))
|
# Flyway schema drift (an applied migration no longer resolvable
|
||||||
ROLLBACK_NEEDED=true
|
# locally) aborts startup. Repair the history table and retry once
|
||||||
|
# before giving up, so updates survive upstream migration renames.
|
||||||
|
local flyway_drift=false
|
||||||
|
if sudo journalctl -u "$EMULATOR_SERVICE" --no-pager -n 300 2>/dev/null \
|
||||||
|
| grep -qiE 'applied migration not resolved locally|FlywayValidateException|Migrations have failed validation'; then
|
||||||
|
flyway_drift=true
|
||||||
|
warn "Flyway schema drift detected — running repair and retrying"
|
||||||
|
# Halt the restart loop so repair cannot race migrate on the history table
|
||||||
|
sudo systemctl stop "$EMULATOR_SERVICE" 2>/dev/null || true
|
||||||
|
if flyaway_repair_db; then
|
||||||
|
sudo systemctl start "$EMULATOR_SERVICE" 2>/dev/null || true
|
||||||
|
if wait_for_emulator_health; then
|
||||||
|
ok "Emulator healthy after Flyway repair"
|
||||||
|
flyway_drift=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
sudo systemctl start "$EMULATOR_SERVICE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "$flyway_drift" = true ]; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
ROLLBACK_NEEDED=true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
elif command -v pm2 &>/dev/null; then
|
elif command -v pm2 &>/dev/null; then
|
||||||
pm2 restart all 2>/dev/null && ok "PM2 restarted" || warn "PM2 restart failed"
|
pm2 restart all 2>/dev/null && ok "PM2 restarted" || warn "PM2 restart failed"
|
||||||
@@ -1648,10 +1705,10 @@ cmd_flyaway_repair() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
step 1 6 "System Diagnostics"
|
step 1 7 "System Diagnostics"
|
||||||
preflight_check
|
preflight_check
|
||||||
|
|
||||||
step 2 6 "$(_t "Flyaway Repair")"
|
step 2 7 "$(_t "Flyaway Repair")"
|
||||||
local repaired=0
|
local repaired=0
|
||||||
|
|
||||||
for repo in "$EMULATOR_REPO" "$NITRO_CLIENT" "$NITRO_RENDERER"; do
|
for repo in "$EMULATOR_REPO" "$NITRO_CLIENT" "$NITRO_RENDERER"; do
|
||||||
@@ -1691,18 +1748,26 @@ cmd_flyaway_repair() {
|
|||||||
repaired=$((repaired + 1))
|
repaired=$((repaired + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Realign Flyway schema history with the packaged migrations
|
||||||
|
step 3 7 "$(_t "Flyway DB Repair")"
|
||||||
|
if flyaway_repair_db; then
|
||||||
|
ok "$(_t "Flyway schema history repaired")"
|
||||||
|
else
|
||||||
|
fail "$(_t "Flyway DB repair failed — the emulator will retry on restart")"
|
||||||
|
fi
|
||||||
|
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
step 3 6 "$(_t "Set Permissions")"
|
step 4 7 "$(_t "Set Permissions")"
|
||||||
fix_perms
|
fix_perms
|
||||||
ok "Permissions fixed"
|
ok "Permissions fixed"
|
||||||
|
|
||||||
# Sync / validate configs
|
# Sync / validate configs
|
||||||
step 4 6 "$(_t "Sync Configurations")"
|
step 5 7 "$(_t "Sync Configurations")"
|
||||||
sync_configs
|
sync_configs
|
||||||
|
|
||||||
# Rebuild frontend if dist is missing or empty
|
# Rebuild frontend if dist is missing or empty
|
||||||
if [ ! -d "$NITRO_CLIENT/dist" ] || [ -z "$(ls -A "$NITRO_CLIENT/dist" 2>/dev/null)" ]; then
|
if [ ! -d "$NITRO_CLIENT/dist" ] || [ -z "$(ls -A "$NITRO_CLIENT/dist" 2>/dev/null)" ]; then
|
||||||
step 5 6 "$(_t "Rebuild Frontend")"
|
step 6 7 "$(_t "Rebuild Frontend")"
|
||||||
spinner_start "$(_t "Building frontend...")"
|
spinner_start "$(_t "Building frontend...")"
|
||||||
cd "$NITRO_CLIENT"
|
cd "$NITRO_CLIENT"
|
||||||
if yarn build >> "$LOG_FILE" 2>&1; then
|
if yarn build >> "$LOG_FILE" 2>&1; then
|
||||||
@@ -1719,7 +1784,7 @@ cmd_flyaway_repair() {
|
|||||||
|
|
||||||
# Rebuild emulator if jar is missing
|
# Rebuild emulator if jar is missing
|
||||||
if [ ! -d "$EMULATOR_MODULE/target" ] || ! ls "$EMULATOR_MODULE"/target/Polaris-*-jar-with-dependencies.jar &>/dev/null 2>&1; then
|
if [ ! -d "$EMULATOR_MODULE/target" ] || ! ls "$EMULATOR_MODULE"/target/Polaris-*-jar-with-dependencies.jar &>/dev/null 2>&1; then
|
||||||
step 6 6 "$(_t "Rebuild Emulator")"
|
step 7 7 "$(_t "Rebuild Emulator")"
|
||||||
cd "$EMULATOR_MODULE"
|
cd "$EMULATOR_MODULE"
|
||||||
backup_binaries
|
backup_binaries
|
||||||
ROLLBACK_NEEDED=true
|
ROLLBACK_NEEDED=true
|
||||||
|
|||||||
+4
-4
@@ -24,10 +24,10 @@ export default defineConfig({
|
|||||||
provider: "v8",
|
provider: "v8",
|
||||||
reporter: ["text", "lcov", "html"],
|
reporter: ["text", "lcov", "html"],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
statements: 7,
|
statements: 10,
|
||||||
branches: 5,
|
branches: 7,
|
||||||
functions: 6,
|
functions: 9,
|
||||||
lines: 7,
|
lines: 10,
|
||||||
},
|
},
|
||||||
include: ["src/**/*.{ts,tsx}"],
|
include: ["src/**/*.{ts,tsx}"],
|
||||||
exclude: [
|
exclude: [
|
||||||
|
|||||||
Reference in new issue
Block a user