diff --git a/TODO.md b/TODO.md index f934127..658bba0 100644 --- a/TODO.md +++ b/TODO.md @@ -18,8 +18,8 @@ ## Present to Boss (2/7/2026) [x] discuss logo baru MyKOPKB [x] layout footer untuk surat rasmi -[ ] maklumat pekerjaan kena buat dropdown untuk nama majikan -[ ] tambah minima untuk borang permohonan (tukar jadi caruman saham potongan bulanan) +[x] maklumat pekerjaan kena buat dropdown untuk nama majikan +[x] tambah minima untuk borang permohonan (tukar jadi caruman saham potongan bulanan) [x] tukar waris ke penama [x] ic dan surat pengesahan majikkan perlu mandatori [ ] semakan lembaga tukar jadi maklumat keputusan @@ -27,5 +27,5 @@ [x] surat attach dekat email selepas bos [ ] pembayaran pertama tukar jadi one-off [ ] syer maksima silap tukar jadi rm50.00 -[ ] letak sign digital +[x] letak sign digital [x] running no anggota dalam surat dan masa. \ No newline at end of file diff --git a/be/.env.example b/be/.env.example index f0a4c9c..cc40028 100644 --- a/be/.env.example +++ b/be/.env.example @@ -67,11 +67,12 @@ AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" # Browsershot (PDF generation via headless Chrome + Puppeteer in be/) -# Required in Docker/Sail. After first sail up: sail npm install && sail npx puppeteer browsers install chrome-headless-shell +# Local/Sail: sail npm install && sail npx puppeteer browsers install chrome-headless-shell +# Production Docker image bundles Node + Puppeteer; keep NO_SANDBOX enabled in containers. BROWSERSHOT_NO_SANDBOX=true -# BROWSERSHOT_NODE_BINARY= +# BROWSERSHOT_NODE_BINARY=/usr/local/bin/node # BROWSERSHOT_NPM_BINARY= -# BROWSERSHOT_NODE_MODULE_PATH= +# BROWSERSHOT_NODE_MODULE_PATH=/var/www # BROWSERSHOT_CHROME_PATH= # BROWSERSHOT_TIMEOUT=60 diff --git a/be/.env.production b/be/.env.production index 80a9c56..d0ac0a3 100644 --- a/be/.env.production +++ b/be/.env.production @@ -105,4 +105,5 @@ API_VALID_KEYS=86f58825e5c1e0872d8786092d47e3bd,6461fc5390660c55dc47bdfcd85ed9ae API_LOG_KEY_USAGE=true PUBLIC_PROFILE_TOKEN_TTL_DAYS=7 -FRONTEND_URL=https://mykopkb.koppkb.com \ No newline at end of file +FRONTEND_URL=https://mykopkb.koppkb.com +BROWSERSHOT_NO_SANDBOX=true \ No newline at end of file diff --git a/be/config/mail.php b/be/config/mail.php index 3b53d7f..8341d09 100644 --- a/be/config/mail.php +++ b/be/config/mail.php @@ -124,6 +124,6 @@ return [ | */ - 'logo_path' => public_path('images/logo/logo.svg'), + 'logo_path' => public_path('images/logo/logo.png'), ]; diff --git a/be/docker/common/unified/Dockerfile b/be/docker/common/unified/Dockerfile index d6a5f7c..49c5063 100644 --- a/be/docker/common/unified/Dockerfile +++ b/be/docker/common/unified/Dockerfile @@ -46,6 +46,19 @@ COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer RUN composer install --no-dev --optimize-autoloader --no-interaction --no-progress --prefer-dist --no-scripts \ && rm -rf bootstrap/cache/services.php bootstrap/cache/packages.php +# Stage 1b: Node + Puppeteer for Browsershot PDF generation +FROM node:22-bookworm-slim AS node-deps + +WORKDIR /var/www + +COPY be/package.json be/package-lock.json ./ + +RUN npm ci --omit=dev + +ENV PUPPETEER_CACHE_DIR=/var/www/.cache/puppeteer + +RUN npx puppeteer browsers install chrome-headless-shell + # Stage 2: Unified production image with PHP-FPM + Nginx FROM php:8.4-fpm AS base @@ -79,6 +92,37 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && docker-php-ext-enable redis \ && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +# Chromium runtime libraries for Puppeteer (Browsershot PDF generation) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + fonts-liberation \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libdrm2 \ + libexpat1 \ + libfontconfig1 \ + libgbm1 \ + libglib2.0-0 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxkbcommon0 \ + libxrandr2 \ + libxrender1 \ + && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + # Download and install php-fpm health check script RUN curl -o /usr/local/bin/php-fpm-healthcheck \ https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck \ @@ -120,11 +164,20 @@ RUN echo '[www]' > /usr/local/etc/php-fpm.d/zzz-status.conf && \ # Copy the application code and dependencies from the build stage first COPY --from=builder /var/www /var/www +# Node runtime + Puppeteer for Browsershot (letter PDF generation) +COPY --from=node-deps /usr/local/bin/node /usr/local/bin/node +COPY --from=node-deps /var/www/node_modules /var/www/node_modules +COPY --from=node-deps /var/www/.cache/puppeteer /var/www/.cache/puppeteer + +ENV PUPPETEER_CACHE_DIR=/var/www/.cache/puppeteer \ + BROWSERSHOT_NO_SANDBOX=true \ + BROWSERSHOT_NODE_BINARY=/usr/local/bin/node + # Set working directory WORKDIR /var/www # Ensure correct permissions -RUN chown -R www-data:www-data /var/www +RUN chown -R www-data:www-data /var/www /var/www/.cache/puppeteer # Create Nginx log directory RUN mkdir -p /var/log/nginx && chown -R www-data:www-data /var/log/nginx diff --git a/be/public/images/logo/logo.png b/be/public/images/logo/logo.png new file mode 100644 index 0000000..bcfe625 Binary files /dev/null and b/be/public/images/logo/logo.png differ diff --git a/fe/src/modules/profile/utils/member-digital-card.utils.ts b/fe/src/modules/profile/utils/member-digital-card.utils.ts index 81f23ed..46e28ff 100644 --- a/fe/src/modules/profile/utils/member-digital-card.utils.ts +++ b/fe/src/modules/profile/utils/member-digital-card.utils.ts @@ -14,14 +14,22 @@ export function buildCardDownloadFileName( export function toProxiedStorageUrl(url: string | null | undefined): string | null | undefined { if (!url) return url - try { - const parsed = new URL(url, window.location.origin) - if (parsed.pathname.startsWith('/storage/')) { - return `${parsed.pathname}${parsed.search}` - } - } catch { - // Keep original URL when parsing fails. - } + const apiBase = import.meta.env.VITE_API_BASE_URL?.replace(/\/$/, '') - return url + try { + const parsed = new URL(url, apiBase || window.location.origin) + if (!parsed.pathname.startsWith('/storage/')) { + return url + } + + // Production serves the SPA from a different host than Laravel storage files. + if (apiBase) { + return `${apiBase}${parsed.pathname}${parsed.search}` + } + + // Dev fallback: Vite proxies /storage to the API. + return `${parsed.pathname}${parsed.search}` + } catch { + return url + } }