Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证System calendar and dashboard for task management of Camarines Norte PIO
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗Coverage tracking for the Provincial Information Office (PIO) of Camarines Norte. Every event the office covers is tracked from request to the published Facebook post, with weekly and monthly reports of who did what.
It replaces chat threads and memory with one place to:
Live app: pio-production.vercel.app · public request form: pio-production.vercel.app/request
All dates, weeks (Monday–Sunday) and months are computed in Asia/Manila. Timestamps are stored in UTC.
| Page | Who | What it's for |
|---|---|---|
Dashboard / | everyone | This week / This month / custom range. Signed in, Waiting on you comes first. Then posts published and events covered (vs the previous period), in progress, stuck; the Needs assignment (head and viewers), Stuck and Next 7 days lists; Waiting on (who acts next on each event, one card per person, most overdue first; folded for staff other than the head); posts by source and pipeline now; and Work by person, one tab per kind of work, opening on the viewer's own. Every chart has a "Show as table" view. |
Board /board | everyone; changes need a PIN | One column per status. Signed in, a view switch picks Everything, Mine (events you're on or that wait on you; empty columns fold to a strip) or your stage (only the columns your roles act on, e.g. Production for writers and editors). The head starts on Everything, everyone else on Mine; ?view= keeps the choice in the URL. Drag a card by its handle one step forward, or one step back (head, with a reason). Illegal drops snap back with the reason. Tick Photos/Video and Caption on the card. Filters (source, municipality, person, dates, cancelled) live in the URL. |
Event (drawer or /events/<id>) | everyone; changes need a PIN | All details, the actions you're allowed to take (with the reason when one is blocked), team assignment, the caption (see Caption review), Drive link, posting details, and the full activity timeline. |
Calendar /calendar | everyone | Month grid of Events (by event date) or Published posts (by posting date), coloured by source. Hover a chip for details on a computer; tap a day on a phone. Click an event to open it. |
Assign /needs-assignment | head assigns; others view | Requested or scheduled events in the next 14 days missing a writer or photo/video. Pick people inline; each option shows how many other events that person has that day. |
Reports /reports | everyone | Monthly or weekly. Posted events grouped by source with subtotals, a per-person table (written / proofread / photographed / videographed), CSV downloads, and an A4 print layout with "Prepared by / Noted by" lines. |
My work /me | signed-in staff | Waiting on you: every event that needs your next step, overdue first. Then your own events month by month (by event date): the last 6 months at a glance with events held and posted, then the chosen month's events with your role, source and status. Step back through earlier months with the arrows. |
Staff /admin | head | Add people, set roles, reset PINs, deactivate leavers, clear sign-in locks, restore deleted events. |
Request coverage /request | public, no sign-in | Organisers and LGUs request coverage, and choose photos/video/article, a live stream, or both. Lands on the board as Requested. |
First-run setup /setup | once | Creates the first head when there are no staff yet. |
Source colours are the same everywhere: Provincial Government blue, Other LGU purple, Community green, Private orange. They were checked for colour-blind separation and always appear with a text label.
VIEW_KEY and viewers must open the app once with ?k=<VIEW_KEY>. A cookie then remembers the device. The public request form stays open to everyone.Next.js 16 (App Router, Server Components, Server Actions, Cache Components), Supabase Postgres (SQL migrations; Supabase Auth is not used), Tailwind CSS 4 + shadcn/ui, Recharts, @dnd-kit, zod, date-fns / date-fns-tz, Vitest and Playwright. Deploys to Vercel + Supabase on their free tiers.
You need: Node 22+, pnpm 10+, and Docker (for local Supabase).
pnpm install
cp .env.example .env.local
pnpm db:start # starts local Supabase and prints the API URL and keys
Fill in .env.local:
| Variable | Value |
|---|---|
SUPABASE_URL | http://127.0.0.1:54321 locally |
SUPABASE_SERVICE_ROLE_KEY | the service_role (or secret, sb_secret_…) key printed by pnpm db:start |
SESSION_SECRET | 32+ random characters: openssl rand -hex 32 |
VIEW_KEY | optional; leave blank for open viewing |
Then:
pnpm db:reset # apply supabase/migrations/ to a clean local database
pnpm seed # 10 placeholder staff (PIN 1234) + 28 sample events
pnpm dev # http://localhost:3000
Sign in as Grace S. (head + proofreader) with PIN 1234, or as anyone else below. To try the first-run setup screen instead, run pnpm db:reset without seeding and open /setup.
Open
http://localhost:3000, not127.0.0.1:3000. In development Next.js only serves its scripts tolocalhost, so pages wouldn't become interactive.
scripts/seed.ts loads supabase/seed/pio-coverage-pipeline.csv:
NODE_ENV=production, or against a non-local database without --allow-remote.| Staff (PIN 1234) | Roles |
|---|---|
| Grace S. | head, proofreader |
| Joel B. | proofreader, page admin |
| Rico M., Jessa T. | photographer |
| Paolo D. | videographer |
| Ana R., Mark V., Liza C. | writer |
| Ben A. | cameraman, videographer |
| Kim L. | OBS tech |
SQL migrations live in supabase/migrations/. Locally, pnpm db:reset rebuilds the database from them. For a hosted project, see Deploy. To add a change, create a new file (supabase migration new <name>) rather than editing an applied one.
| Command | What it runs |
|---|---|
pnpm check | typecheck + lint + unit tests with coverage |
pnpm test / pnpm test:coverage | Vitest. lib/workflow.ts and lib/metrics.ts are held to 100% branch coverage, including Manila timezone edge cases. |
pnpm test:e2e | Playwright: builds and starts the app, then runs the end-to-end tests on a phone (Pixel 7) and a desktop viewport. |
The end-to-end tests need local Supabase running and the seed loaded (pnpm db:reset && pnpm seed). The main one follows one event through its whole life:
delete-restore.spec.ts covers deleting an event and bringing it back (Undo and Staff → Recently deleted events), the typed confirmation for posted events, and deactivating and deleting accounts.
They are safe to rerun. Set E2E_BASE_URL to test a server that's already running. If you use a preinstalled Chromium instead of npx playwright install, set PLAYWRIGHT_CHROMIUM_PATH.
GitHub Actions runs them on every pull request and push to main (.github/workflows/e2e.yml): it starts local Supabase with the CLI, loads the seed and uploads the Playwright report as an artifact.
Both free tiers are enough for one office.
At supabase.com, create a project. Choose the Southeast Asia (Singapore) region, which is closest to Camarines Norte, and save the database password.
Apply the schema from your machine:
pnpm exec supabase login
pnpm exec supabase link --project-ref <your-project-ref> # from the project URL
pnpm exec supabase db push # applies supabase/migrations/
In Project Settings → API Keys, copy the Project URL and the service_role / secret key. Treat that key like a password: it bypasses all database security.
Do not run pnpm seed against production; it creates staff with a shared PIN.
At vercel.com, choose Add New → Project and import this GitHub repository. Vercel detects Next.js and pnpm automatically.
Under Environment Variables, add these for Production (and Preview if you use it):
| Name | Value |
|---|---|
SUPABASE_URL | the Project URL |
SUPABASE_SERVICE_ROLE_KEY | the service_role / secret key |
SESSION_SECRET | a new openssl rand -hex 32 (different from development) |
VIEW_KEY | optional: a long random string if viewing should need the shared link |
Never prefix these with NEXT_PUBLIC_; that would send them to browsers.
Optional: in Settings → Functions, set the region to Singapore (sin1) so the app runs next to the database.
Deploy.
https://pio-production.vercel.app/setup and create the head's account and PIN.https://pio-production.vercel.app/. Add ?k=<VIEW_KEY> if you set one.https://pio-production.vercel.app/request.pnpm exec supabase db push before or right after the deploy.pnpm exec supabase db dump --data-only > backup.sql now and then.lib/workflow.ts is the single source of truth. It's a pure module with full test coverage; the server enforces it on every change, and the UI uses it to show only the actions that will work.
requested → scheduled → covered → in_production → for_proofreading → approved → scheduled_for_posting → posted. The head can cancel an event from any status before posted, with a reason, and can restore it later. The head can also move an event back one step, with a reason.
| Step | Who | Needs |
|---|---|---|
| requested → scheduled | head | source and municipality |
| scheduled → covered | assigned photographer/videographer, live crew, or head | the head has ticked Coverage confirmed (for a live stream, only possible once a cameraman and an OBS tech are assigned) |
| covered → posted (live stream only) | assigned live crew, page admin or head | the Facebook live video link |
| covered → in_production | assigned team or head; also automatic when either checkbox is ticked | — |
| in_production → for_proofreading | automatic | Photos/Video done (photographer/videographer or head) and Caption done (writer or head), whichever is ticked last |
| for_proofreading → approved | proofreader or head | becomes the proofreader if none was assigned |
| for_proofreading → in_production (request caption changes) | proofreader or head | a note for the writer; unticks Caption done |
| approved → scheduled_for_posting | page admin or head | posting date and time |
| scheduled_for_posting → posted | page admin or head | a facebook.com or fb.watch post link; posted time defaults to now |
Only the head assigns people and edits event details. The Drive folder link and caption text are optional references; no rule requires them (but see Caption review). Every change is written to the append-only event_activity log.
Concurrent edits: if two people change the same event at once, the second gets "Someone else just updated this event" and sees the fresh version, so nobody overwrites anyone. On the board, changes appear immediately and roll back if the server refuses them or the connection drops.
Each event has a coverage: Photos, video and article (the usual), Live stream only, or Live stream + photos, video and article. The requester picks it on the public form; the head can change it under Edit details until production starts.
requested → scheduled → covered → posted. Once it's covered, the live crew, a page admin or the head adds the Facebook live video link (Add the live video), and the event is posted. There are no Photos/Video or Caption boxes, and no caption.Writers post the caption in the event itself, so the proofreader reviews exactly what will go on the page instead of hunting for it in chat.
caption_revisions, with its author. Signed-in staff can compare the current caption with any earlier version, word by word (additions in green, removals struck through). The proofreader sees exactly what the writer fixed, and the writer sees the proofreader's corrections.The activity log records each caption edit and request (with the note), but not the caption text itself; the text lives in caption_revisions, which is removed along with its event.
Every event in progress shows who it's waiting on (on the board card, at the top of the event, on the dashboard and in My work). It's worked out from the event's status, checkboxes and team in lib/waiting.ts, never typed in, so it is always current.
| Status | Waiting on |
|---|---|
| Requested | the head, to add source/municipality and schedule |
| Scheduled | the head, to assign a writer and photo/video (and a cameraman and OBS tech for a live stream), then confirm coverage; nobody until the event day; then the photographer/videographer and live crew, to mark it covered |
| Covered, live stream only | the OBS tech (or a page admin), to add the live video link |
| Covered / In production | whoever still owes work: the editor (or, with no editor, the photographer/videographer) for Photos/Video, the writer for the caption ("revise" after changes were requested); the head if a needed slot is empty |
| For proofreading | the assigned proofreader, or any proofreader |
| Approved / Scheduled for posting | a page admin, to schedule and then publish the post |
These live in lib/metrics.ts and are covered by unit tests.
posted_at falls inside the period, in Manila time. Cancelled events never count.app/ routes (dashboard, board, calendar, needs-assignment, reports, me, admin,
events/[id], request, login, setup) and Server Actions in app/actions/
app/@drawer/ intercepted /events/[id] → side drawer over the current page
components/ UI by area (board, events, dashboard, calendar, assign, reports, admin, ui = shadcn)
lib/workflow.ts transition rules (pure, 100% tested)
lib/metrics.ts reporting definitions (pure, 100% tested)
lib/my-work.ts one person's events by month (pure, tested)
lib/waiting.ts who each event is waiting on (pure, 100% tested)
lib/text-diff.ts word diff for comparing caption versions
lib/commands.ts zod schema for event commands (server); lib/command-preview.ts for the browser
lib/data/ database reads (server only)
lib/auth/ PIN hashing, sessions, lockout
supabase/migrations/ SQL schema
scripts/seed.ts development seed
tests/unit, tests/e2e
Notifications such as email or SMS are out of scope. To add one, send it from the Server Actions in app/actions/ after a change is saved; each one has the activity rows it wrote.
localhost:3000, not 127.0.0.1:3000..env.local is missing a value; SESSION_SECRET needs at least 32 characters.supabase start can't pull images (registry rate limits): run only the database with supabase db start, then follow the comments in scripts/dev/rest-proxy.mjs to run PostgREST behind a small /rest/v1 proxy.update staff set roles = array_append(roles, 'head') where display_name = '<someone>'; for another staff member, then have that person reset the PIN from Staff.In keeping with the Data Privacy Act of 2012 (RA 10173), the app stores only:
IP addresses used for rate limiting and lockout are stored only as keyed hashes. Old data is deleted automatically (prune_expired_data, run occasionally on sign-in):