Open-Source Cold Email & Warmup Platform
Warmbly 是開源的冷郵件平台:溫暖您的收件箱、發送活動、並對每個回覆進行分流。可在我們的雲端運行或自行託管整個系統。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
A sales manager uploads a call recording; twenty seconds later they get the
transcript, the talk ratio, a SPIN score and the moments they let slip.
This repository is the frontend. Transcription and analysis run on a separate
Railway service, and the data lives in Supabase.
npm run build type-checks and compiles; npm run typecheck does the former
alone. Deploy target is Vercel — the four variables above are all it needs.
Two things live in the database rather than in this repo, and the app cannot
work without them. Both are in supabase/ and are safe to re-run.檢視完整 README ↗A sales manager uploads a call recording; twenty seconds later they get the transcript, the talk ratio, a SPIN score and the moments they let slip.
This repository is the frontend. Transcription and analysis run on a separate Railway service, and the data lives in Supabase.
npm install
cp .env.local.example .env.local # then fill in the Supabase values
npm run dev
| Variable | What it is |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Anon key. Never the service key — row level security is what separates one company's calls from another's, and a service key in the bundle would defeat it |
NEXT_PUBLIC_API_URL | Railway processing API |
NEXT_PUBLIC_API_AUTH | 1 once that API verifies Supabase JWTs; 0 (default) until then |
npm run build type-checks and compiles; npm run typecheck does the former
alone. Deploy target is Vercel — the four variables above are all it needs.
Two things live in the database rather than in this repo, and the app cannot
work without them. Both are in supabase/ and are safe to re-run.
profile-on-signup.sql — a trigger that
creates a profiles row for each new auth.users record. Every RLS policy
keys off my_company_id(), which reads from profiles; with no profile row
that returns NULL, every policy fails, and a signed-in person sees an app with
nothing in it. New profiles get company_id = NULL and the lowest role, so
signing up grants nobody access to a company's calls — an owner attaches them
afterwards with the update at the bottom of that file.fix-seller-stats-rls.sql — sets
security_invoker on the seller_stats view. Without it the view runs as its
owner, RLS on the underlying tables is never evaluated, and every company's
figures are readable by anyone holding the publishable key — which ships in
the browser. Verify with an unauthenticated request: it must return [].app/[locale] so next-intl
owns the URL. Uzbek is the default; the choice is kept in a cookie.app/globals.css holds the whole design
system: semantic colour tokens on :root, the same names redefined under
.dark, and @theme inline mapping them to utilities. Nothing is hardcoded,
so both themes come from one set of declarations.components/SessionProvider.tsx resolves
the session, the profile row and the company, then hands them to the app
shell. No session goes to sign-in; a profile with no company_id gets its own
screen rather than an empty dashboard.lib/api.ts. Every failure is
mapped onto a small vocabulary (limit, not_found, too_large, server,
network) that the UI turns into a localised sentence — no server prose ever
reaches a user.The ribbon (components/Ribbon.tsx) is the product. One component at three
sizes: 6px in a list row, 10px averaged on a seller, 14px and interactive on the
call detail page, where clicking a slice seeks the audio. Its slice elements are
memoised separately from the playhead, because a long call is a few hundred
slices and the player moves the playhead four times a second.
Speaker roles come from one convention, encoded once in lib/speakers.ts:
the person who speaks first is the salesperson. The diarizer only ever hands
back SPEAKER_00, SPEAKER_01; resolve the map once per call and pass it down.
Never render a raw label.
Red and blue mean one thing each. Red is the salesperson, blue is the client, everywhere. The single exception is the SPIN block, where an axis scoring four or below turns red to mark it weak — and that exception does not get extended anywhere else.
Dates and numbers are not formatted with Intl for Uzbek. Chrome ships no
uz locale data and silently falls back to the ICU root format (2026 M09 4)
and to English number conventions (1,234.5), while Node has the Uzbek data —
so the same value rendered differently on the server and in the browser, and
what the browser showed was wrong for the market. Dates are composed from month
names in the message files (lib/useDateFormat.ts, pinned to Asia/Tashkent) and
numbers use ru-RU, whose grouping and decimal marks match Uzbek usage and
which every engine ships.
Live status has two paths. Supabase Realtime on the calls table is the
fast one; polling GET /calls/{id} every five seconds for unsettled calls is
the guarantee, and it stops as soon as everything has settled.
Flowbite supplies the modal, dropdown, drawer, table, badge, avatar and form
controls. components/flowbiteTheme.ts remaps every surface it paints onto the
design tokens, and clears Flowbite's own strings for those keys — merging alone
leaves its dark:bg-gray-700 sitting next to our bg-canvas, because
tailwind-merge sees no conflict between a variant and a bare utility.
Motion is rationed. The ribbon draws itself in once on the detail page,
left to right over about half a second; SPIN bars grow from zero; the total
score counts up. That is all of it, and prefers-reduced-motion turns each one
off.
The build is clean and the app runs on Vercel unchanged, but three things have to be set up outside the repository or it will look broken.
1. Environment variables. .env.local is gitignored, so a fresh deploy has
none. In Vercel → Settings → Environment Variables, add all four from
.env.local.example. Without them every screen renders
the "could not reach the data" notice. NEXT_PUBLIC_API_AUTH must stay 0:
the processing service replies Access-Control-Allow-Headers: content-type,
so a browser preflight carrying Authorization would be refused and every
upload would fail.
2. Supabase auth URLs. Authentication → URL Configuration: set the Site URL to the deployed origin and add it to Redirect URLs. Otherwise the confirmation link in every sign-up email still points at localhost, and nobody can finish registering.
3. The database. A new Supabase project has none of this. Run
supabase/schema.sql then
supabase/api-keys.sql.
Serverless functions cap the request body at 4.5 MB, and the platform rejects
the request before any of this code runs. That affects exactly one path:
POST /api/v1/calls, the endpoint customers call with an API key, because it
proxies the audio.
It proxies deliberately. The processing service trusts whatever company_id
it is handed, so letting a customer post to it directly would let them file
recordings into — and then read them back out of — any company whose id they
could guess. The proxy is what makes the API key mean anything.
The dashboard's own uploader is not affected: it posts from the browser straight to the processing service and keeps the full 200 MB. Raising the API ceiling means either verifying JWTs on the processing service, so scoped credentials can be handed out, or hosting that one route somewhere without the cap.