feat(branding): implement dynamic branding in service worker and reports (#383)
* feat(branding): implement dynamic branding in service worker and reports * refactor(service-worker): remove push notification handling code * feat(service-worker): implement dynamic branding in service worker and related scripts
This commit is contained in:
@@ -51,6 +51,10 @@ supabase/.temp/
|
||||
# swarm audit reports (generated by /swarm)
|
||||
.swarm/
|
||||
|
||||
# generated service worker (built from public/sw.template.js by
|
||||
# scripts/inject-public-branding.mjs — runs via predev/prebuild)
|
||||
/public/sw.js
|
||||
|
||||
# dev docs (internal reference, not published)
|
||||
/dev_docs
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ ENV NEXT_PUBLIC_APP_URL=__NEXT_PUBLIC_APP_URL__
|
||||
ENV NEXT_PUBLIC_VAPID_PUBLIC_KEY=__NEXT_PUBLIC_VAPID_PUBLIC_KEY__
|
||||
ENV NEXT_PUBLIC_SELF_HOSTED=__NEXT_PUBLIC_SELF_HOSTED__
|
||||
ENV NEXT_PUBLIC_REQUIRE_MFA=__NEXT_PUBLIC_REQUIRE_MFA__
|
||||
# Keep the branding placeholder intact through prebuild's inject script so
|
||||
# docker-entrypoint.sh can substitute the runtime value into public/sw.js.
|
||||
ENV NEXT_PUBLIC_BRANDING_APP_NAME=__NEXT_PUBLIC_BRANDING_APP_NAME__
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
|
||||
+12
-1
@@ -53,6 +53,17 @@ Resolution order (last wins): **defaults → env vars → extension override**.
|
||||
|
||||
`NEXT_PUBLIC_*` env vars are inlined at build time. Changing them requires a fresh `npm run build` to propagate.
|
||||
|
||||
`NEXT_PUBLIC_BRANDING_APP_NAME` also stamps the service worker push-notification fallback title in `public/sw.js`. This happens at build time for Vercel/local builds (via `scripts/inject-public-branding.mjs`, run from `prebuild`) and at container start for Docker (via `docker-entrypoint.sh`).
|
||||
|
||||
### Email / Resend (when `email` or `invoice-inbox` extensions are enabled)
|
||||
|
||||
| Env var | Purpose |
|
||||
|---|---|
|
||||
| `RESEND_API_KEY` | Resend API key — required for both outbound mail and the inbox webhook |
|
||||
| `RESEND_FROM_EMAIL` | Default `From` address (e.g. `noreply@your-brand.se`); also used as the address you From-spoof through Resend |
|
||||
| `RESEND_INBOUND_DOMAIN` | Domain used to compose per-company invoice-inbox addresses: `{local-part}@{RESEND_INBOUND_DOMAIN}` |
|
||||
| `RESEND_INBOUND_WEBHOOK_SECRET` | Verifies the `/inbound` webhook signature from Resend |
|
||||
|
||||
## Things you MUST NOT change
|
||||
|
||||
These are stable contracts. Renaming them breaks existing data, sessions, or external clients (npm package consumers, MCP connectors, browser sessions, invite links). Leave them alone in your fork:
|
||||
@@ -75,9 +86,9 @@ A few things that look brand-related but are configured elsewhere:
|
||||
- **Resend sending domain** — verify `noreply@your-brand.se` (or wherever) in Resend, set `RESEND_FROM_EMAIL`.
|
||||
- **DNS / domain** — point `app.your-brand.se` at your Vercel deployment.
|
||||
- **OAuth redirect allowlist for MCP** — `app/api/mcp-oauth/authorize/route.ts` lists `claude.ai/api/*`, `claude.com/api/*`, and localhost. Your domain is the OAuth issuer, not a redirect target — no change needed unless you're integrating with new MCP clients.
|
||||
- **Service worker push notification fallback title** (`public/sw.js`) — currently hardcoded as `'Ekonomi'`. Service workers can't read env vars at runtime; change the file directly in your fork if it matters.
|
||||
- **iCal feed PRODID** (`lib/calendar/ics-generator.ts`) — defaults to `erp-base.se`, callers may pass their domain.
|
||||
- **`NEXT_PUBLIC_APP_URL`** — used as the OAuth issuer. Set this to your domain (e.g. `https://app.your-brand.se`).
|
||||
- **Skatteverket submission identity** — `extensions/general/skatteverket/lib/api-client.ts` does not set a custom `User-Agent`; submissions go out with the Node/Vercel runtime default. If your deployment needs to identify itself to Skatteverket under a different brand, that's a future enhancement (env var + header), not something the current branding service covers.
|
||||
|
||||
## Staying in sync with upstream
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ export async function GET(request: Request) {
|
||||
fiscal_period_id: periodId,
|
||||
company_name: company.company_name || 'Unknown',
|
||||
org_number: company.org_number,
|
||||
program_name: 'ERPBase',
|
||||
})
|
||||
|
||||
// Return as downloadable file
|
||||
|
||||
@@ -40,4 +40,13 @@ if [ -d /app/.next/static ]; then
|
||||
{} +
|
||||
fi
|
||||
|
||||
# Stamp the service worker fallback notification title with the brand name.
|
||||
# public/sw.js is served as a static file (not bundled by Next), so NEXT_PUBLIC_*
|
||||
# inlining doesn't reach it — substitute the placeholder here at container start.
|
||||
if [ -f /app/public/sw.js ]; then
|
||||
sed -i \
|
||||
-e "s|__NEXT_PUBLIC_BRANDING_APP_NAME__|${NEXT_PUBLIC_BRANDING_APP_NAME:-Gnubok}|g" \
|
||||
/app/public/sw.js
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -118,7 +118,6 @@ export async function generateFullArchive(
|
||||
company_name: company.company_name || 'Unknown',
|
||||
trade_name: company.trade_name,
|
||||
org_number: company.org_number,
|
||||
program_name: 'ERPBase',
|
||||
})
|
||||
sieFolder.file(`${periodLabel(period)}.se`, sie)
|
||||
|
||||
@@ -135,7 +134,6 @@ export async function generateFullArchive(
|
||||
company_name: company.company_name || 'Unknown',
|
||||
trade_name: company.trade_name,
|
||||
org_number: company.org_number,
|
||||
program_name: 'ERPBase',
|
||||
})
|
||||
zip.file('bokforing.se', sie)
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { NEDeclaration, SRUFile, SRURecord } from '@/lib/reports/ne-bilaga/types'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
|
||||
function sanitizeString(str: string): string {
|
||||
return str.replace(/#/g, '').replace(/[\r\n]/g, ' ').substring(0, 250)
|
||||
}
|
||||
|
||||
/**
|
||||
* SRU File Generator
|
||||
@@ -54,7 +59,7 @@ export function generateSRUFile(declaration: NEDeclaration): SRUFile {
|
||||
// File header
|
||||
records.push({ fieldCode: 'PRODUKT', value: 'KONTROLLUPPGIFTER' })
|
||||
records.push({ fieldCode: 'SESSION', value: '1' })
|
||||
records.push({ fieldCode: 'PROGRAMNAMN', value: 'ERPBase' })
|
||||
records.push({ fieldCode: 'PROGRAMNAMN', value: sanitizeString(getBranding().appName) })
|
||||
records.push({ fieldCode: 'PROGRAMVERSION', value: '1.0' })
|
||||
records.push({
|
||||
fieldCode: 'SKAPAT',
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
||||
import { getBranding } from '@/lib/branding/service'
|
||||
import type { SIEExportOptions, JournalEntry, JournalEntryLine, BASAccount } from '@/types'
|
||||
|
||||
function sanitizeProgramName(str: string): string {
|
||||
return str.replace(/"/g, '').replace(/[\r\n]/g, ' ').substring(0, 60)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SIE4 export file
|
||||
*
|
||||
@@ -81,7 +86,8 @@ export async function generateSIEExport(
|
||||
lines.push('#FLAGGA 0')
|
||||
lines.push('#FORMAT PC8')
|
||||
lines.push('#SIETYP 4')
|
||||
lines.push(`#PROGRAM "${options.program_name || 'ERPBase'}" "1.0"`)
|
||||
const programName = sanitizeProgramName(options.program_name || getBranding().appName)
|
||||
lines.push(`#PROGRAM "${programName}" "1.0"`)
|
||||
lines.push(`#GEN ${formatSIEDate(now)}`)
|
||||
|
||||
if (options.org_number) {
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"setup:extensions": "npx tsx scripts/generate-extension-registry.ts",
|
||||
"predev": "npm run setup:extensions",
|
||||
"predev": "npm run setup:extensions && node scripts/inject-public-branding.mjs",
|
||||
"dev": "next dev",
|
||||
"prebuild": "npm run setup:extensions",
|
||||
"prebuild": "npm run setup:extensions && node scripts/inject-public-branding.mjs",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Service Worker for Push Notifications
|
||||
* Handles incoming push notifications and notification click events
|
||||
*
|
||||
* NOTE: This is the source template. The deployed file at public/sw.js is
|
||||
* generated by scripts/inject-public-branding.mjs (runs via predev/prebuild)
|
||||
* and is gitignored. Edit this template, not the generated file.
|
||||
*/
|
||||
|
||||
// Handle push events
|
||||
@@ -32,7 +36,7 @@ self.addEventListener('push', (event) => {
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title || 'Ekonomi', options)
|
||||
self.registration.showNotification(title || '__NEXT_PUBLIC_BRANDING_APP_NAME__', options)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Stamps public/sw.js from public/sw.template.js with the runtime brand name.
|
||||
*
|
||||
* The service worker is served as a static file (Next.js does not bundle
|
||||
* public/), so NEXT_PUBLIC_* inlining doesn't reach it. We generate sw.js
|
||||
* from a template at build time (Vercel/local) so the deployed file shows
|
||||
* the configured brand name.
|
||||
*
|
||||
* Docker uses a different strategy: the builder stage exports
|
||||
* NEXT_PUBLIC_BRANDING_APP_NAME=__NEXT_PUBLIC_BRANDING_APP_NAME__ so the
|
||||
* placeholder survives the build, and docker-entrypoint.sh substitutes the
|
||||
* runtime value via sed at container start.
|
||||
*
|
||||
* public/sw.js is gitignored — the source of truth is the template.
|
||||
*/
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const TEMPLATE_PATH = join(process.cwd(), 'public', 'sw.template.js')
|
||||
const OUTPUT_PATH = join(process.cwd(), 'public', 'sw.js')
|
||||
const PLACEHOLDER = '__NEXT_PUBLIC_BRANDING_APP_NAME__'
|
||||
const value = process.env.NEXT_PUBLIC_BRANDING_APP_NAME || 'Gnubok'
|
||||
|
||||
if (!existsSync(TEMPLATE_PATH)) {
|
||||
console.log(`[inject-public-branding] ${TEMPLATE_PATH} not found, skipping`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const template = readFileSync(TEMPLATE_PATH, 'utf8')
|
||||
const output = template.split(PLACEHOLDER).join(value)
|
||||
writeFileSync(OUTPUT_PATH, output)
|
||||
console.log(`[inject-public-branding] generated public/sw.js with brand "${value}"`)
|
||||
Reference in New Issue
Block a user