Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Digitale kundeprislister for eventfoto. Chen Media setter opp én fast lenke per kunde med forhåndsavtalte pakker, tillegg og vilkår. Kunden ser prisene og sender en strukturert forespørsel fra samme side. Erstatter PDF-en og den løse e-postprosessen. Status: beta / første fungerende utkast. Ikke produksjonsklar (se «Før drift»). Next.js 15.5 (App Router) · React 19 · TypeScript 5.9 · Tailwind CSS 4 · Prisma 6.19 + Postgres (Supabase) · Zod 3 · Vitest 3 · Playwright 1.63 · Node 22. Versjonene er låst (package.json + package-lock.json).查看完整 README ↗
Digitale kundeprislister for eventfoto. Chen Media setter opp én fast lenke per kunde med forhåndsavtalte pakker, tillegg og vilkår. Kunden ser prisene og sender en strukturert forespørsel fra samme side. Erstatter PDF-en og den løse e-postprosessen.
Status: beta / første fungerende utkast. Ikke produksjonsklar (se «Før drift»).
Next.js 15.5 (App Router) · React 19 · TypeScript 5.9 · Tailwind CSS 4 · Prisma 6.19 + Postgres (Supabase) ·
Zod 3 · Vitest 3 · Playwright 1.63 · Node 22. Versjonene er låst (package.json + package-lock.json).
Design følger Chen Media Brandguideline V1: Chen Svart #111111, Chen Krem #FBF8D0, hvit, én
aksent (terrakotta #C25A2E, bare på knapper, understreker og statusprikker). Helvetica (versaler)
til titler, Open Sans til brødtekst, Merriweather til ingress, Space Mono til etiketter. Fontene
(unntatt Helvetica, som er systemfont) hentes lokalt via @fontsource, ingen Google-kall.
Logo og krusedull (public/brand/) er klippet ut av brandguiden som PNG. Bytt gjerne med
originale vektorfiler.
Trenger en Postgres-database. Har du Docker: docker run -d -p 5433:5432 -e POSTGRES_HOST_AUTH_METHOD=trust postgres:16
og createdb -h localhost -p 5433 -U postgres kundepriser_dev (og tilsvarende kundepriser_test, kundepriser_e2e
for testene). Annen Postgres går også: sett DATABASE_URL / DIRECT_URL i .env.
npm install
cp .env.example .env # sett ADMIN_PASSWORD (min. 12 tegn) og gjerne APP_SECRET
npm run db:setup # kjører migrasjoner og seeder OBOS (+ administrator)
npm run dev # http://localhost:3000
ADMIN_EMAIL / ADMIN_PASSWORD)./k/<hemmelig token>. Tokenet skrives aldri til terminal eller logg.npm run admin:create (spør etter e-post/passord).example.com) legges inn når SEED_DEMO_INQUIRIES=1.@media print i globals.css) som følger nettsiden: kremfarget side, hvite avrundede kort, pakkene side om side, svart «Praktisk»-flate og svart bunnfelt. Knapper, dialog, galleri og pakkebilder skjules (heltebildet beholdes), og standardinnholdet holder seg til én side. Bakgrunnsfarger skrives ut selv om «Bakgrunnsgrafikk» er av. Utskrift fra admin-forhåndsvisning og arkiv skjuler admin-skallet. Filnavnet blir «Chen Media - Prisliste - .pdf» (sidetittelen byttes bare mens utskriftsdialogen er åpen). Tips: slå av «Topptekster og bunntekster» i utskriftsdialogen, ellers skriver nettleseren lenken (med token) i bunnen av PDF-en.STORAGE_DIR. Med SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY
satt brukes Supabase Storage i stedet (samme kode som i drift)./admin/foresporsler/eksport, semikolon + UTF-8 for norsk Excel, celler som starter med = + - @ får apostrof mot formelinjeksjon). Statuser: Ny, Under oppfølging, Avklart, Booket, Tapt, Avsluttet.| Variabel | Formål |
|---|---|
DATABASE_URL | Postgres. I drift: Supabase pooler-adressen (port 6543) med ?pgbouncer=true&connection_limit=1. |
DIRECT_URL | Direkte Postgres-adresse (port 5432). Brukes til migrasjoner. |
APP_SECRET | Krypterer kundelenker som lagres for «Kopier lenke». Påkrevd i produksjon (min. 16 tegn). Byttes den, kan gamle lenker ikke vises (generer ny lenke). |
APP_URL | Offentlig adresse, brukes til å bygge kundelenker. Mangler den på Vercel i produksjon, brukes prosjektets produksjonsadresse (VERCEL_PROJECT_PRODUCTION_URL). |
CRON_SECRET | Minst 16 tegn. Vercel sender den som Authorization: Bearer … til /api/cron/*. Uten den avvises alle cron-kall, og feilede e-poster prøves ikke på nytt automatisk. Marker som Sensitive. |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_STORAGE_BUCKET | Bildelagring i Supabase Storage (privat bucket, standard kundeportal-media). Service-nøkkelen brukes bare på serveren og må aldri eksponeres i nettleseren. |
SUPABASE_PUBLISHABLE_KEY (eller SUPABASE_ANON_KEY) | Offentlig nøkkel som trengs for administratorinnlogging via Supabase Auth. Mangler den, faller innloggingen tilbake til lokal passord-hash. |
INQUIRY_RETENTION_MONTHS, EMAIL_BODY_RETENTION_DAYS | Oppbevaringsfrister, se «Personvern og oppbevaring». Tomt = ingen automatisk sletting. |
STORAGE_DIR | Kun lokal utvikling uten Supabase. Virker ikke på Vercel. |
ADMIN_EMAIL, ADMIN_PASSWORD | Brukes av seed / admin:create. Ingen hardkodet passord finnes. |
EMAIL_PROVIDER, RESEND_API_KEY, EMAIL_FROM | Aktiverer ekte e-post via Resend. Tomt = lokal utboks. |
HUBSPOT_* | Overføring av forespørsler til HubSpot, se «HubSpot». Tomt = av. |
NOTIFY_EMAIL | Mottaker av varsel om nye forespørsler (faller ellers tilbake til kontakt-e-posten på kundesiden). |
Vercel har ingen varig disk, derfor brukes Supabase for data (Postgres) og bilder (Storage). Appen
er klargjort (vercel.json, region cdg1 (Paris) nær Supabase-prosjektet i eu-west-3), men ikke deployet.
DATABASE_URL=<direct> DIRECT_URL=<direct> npm run db:migrate. Kjør deretter
supabase/setup-storage.sql i SQL Editor (oppretter den private bucketen).
Migrasjonen slår på Row Level Security uten policies på alle tabeller, slik at Supabase' åpne
Data API ikke kan lese kundedata, tokens eller passordhasher med anon-nøkkelen.
Tips: deler prosjektet database med andre apper, bruk et eget skjema (?schema=kundepriser).POSTGRES_PRISMA_URL/POSTGRES_URL
hvis DATABASE_URL mangler, og SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY leveres av integrasjonen.
Det du selv må sette er APP_SECRET (Sensitive) og APP_URL. For migrasjoner lokalt bruker du
POSTGRES_URL_NON_POOLING som DIRECT_URL. Full liste: DATABASE_URL (pooler), DIRECT_URL, APP_SECRET,
APP_URL (den offentlige adressen), SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY,
SUPABASE_STORAGE_BUCKET, NOTIFY_EMAIL. Marker nøkler og passord som Sensitive.noindex og no-store.SUPABASE_URL og en publisert/anon-nøkkel finnes) sjekkes
passordet hos Supabase Auth. Hvem som er administrator bestemmes av tabellen AdminUser
(godkjenningsliste): en Supabase-bruker uten rad der får ingen tilgang. Legg til en administrator ved å
opprette brukeren i Supabase (Authentication → Users) og kjøre
DATABASE_URL=<direct> DIRECT_URL=<direct> ADMIN_EMAIL=... npm run admin:create (eller sette inn raden i
AdminUser). Slå av offentlig registrering i Supabase (Authentication → Sign In / Providers → «Allow new
users to sign up»). Passordendring og tilbakestilling gjøres i Supabase-dashbordet. Appen har ingen egen
glemt-passord-side, og Supabase' innebygde e-post er begrenset til testing.
Uten Supabase-oppsett (lokal utvikling) brukes passordhash i som før.
Seed () legger inn OBOS. Kjør den bare hvis du vil ha demo-kunden i produksjon.Bilder lastes opp direkte fra nettleseren til Supabase Storage med en kortlevd signert URL (Vercel tillater bare ca. 4,5 MB gjennom en funksjon). Serveren validerer filformatet etterpå og sletter ugyldige filer. Kunder får bildet via en tilgangskontrollert rute som videresender til en signert URL på 60 sekunder.
/admin/kunder/[id]/versjoner).src/server/email.ts, Resend støttes). Uten leverandør
havner e-postene i E-postutboksen (/admin/utboks, kun innlogget), merket
«Lokal forhåndsvisning – ikke sendt». Feilede utsendinger kan prøves på nytt fra forespørselen, og en
daglig cron (/api/cron/email-retry, se vercel.json) prøver feilede jobber på nytt (opptil fem forsøk
i tre døgn). Hver utsending har tidsavbrudd (8 s) og idempotensnøkkel mot Resend, og en jobb tas atomisk
slik at samme e-post ikke sendes to ganger. Antall feilede e-poster vises som rødt merke i menyen.
Kundens adresse er Reply-To, aldri avsender. Kvittering til kunden sendes maks tre ganger i døgnet per
adresse (teamet varsles uansett), slik at skjemaet ikke kan brukes til å sende tekst til vilkårlige adresser.Referrer-Policy: no-referrer,
Cache-Control: private, no-store og noindex. Ingen analyse eller sporing.npm run typecheck # tsc
npm run lint # eslint
npm test # Vitest (Postgres *_test-database): tilgangskontroll, versjoner, prisøyeblikk,
# doble innsendinger, e-post, bildevalidering og lagringsadapter
npm run build # produksjonsbygg
# E2E (Playwright). Bruker databasen *_e2e og port 3100:
CHROMIUM_PATH=/sti/til/chromium npx playwright test # utelat CHROMIUM_PATH hvis `npx playwright install chromium` er kjørt
Skjemaet på kundesiden er portalens eget (ikke et HubSpot-skjema), fordi portalen kjenner kunden, låser prisen kunden så og beskytter mot utdaterte versjoner. Etter innsending overføres forespørselen server-til-server til HubSpot:
SELSKAP // ARRANGEMENT - EVENTPHOTO - DD/MM/ÅÅÅÅ (EVENTFILM hvis pakken nevner film/video, «dato ikke avklart» uten dato)src/lib/crm.ts, terskler øverst i filen) og begrunnes i beskrivelsen. Poeng for
budsjett (≥ 30 000 kr: +2, ≥ 10 000 kr: +1, lavere: −1, ukjent: 0) og for kunden (≥ 3 vunne deals eller LTV ≥ 100 000 kr: +2,
minst én vunnet deal: +1). Sum ≥ 3 = høy, ≤ −1 = lav, ellers middels. LTV er samlet beløp på selskapets vunne deals
(leser inntil 100) og regnes ut fra HubSpot ved hver forespørsel, så det følger med når du markerer deals som vunnetHUBSPOT_OWNER_ID satt, eier den personen dealen og får en oppgave. HubSpot varsler da etter egne
varslingsinnstillinger (sjekk Innstillinger → Varsler for deal- og oppgavetildeling). Teamvarselet på e-post
sendes da ikke fra portalen; kvitteringen til kunden sendes som før.Status flyttes manuelt i HubSpot. Feil lagres per forespørsel (CrmSync), vises i admin (merke, banner,
«Synk på nytt») og prøves på nytt av /api/cron/crm-sync (daglig, maks fem forsøk i sju døgn). Kunden merker aldri feil.
Oppsett
hs project create og velg app, sett auth.type til static og
auth.distribution til private i app-hsmeta.json (fjern redirectUrls), legg inn scopes under, kjør
hs project upload og installer appen på kontoen. Tilgangstokenet står i appens innstillinger i HubSpot
(hs project open, velg appen, fanen Auth). Se
HubSpots veiledning.
Scopes (statisk autentisering støtter bare påkrevde scopes): crm.objects.contacts.read/write,
crm.objects.companies.read/write, crm.objects.deals.read/write, crm.objects.owners.read og
crm.schemas.deals.read/write (egenskaper). Oppgaver opprettes med kontakt-/dealscopene. Gir oppgaveopprettelse 403
(«failed» med feilkode http_403 i admin), legg til scope for oppgaver/engasjementer. Rettighetene er ikke testet mot den faktiske kontoen.HUBSPOT_ACCESS_TOKEN=... npm run hubspot:setup (eller legg token i .env). Skriptet oppretter dealegenskapene
(trygt å kjøre flere ganger) og sjekker at pipelinen og steget finnes.HUBSPOT_ACCESS_TOKEN (Sensitive), HUBSPOT_OWNER_ID og HUBSPOT_PORTAL_ID i Vercel og redeploy. Integrasjonen er
på så snart tokenet er satt. Pipeline og steg er som standard default og appointmentscheduled; andre pipelines
velges med HUBSPOT_PIPELINE_ID og HUBSPOT_STAGE_NEW.0005_crm_sync må være kjørt mot Supabase først.Personvern. Personopplysninger sendes bare til kontakten i HubSpot; dealbeskrivelsen inneholder ikke kontaktdata. Kontakter meldes ikke på markedsføring. Personvernteksten på kundesiden nevner HubSpot (EU-datasenter) når integrasjonen er på. Automatisk oppbevaringsfrist gjelder bare portalen: det som er overført til HubSpot følges opp og slettes der. «Slett alt fra en e-postadresse» arkiverer dealene i HubSpot først (og avbryter hvis det feiler), og kan valgfritt slette kontakten permanent (GDPR-sletting). Å slette én enkelt forespørsel i admin rører ikke HubSpot.
Forespørsler inneholder kontaktperson, e-post, telefon og fritekst, og e-postjobbene gjentar de samme opplysningene. Appen sletter ingenting av seg selv før fristene er bestemt:
| Variabel | Virkning |
|---|---|
INQUIRY_RETENTION_MONTHS | Avsluttede forespørsler (status «Avsluttet») slettes når de ikke er endret på så mange måneder (1 til 120). Tomt = beholdes. Når satt, vises fristen i personvernteksten på kundesiden. |
EMAIL_BODY_RETENTION_DAYS | Innholdet i sendte e-poster tømmes (og kundens adresse fjernes fra kvitteringen) etter så mange dager. Tomt = beholdes. |
CRON_SECRET | Kreves for at den daglige opprydningen (/api/cron/retention, kl. 03:30 UTC, se vercel.json) skal kjøre. |
Under Forespørsler i admin finnes «Slett alt fra en e-postadresse» (innsyn/sletting på forespørsel). Kontaktpersonen
du selv har lagt inn på en kunde (Customer.contact*) slettes ikke automatisk, og kunder kan ikke slettes, bare deaktiveres.
npm run admin:create).*_test og *_e2e og nekter å røre andre databaser.x-forwarded-for, som bare er pålitelig bak en proxy du kontrollerer.Dette må avklares og verifiseres før kunder får lenker:
RESEND_API_KEY, test mot ekte innboks).
Inntil videre gir appen varsling i nettsiden (teller, melding og fanetittel) for administrator.INQUIRY_RETENTION_MONTHS (og gjerne
EMAIL_BODY_RETENTION_DAYS) samt CRON_SECRET. Uten dem slettes ingenting automatisk (se «Personvern og oppbevaring»).
Gå også gjennom personverntekstens ordlyd.main er produksjon (Vercel publiserer bare herfra; alle andre branches får kun forhåndsvisning).main via pull request. Vercel lager en egen
forhåndsvisnings-URL for hver branch/PR, som kan sjekkes før kundene ser endringen.prisma/migrations/) må kjøres mot Supabase før koden som trenger dem
slås sammen. Se «Vercel + Supabase» over.SEED_DEMO_INQUIRIES | 1 legger inn to eksempelforespørsler ved seed. |
AdminUsernpm run db:seed