FloCafe
FloCafe is the free, local-first restaurant POS in the FloPOS ecosystem. Run orders, tables, kitchen display, and local data without a software subscription.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Papan TV 16:9 "Ranking Jastiper": semua jastiper diurutkan dari belanja terbesar per nomor HP, dalam
dua kolom yang mengalir sebagai satu daftar (kolom kanan melanjutkan kolom kiri, berhenti sejenak di
peringkat 1), plus "Juara sementara" di kanan atas. Data dibaca dari Postgres (warehouse),
Google Sheet per brand (satu folder Drive) dan opsional Jubelio,
digabung per nomor HP di server, lalu halaman TV polling /api/state.
Postgres ─┐
Folder Drive ┼─► server (Node) ─ gabung per nomor HP ─► /api/state ─► public/index.html (TV)
(Sheet brand)│ poll tiap POLL_MS, simpan data terakhir yang berhasil
Jubelio ───┘
0812…, 62812…, +62 812… dianggap nomor yang sama, dari sumber mana pun.***********24) dikenali dari nama + 2 digit terakhir, dan tampil
sebagai xxxx-xxxx-xx24. Pembeli tersamar digabung ke pembeli Sheet brand yang nomornya berakhiran sama
dan nama depannya sama, kalau hanya ada satu yang cocok.0812-xxxx-7890) di server; nomor lengkap tidak pernah dikirim ke browser.#cek dan /healthz.Butuh Node.js 20.12+.
cp .env.example .env # isi data koneksi database dan DRIVE_FOLDER
npm install
npm start # http://localhost:3000
Atau semua sekaligus dengan Postgres contoh: docker compose up --build.
Repo ini siap untuk Vercel (vercel.json):
public/./api/state dilayani Vercel Function api/state.js; /healthz di-rewrite ke function yang sama.
Logikanya di server/api.js, memakai kode yang sama dengan server Node (server/).sin1 (Singapura), dekat dengan database.npm test; deploy batal kalau tes gagal.Langkah:
martech-sys/IMPERPECT-SALES-2026. Framework Preset
Other; pengaturan build lain sudah diambil dari vercel.json, tidak perlu diubah.https://<project>.vercel.app/healthz → 200 artinya minimal satu sumber terbaca.Bedanya dengan server Node: di Vercel sumber data dibaca saat ada request (bukan timer), paling sering
sekali per CACHE_SECONDS per instance, dan hasil /api/state di-cache di CDN Vercel selama
CACHE_SECONDS. Setelah itu CDN tetap langsung menjawab dengan ranking terakhir sambil
memperbarui di belakang layar (stale-while-revalidate, juga saat function error), jadi TV tidak
menunggu function yang baru menyala membaca ulang semua Sheet brand. Jadi berapa pun TV yang membuka halaman, database dibaca paling sering sekali per
CACHE_SECONDS. Kalau satu sumber gagal, data terakhirnya hanya bertahan selama instance function masih
hangat.
Kalau belum ada sumber data yang terbaca, /healthz menjawab 500 beserta daftar variabel yang diterima
deploy (terisi/kosong, tanpa nilainya) dan alasan tiap sumber dilewati.
Tambahkan #cek di URL (http://localhost:3000/#cek) untuk melihat jumlah baris valid / dilewati per sumber.
we_data_store)Langkah menyambungkan papan ke tabel laporan_penjualan. Password database cukup diisi di .env
(lokal) dan di Vercel; jangan dikirim lewat chat atau di-commit.
Minta DBA membuat user khusus papan yang hanya bisa membaca satu tabel. Lebih aman, dan bebannya terbatas:
CREATE ROLE imperfect_tv LOGIN PASSWORD 'ganti-password-kuat' CONNECTION LIMIT 5;
GRANT CONNECT ON DATABASE we_data_store TO imperfect_tv;
GRANT USAGE ON SCHEMA public TO imperfect_tv; -- ganti public kalau tabelnya di schema lain
GRANT SELECT ON laporan_penjualan TO imperfect_tv;
ALTER ROLE imperfect_tv SET statement_timeout = '20s';
Sementara belum ada, user read-only yang dipakai OFFLINE-STORE / data-db juga bisa (secrets
DATABASE_HOST, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME di repo itu).
Pilih salah satu cara. Kalau keduanya terisi, DATABASE_URL yang dipakai.
a. Variabel terpisah (disarankan, terutama kalau password banyak tanda bacanya). Password ditulis apa adanya, tanpa encode:
DATABASE_HOST=1.2.3.4
DATABASE_PORT=5432
DATABASE_USER=imperfect_tv
DATABASE_PASSWORD='Rh#4s!a/9?q@x:%20'
DATABASE_NAME=we_data_store
Di .env, selalu apit password dengan kutip tunggal. Tanpa kutip, # di posisi mana pun dibaca
sebagai awal komentar: DATABASE_PASSWORD=Rh#4s hanya terbaca Rh. Kalau password sendiri memuat
kutip tunggal, pakai kutip ganda ("..."). Di Vercel tidak perlu kutip: tempel apa adanya di kolom
nilai.
b. Satu DATABASE_URL.
postgres://USER:PASSWORD@HOST:5432/we_data_store
Tanda baca di password harus di-encode (# → %23, / → %2F, ? → %3F, @ → %40,
: → %3A, % → %25, dst.). Supaya tidak salah, biarkan komputer yang meng-encode. Perintah ini
meminta password tanpa menampilkannya dan tidak menyimpannya di riwayat shell:
read -rs PW && PW="$PW" node -e 'console.log(encodeURIComponent(process.env.PW))'; unset PW
Lainnya:
mysql_db_store): dengan DATABASE_URL, tambahkan di ujungnya
?options=-c%20search_path%3Dmysql_db_store; dengan variabel terpisah, ubah PG_QUERY menjadi
FROM mysql_db_store.laporan_penjualan.DATABASE_SSL=false, jadi mulai dengan PG_SSL=false.psql di langkah 3, variabel terpisah dipetakan ke PGHOST, PGPORT, PGUSER,
PGPASSWORD, PGDATABASE, lalu jalankan psql tanpa argumen koneksi.Dengan psql (atau DBeaver/TablePlus):
psql "$DATABASE_URL" -c "select current_user, current_database(), now()"
psql "$DATABASE_URL" -f scripts/cek-event-postgres.sql
# atau, dengan variabel terpisah:
export PGHOST=... PGPORT=5432 PGUSER=... PGDATABASE=we_data_store
read -rs PGPASSWORD && export PGPASSWORD
psql -f scripts/cek-event-postgres.sql
Hasil skrip cek hanya berisi nama kolom, store/gudang, status, dan hitungan (tanpa data pelanggan),
jadi aman dibagikan. Dari situ PG_QUERY disesuaikan: kolom dan nilai penanda event (warehouse atau
channel), status yang dibuang, dan penyaring yang memakai index
(lihat Beban ke database).
git pull
cp .env.example .env # isi data koneksi (langkah 2); PG_QUERY sesuaikan hasil langkah 3
npm install
npm start # buka http://localhost:3000/#cek
Chip #cek menampilkan "Postgres N valid · M dilewati". "Dilewati" = invoice tanpa nomor HP.
Vercel → project imperpect-sales-2026 → Settings → Environment Variables. Isi tiap variabel
untuk environment Production (centang juga Preview kalau deploy preview perlu data). Tandai
DATABASE_PASSWORD (atau DATABASE_URL) sebagai Sensitive. Beberapa variabel sekaligus bisa
ditempel dalam format .env ke kolom Key.
| Variabel | Nilai |
|---|---|
DATA_SOURCES | postgres,drive (postgres saja kalau folder Sheet brand belum siap) |
DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME | dari langkah 2a (atau satu DATABASE_URL, langkah 2b) |
PG_QUERY | dari .env.example, disesuaikan hasil cek |
PG_SSL | false (atau true kalau server mewajibkan) |
POLL_MS | 60000 |
CACHE_SECONDS | 30 |
Deployments → ⋯ pada deploy teratas → Redeploy. Perubahan env baru berlaku setelah deploy ulang.
Cek:
https://imperpect-sales-2026.vercel.app/healthz → status 200 dengan "ok":true.https://imperpect-sales-2026.vercel.app/#cek → jumlah invoice valid.Pesan error asli tampil di /healthz (sources.postgres.error) dan di chip #cek:
| Pesan | Penyebab dan solusi |
|---|---|
password authentication failed | Password salah. Dengan DATABASE_URL: tanda bacanya belum di-encode; pakai variabel terpisah (langkah 2a). |
timeout / ECONNREFUSED | Host/port salah, atau firewall database menolak. Vercel Functions tidak punya IP tetap, jadi user ini harus boleh masuk dari IP mana pun. |
relation "laporan_penjualan" does not exist | Tabel ada di schema lain: tambahkan search_path (langkah 2). |
permission denied for table | GRANT SELECT belum diberikan (langkah 1). |
no pg_hba.conf entry ... SSL off | Set PG_SSL=true. |
self-signed certificate | Set juga PG_SSL_REJECT_UNAUTHORIZED=false. |
.env)| Variabel | Default | Keterangan |
|---|---|---|
DATABASE_URL | – | Koneksi Postgres sebagai URL (password di-encode). Didahulukan bila terisi. |
DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, DATABASE_NAME | – / 5432 | Koneksi Postgres per bagian; password apa adanya. Dipakai bila DATABASE_URL kosong. Keduanya kosong = sumber Postgres dilewati. |
PG_QUERY | SELECT phone, name, qty, trx_value AS value FROM transactions | Query yang mengembalikan kolom phone, name, qty, value, satu baris per transaksi. |
PG_SSL | false | true untuk DB terkelola (Supabase, RDS, Neon, dll). |
DRIVE_FOLDER | – | Link/ID folder Drive berisi satu spreadsheet per brand. Lihat Sheet brand. |
GOOGLE_SERVICE_ACCOUNT_JSON | – | Isi file key JSON akun layanan. Atau GOOGLE_SERVICE_ACCOUNT_EMAIL + GOOGLE_PRIVATE_KEY. |
DRIVE_MAX_ROWS | 5000 | Baris maksimum yang dibaca per file brand. |
LOCK_TOTALS | – | Kunci total pembeli tertentu (mis. hasil cek struk), satu per baris atau dipisah ;: No HP = total atau No HP = total / trx. Total di papan memakai angka ini, berapa pun jumlah dari sumber data. |
DRIVE_EXCLUDE | – | Nama file brand yang tidak ikut papan, dipisah koma, mis. Mayoutfit. Huruf besar/kecil diabaikan. |
SHEET_URLS | – | Sheet per brand lewat link publik, satu per baris: Brand | link. |
SHEET_URL | – | Satu Sheet tanpa brand (Share: Anyone with the link, Viewer), ID Sheet, atau link CSV . |
we_data_store) — sumber event 2026Transaksi event dibaca dari tabel laporan_penjualan di warehouse we_data_store (sama dengan yang
dipakai dashboard OFFLINE-STORE). Tabel itu berisi satu baris per produk, dengan kolom pelanggan
nama dan no_hp, jadi PG_QUERY mengelompokkannya per invoice:
SELECT MAX(no_hp) AS phone, MAX(nama) AS name, SUM(qty) AS qty, MAX(total_bayar) AS value
FROM laporan_penjualan
WHERE warehouse IN ('MAIN IMPERFECT WAREHOUS 2026', 'TEBUS MURAH IMPERFECT 2026') -- atau channel = '...', lihat cek di bawah
AND tanggal >= DATE '2026-10-30' AND tanggal < DATE '2026-11-02'
AND COALESCE(status, '') !~* 'cancel|batal' -- order batal tidak dihitung
GROUP BY COALESCE(NULLIF(btrim(invoice_number), ''), no_order)
total_bayar, diambil sekali per invoice. total_bayar adalah total satu invoice yang
berulang di tiap baris produknya; invoice berisi 3 produk tetap dihitung satu kali (MAX), bukan
dijumlahkan jadi tiga kali lipat. Invoice-invoice berbeda milik nomor HP yang sama dijumlahkan papan.invoice_number memakai no_order sebagai kunci.status yang memuat "cancel" atau "batal" (huruf besar/kecil bebas, mis. CANCEL,
Canceled, Cancelled, BATAL) tidak dihitung. Status kosong tetap dihitung.MAIN IMPERFECT WAREHOUS 2026 dan TEBUS MURAH IMPERFECT 2026.
Pengelompokan tetap per invoice (bukan per gudang), jadi invoice yang barisnya tercatat di dua gudang
tetap dihitung satu kali. Gudang lain ditambahkan ke daftar IN (...).< hari berikutnya, agar transaksi di hari terakhir tidak terbuang kalau tanggal
bertipe timestamp.no_hp tidak masuk papan (terlihat sebagai "dilewati" di #cek).numeric dari Postgres (mis. "2500000.0000") dibaca dengan benar.Sebelum event, jalankan scripts/cek-event-postgres.sql dengan user read-only. Skrip ini hanya
mencetak nama kolom, nama store/gudang, status, dan hitungan (tanpa data pelanggan) untuk memastikan:
kolom penyaring event (warehouse atau channel) dan nilainya, seberapa banyak transaksi event yang
punya no_hp, status apa yang perlu dibuang (batal/retur), dan seberapa cepat transaksi masuk ke tabel.
psql "$DATABASE_URL" -f scripts/cek-event-postgres.sql
Papan dirancang agar jumlah TV tidak menambah beban database:
/api/state, bukan database. Server Node
menjalankan PG_QUERY sekali tiap POLL_MS; di Vercel hasilnya di-cache di CDN selama
CACHE_SECONDS, jadi function (dan query) jalan paling sering sekali per interval itu.statement_timeout).Yang perlu dipastikan adalah query-nya memakai index. laporan_penjualan besar; kalau penyaringnya
tidak ter-index, Postgres membaca seluruh tabel setiap poll. Bagian 6–7 skrip cek menampilkan index
yang ada dan rencana eksekusi query papan:
(sumber, channel, tanggal). Kalau event tercatat sebagai
satu channel di sumber = 'OFFLINE-STORE', saring dengan
sumber = 'OFFLINE-STORE' AND channel = '...' AND tanggal ... agar index itu terpakai.warehouse, minta DBA membuat index
(warehouse, tanggal), atau naikkan interval.POLL_MS=60000 dan CACHE_SECONDS=30. Kalau laporan_penjualan sendiri diisi
berkala (mis. tiap 30 menit), poll lebih cepat dari itu tidak menambah kesegaran, hanya beban.Untuk tabel lain, cukup ganti PG_QUERY; kolom yang dibutuhkan selalu phone, name, qty, value.
Tabel contoh untuk uji lokal ada di db/schema.sql + db/seed.sql (npm run db:init, npm run db:seed).
Sumber ini hanya membaca: POST /login untuk token, sisanya GET. Perilaku API sudah dicek di akun
ini (lihat juga martech-sys/data-db, docs/jubelio-api.md):
MAIN IMPERFECT WAREHOUS 2026 (id lokasi 26, outlet POS). Gudang kedua
TEBUS MURAH IMPERFECT 2026 dibaca lewat Postgres (PG_QUERY).GET /sales/orders/ tidak bisa difilter per lokasi dan hanya memuat nama lokasi, jadi order di
rentang JUBELIO_DATE_FROM–TO (WIB) dibaca lalu disaring ke JUBELIO_LOCATION (nama persis, tidak
peka huruf besar/kecil). Order batal (internal_status CANCELED) dilewati.**********99). Nomor lengkap diambil dari
GET /contacts/{contact_id} (kolom phone), sekali per pembeli.contact_id -1) tidak punya nomor HP, jadi tidak masuk papan
(terlihat sebagai "dilewati" / "tanpa HP" di #cek). Supaya belanja jastiper terhitung, kasir harus
memilih atau membuat kontak pembeli dengan nomor HP di POS.grand_total (atau JUBELIO_VALUE_FIELD), angka string seperti "750000.0000" dikonversi
dengan benar. Qty = jumlah qty_in_base item yang tidak batal.Sinkronisasinya bertahap dengan batas waktu JUBELIO_BUDGET_MS per pembacaan: pemindaian pertama
berjalan halaman per halaman, setelah itu hanya order yang berubah yang dibaca (lastModifiedSince; API
hanya memakai tanggal UTC-nya, jadi yang terbaca ulang adalah semua perubahan hari itu dan order yang
tidak berubah dilewati). Pembacaan yang terpotong batas waktu dilanjutkan dari halamannya. Progres sinkron
disimpan di memori, jadi di Vercel function yang baru menyala mulai memindai lagi dari awal; untuk event
dengan ribuan order, server Node yang berjalan terus lebih cocok. Progres terlihat di #cek
("memindai order", "N menunggu kontak", "N tanpa HP").
Kredensial Jubelio cukup di .env / environment variables hosting. Jangan di-commit.
Transaksi yang tidak masuk warehouse (dicatat sendiri oleh tiap brand) diisi di satu spreadsheet per brand, semuanya di satu folder Google Drive. Papan membaca folder itu dengan akun layanan (service account) Google, lalu menggabungkannya per nomor HP bersama Postgres.
#cek). Yang dibaca: tab pertama (diekspor sebagai CSV lewat
Drive API; kolom dikenali dari judulnya).#cek dan /healthz (bukan di TV).Format tiap file — baris pertama judul, satu baris = satu transaksi:
| No HP | Nama | qty | trx value |
|---|---|---|---|
| 081234567890 | Dewi Lestari | 3 | 2500000 |
Buat akun layanan — console.cloud.google.com:
imperfect-papan-tv).papan-tv. Lewati langkah
roles dan grant users access (tidak perlu role apa pun)..json terunduh.
Simpan baik-baik: isinya kunci rahasia. Jangan di-commit / dikirim ke chat.papan-tv@<project>.iam.gserviceaccount.com.Buat folder di Google Drive (mis. Imperfect Sale 2026 – Sheet Brand) → Share → tambahkan
email akun layanan sebagai Viewer (hapus centang Notify people). Jangan share folder ke brand.
Google Workspace yang membatasi share ke luar domain perlu mengizinkan domain
iam.gserviceaccount.com (Admin console → Drive → Sharing settings).
Buat file per brand dengan scripts/buat-sheet-brand.gs (daftar
brand event 2026 sudah terisi): buka script.google.com → New project →
tempel isi file → isi FOLDER dan ADMIN → pilih buatSheetBrand → Run. Script membuat file
dengan judul kolom, kolom No HP berformat teks, dan validasi angka. Dengan AKSES = 'email' (bawaan)
tiap file hanya bisa dibuka email di ADMIN (Editor di setiap file, bukan di folder) dan email
brand di BRANDS; admin yang menambah akses tim brand per file lewat Share. aturAksesAdmin
menerapkan ini ke file yang sudah ada dan mematikan akses link. Link tiap brand ditulis ke
spreadsheet Daftar Link Sheet Brand di My Drive pembuat. Aman dijalankan ulang.
membuka tiap file : cepat, tapi siapa pun yang memegang link bisa melihat dan mengubah isinya (termasuk nomor HP pembeli). Riwayat perubahan ada di .
Kalau gagal:
| Pesan | Perbaikan |
|---|---|
Folder Drive tidak bisa dibaca (HTTP 404 …). Share folder ke … sebagai Viewer | Folder belum di-share ke email akun layanan, atau DRIVE_FOLDER salah. |
HTTP 403 … has not been used in project | Google Drive API belum diaktifkan (langkah 1.2). |
HTTP 429: Quota exceeded … sheets.googleapis.com | Deploy lama yang masih memakai Sheets API. Redeploy dari main terbaru. |
Login akun layanan gagal: invalid_grant | Kunci sudah dihapus / salah tempel. Buat key JSON baru. |
GOOGLE_SERVICE_ACCOUNT_JSON bukan JSON yang valid | Tempel ulang seluruh isi file, dari { sampai }. |
| Brand tidak muncul | File harus berupa Google Sheets (bukan .xlsx) dan berada langsung di folder itu. |
Tips untuk brand: isi kolom trx value sebagai angka (2500000); nomor HP boleh 0812…, 62812…, atau
+62 812…. Baris yang nomor HP-nya tidak valid atau nilainya kosong dilewati dan dilaporkan di #cek
sebagai Brand:baris.
Jangan memasukkan transaksi yang sama ke Postgres dan Sheet brand: nilainya akan terhitung dua kali.
Tanpa akun layanan, Sheet bisa dibaca lewat link publik: Share Anyone with the link → Viewer. Siapa pun yang memegang link bisa melihat isinya, jadi untuk omzet brand pakai cara folder Drive di atas.
Daftar Sheet diisi di SHEET_URLS, satu brand per baris (di .env satu baris, pisahkan dengan ;), dan
DATA_SOURCES harus memuat sheet:
Heymale | https://docs.google.com/spreadsheets/d/AAA/edit?gid=0
Heylocal | https://docs.google.com/spreadsheets/d/BBB/edit?gid=0
Tiap brand jadi sumber sendiri (sheet:Heymale, …); Sheet yang gagal dibaca tidak menghentikan papan.
SHEET_URL membaca satu Sheet tanpa nama brand.
scripts/sinkron-jubelio.gs)Apps Script yang mengambil laporan Sales List Jubelio (lokasi 26 dan 64, 30 Okt–1 Nov, pesanan JP…,
tanpa Cancel) dan menulisnya ke tab pertama Sheet Internal dengan format papan (No HP, Nama, qty,
trx value), satu baris per pesanan dengan Grand Total diambil sekali. Laporan Jubelio memuat nomor HP
lengkap, jadi dipakai sebagai pengganti tabel warehouse yang nomornya tersamar
(DATA_SOURCES=drive). Jadwal otomatis tiap 15 menit lewat pasangJadwal; kredensial Jubelio di Script Properties.
Sheet Internal harus berada di folder Drive papan dan tidak di-share ke brand.
Pesanan yang tidak masuk papan (No Telp kosong, tersamar, tidak valid, atau Grand Total 0) ditulis ke
tab Tanpa HP dengan No Pesanan, No Telp asli, Pelanggan, qty, Grand Total, Lokasi dan alasannya. Papan
hanya membaca tab pertama, jadi tab ini tidak ikut dihitung; lengkapi nomornya di Jubelio, sinkron
berikutnya memindahkan pesanan itu ke papan.
Hasil validasi manual pesanan tanpa HP diisi di tab Transaksi_Tanpa_HP_Valid (No HP, Nama, qty,
trx value, opsional No Pesanan); setiap sinkron isinya disalin ke tab pertama sehingga ikut papan. Baris
dengan No Pesanan yang belakangan sudah punya No HP valid di Jubelio dilewati agar tidak dobel. Jumlah
baris yang masuk / salah tampil di tab Sinkron.
scripts/top10-spender.gs)Apps Script (project yang sama dengan buat-sheet-brand.gs) yang setiap 10 menit membaca tab pertama
semua file di folder papan, menghitung seperti papan, dan menulis 10 pembeli teratas (nama, nomor HP
lengkap, total, trx, qty, tier, hadiah) ke spreadsheet Top 10 Spender di My Drive. Akses diatur dengan
AKSES_TOP10 (email / domain / link, hanya melihat). Isinya data pribadi pembeli: pilih akses
sesempit mungkin.
scripts/ranking-manual.sql — jalankan di tool database (read-only):
total_bayar sekali per
invoice, tier);scripts/rekap-manual.gs — Apps Script (project yang sama dengan buat-sheet-brand.gs):
rekapManual menggabungkan hasil query 1 (ditempel di tab Postgres) dengan semua Sheet brand ke
tab Ranking di spreadsheet Rekap Manual Ranking (My Drive).Hasil keduanya memuat nomor HP lengkap: untuk tim internal saja, jangan ditampilkan di TV.
GET / — halaman TVGET /api/state — { state: { updatedAt, totals, top[] }, sources: { postgres, drive, … }, pollMs }GET /healthz — 200 bila minimal satu sumber terbaca, 503 bila tidaknpm test
SHEET_GID | 0 | Tab yang dibaca bila link tidak memuat gid. |
DATA_SOURCES | postgres,sheet,drive,jubelio | Batasi sumber. Event 2026: postgres,drive. |
JUBELIO_EMAIL, JUBELIO_PASSWORD | – | Login API Jubelio. Kosong = sumber Jubelio dilewati. |
JUBELIO_LOCATION | – | Nama gudang persis di Jubelio: MAIN IMPERFECT WAREHOUS 2026. |
JUBELIO_DATE_FROM, JUBELIO_DATE_TO | – / tanpa batas | Rentang tanggal transaksi event (WIB, YYYY-MM-DD). FROM wajib. |
JUBELIO_VALUE_FIELD | grand_total | Kolom nilai dari detail order, mis. sub_total. |
JUBELIO_BUDGET_MS | 20000 | Batas waktu sinkron Jubelio per pembacaan (di bawah maxDuration 30 detik Vercel). |
POLL_MS | 30000 | Interval baca ulang di server Node, sekaligus interval polling halaman TV (min 5000). |
CACHE_SECONDS | 30 | Vercel saja: lama hasil /api/state di-cache di CDN (= jarak minimum antar query). |
PORT | 3000 | Port HTTP. |
AKSES = 'link'Vercel → Settings → Environment Variables (Production), lalu Redeploy:
| Variabel | Isi |
|---|---|
DATA_SOURCES | postgres,drive |
DRIVE_FOLDER | link folder dari langkah 2 |
GOOGLE_SERVICE_ACCOUNT_JSON | seluruh isi file .json dari langkah 1, ditempel apa adanya. Tandai Sensitive. |
Cek https://<project>.vercel.app/healthz: sumber drive ok: true, dan #cek di halaman TV
menampilkan Sheet brand … · N brand.