Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Turns recurring shared costs across a household of eight into computed annual invoices, tracked payments, and share links anyone can check without asking the organizer. Dispute handling, email invoicing, and an append-only audit trail included.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A cloud-based web application for coordinating and settling annual shared bills among friends and family. Features multi-user authentication, billing year lifecycle management, settlement progress tracking, share links, dispute resolution, and email invoicing.
👥 User Management
• ✅ Email/Password authentication with forgot-password recovery
• ✅ Google Sign-In (primary action)
• ✅ Secure logout
• ✅ Per-user data isolation
• ✅ Cloud sync across devicesREADME 전문 보기 ↗A cloud-based web application for coordinating and settling annual shared bills among friends and family. Features per-account authentication, billing year lifecycle management, settlement progress tracking by Household, share links, Review Requests, Credits with Refunds or Carry-forward, Usage Charges and Service Credits, and invoicing by email or text.
🌐 Live Application: https://friends-and-family-billing.web.app
🎨 Redesign Wireframes: plans/ffb-wireframes.html (rendered); see Redesign Wireframes
The terms below follow the domain glossary in CONTEXT.md.
VITE_LOGODEV_KEY is configured), then initials%first_name%, %full_name%, %billing_year% and %household_total%mailto:) are also availablebillingEvents) for financial mutationsversion.json every minute and offers a refresh when a new deploy is liveRequires Node.js 24 or later (.nvmrc pins 24).
# Clone the repository
git clone https://github.com/nathanjohnpayne/friends-and-family-billing.git
cd friends-and-family-billing
# Install dependencies (postinstall also installs functions/ dependencies)
npm install
# Create the local Firebase config (gitignored) and fill in the VITE_FIREBASE_* values.
# Maintainers with 1Password access can run ./scripts/bootstrap.sh instead.
cp .env.example .env.local
# Generate firebase-config.local.js (gitignored), the runtime config bridge index.html loads
node scripts/generate-firebase-config.js
# Dev server with hot module replacement
npm run dev
# Production build (build:react → build:assemble, output in app/)
npm run build
# Tests: Vitest suite + Cloud Functions tests + tracked-file secret scan
npm test
# Playwright end-to-end tests against an E2E-mode build (Chromium)
npm run test:e2e
# Firestore security-rules tests on the emulator (needs Java 21+)
npm run test:rules
# Deploy (requires 1Password CLI + deploy tooling)
npm run deploy # Hosting + Firestore rules + Storage rules
npm run deploy:functions # Cloud Functions
npm run deploy:all # Everything
The deploy scripts use op-firebase-deploy, and Firebase Hosting runs the configured predeploy hook (npm run build && node stamp-version.js) before upload. See DEPLOYMENT.md for full setup instructions.
| Script | What it does |
|---|---|
npm run dev | Vite dev server with hot module replacement (opens a browser) |
npm run build | build:react (Vite) then build:assemble (scripts/assemble-deploy.js), output in app/ |
npm run preview | Serve the last Vite build locally |
npm test | test:react + test:functions + test:secrets |
npm run test:react | Vitest + React Testing Library suite under tests/react/ |
npm run test:functions | Node test runner over tests/functions.test.js (Cloud Functions logic) |
npm run test:secrets | Scan tracked files for committed API keys, OAuth tokens and private keys |
npm run test:coverage | Vitest with V8 coverage |
npm run test:rules | Firestore security-rules tests on the emulator (scripts/test-rules.sh, Java 21+) |
npm run test:e2e | Build with VITE_E2E_MODE=true, then run Playwright (Chromium) against it |
npm run lint | ESLint over the repo |
npm run deploy | Hosting + Firestore rules + Storage rules via op-firebase-deploy |
npm run deploy:functions | Cloud Functions only |
npm run deploy:all | Everything in firebase.json |
| Variable | Where | Purpose |
|---|---|---|
VITE_FIREBASE_* | .env.local (from .env.example, or .env.tpl via 1Password) | Firebase web config; turned into firebase-config.local.js by scripts/generate-firebase-config.js |
VITE_LOGODEV_KEY | .env.local (optional) | Enables the Logo.dev fallback for bill logos |
VITE_USE_EMULATORS | .env.local (optional, dev only) | true connects the dev server to local Firebase emulators |
VITE_E2E_MODE | set by npm run test:e2e (.env.e2e) | Swaps Firebase for injected fixture data so Playwright runs without a backend |
RESEND_API_KEY | Cloud Functions secret | Resend API key used by processMailQueue |
src/
app/
App.jsx routes (React Router v7)
main.jsx entry point
components/ shared UI: SettlementBoard, dialogs, TipTap editor nodes, NavBar, toasts
contexts/ AuthContext, ToastContext
hooks/ useBillingData, useDisputes, useRefundNotices
views/ Dashboard, Manage (five tabs), Settings, Login, Share
lib/ framework-free domain logic and services
BillingYearService.js owns billing-year state (useSyncExternalStore)
SaveQueue.js serialized saves
persistence.js buildSavePayload allowlist (ADR 0008)
calculations.js shares, balances, Credits, owed adjustments
share.js share-link payloads and scopes
invoice.js, mail.js, sms.js, template-doc.js invoicing
ChargeNoticeService.js, RefundNoticeService.js, ShareLinkService.js
functions/
index.js HTTP Cloud Functions and the mail-queue trigger
billing.js server-side billing math for share pages
tests/
react/ Vitest component, hook, view and lib tests
functions.test.js Cloud Functions tests (node --test)
rules/ Firestore rules tests (emulator)
e2e/ Playwright specs and fixtures
test_*.sh / *.py review-tooling tests (shared mergepath scripts)
specs/ feature specifications and acceptance criteria
docs/adr/ architecture decision records
plans/ design artifacts, including the redesign wireframes
scripts/ build, config, secret-scan and review-policy tooling
firestore.rules, storage.rules, firebase.json, design-tokens.css
| Route | Screen | Access |
|---|---|---|
/login | Sign in / create account / forgot password | Signed-out only |
/dashboard | Lifecycle bar, KPIs, settlement board | Signed in (/ redirects here) |
/manage/members | Members and Households | Signed in |
/manage/bills | Bills, logos, Service Credits, history | Signed in |
/manage/invoicing | Invoice template editor and test email | Signed in |
/manage/reviews | Review Requests | Signed in |
/manage/refunds | Refund Notices | Signed in |
/settings | Billing Controls and Payment Methods | Signed in |
/share?token=… | Member's Household billing summary | Public (token) |
Legacy paths (/app/*, /login.html, /share.html, /site/*) 301-redirect to the current routes.
All run in us-central1. The HTTP functions are reached through Hosting rewrites of the same name (for example /resolveShareToken).
| Function | Trigger | Purpose |
|---|---|---|
resolveShareToken | HTTP | Validate a share token and return the live Household summary for its scopes; refreshes the publicShares cache |
requestShareLink | HTTP | From an expired or revoked link, email the Administrator a request for a new one (rate-limited) |
submitDispute | HTTP | Create a Review Request from a share link (disputes:create) |
getEvidenceUrl | HTTP | Return a short-lived signed URL for Review Request evidence |
submitDisputeDecision | HTTP | Record a member's acceptance or rejection of a resolution |
submitRefundConfirmation | HTTP | Record a member's "received" / "not received" answer on a Refund Notice |
processMailQueue | Firestore mailQueue document created | Validate sender, recipient and rate limit, render HTML, send through Resend |
The share-page functions append to a server-only auditLog under the Administrator's user document (share views, Review Requests, decisions, refund confirmations).
| Path | Contents | Access |
|---|---|---|
users/{uid} | activeBillingYear | Owner |
users/{uid}/billingYears/{yearId} | One billing year (see Data Structure) | Owner |
users/{uid}/billingYears/{yearId}/disputes/{id} | Review Requests, Refund Notices and Charge Notices | Owner; members via Cloud Functions |
users/{uid}/auditLog/{id} | Server-written audit entries | Owner read; server write |
shareTokens/{sha256} | Share-link metadata: owner, member, year, scopes, expiry, revocation | Owner |
publicShares/{sha256} | Cached share-page payload | Get by hash; owner list; guests update access counters only |
publicQrCodes/{uid}_{id} | Payment-method QR images | Get by ID; owner list and write |
mailQueue/{id} | Outbound email awaiting processMailQueue | Client create (own uid, pending, fixed field allowlist); owner read |
Storage holds Review Request evidence under the owner's path (storage.rules).
BillingYearService reads the active billing year once when you sign in or switch years, and saves by overwriting the whole year document (setDoc without merge, from the buildSavePayload allowlist in src/lib/persistence.js; see ADR 0008). Saves go through a serialized SaveQueue. There is no live listener on the year document, so two tabs or devices editing the same year are last-writer-wins (#463); reload to pick up changes made elsewhere. Closed and Archived years are never saved. On load, missing fields get defaults and legacy email-template tokens are renamed.
publicQrCodes collection; the year document stores only a hasQrCode flagmailQueue collection; the processMailQueue Cloud Function renders HTML from the message body and sends it through Resend, with a plain-text fallbacksms: deep links; no SMS is sent by the app itselfEach billing year is one document at /users/{userId}/billingYears/{yearId}; /users/{userId} holds activeBillingYear. Review Requests, Refund Notices and Charge Notices live in the year's disputes subcollection, distinguished by kind (ADR 0002). The year document, as written by buildSavePayload:
{
label: "2026",
status: "open", // "open" | "settling" | "closed" | "archived"
createdAt: Timestamp,
closedAt: Timestamp | null,
archivedAt: Timestamp | null,
familyMembers: [
{
id: number,
name: string,
email: string,
phone: string, // E.164 format (e.g. "+14155551212")
avatar: string, // base64 data URL (legacy; no upload in the current app)
paymentReceived: number, // legacy counter, defaults to 0
linkedMembers: [number] // Linked Member IDs (set on the Primary Member)
}
],
bills: [
{
id: number,
name: string,
amount: number, // canonical amount as entered
billingFrequency: string, // "monthly" | "annual"
website: string,
logo: string, // base64 data URL
members: [number] // member IDs assigned to bill
}
],
payments: [
{
id: string,
memberId: number,
amount: number, // negative for reversals
receivedAt: string, // ISO 8601
note: string,
method: string, // "venmo" | "zelle" | "cash" | etc.
reversed: boolean, // true if reversed by a later entry
type: string, // "reversal" for reversal entries
reversesPaymentId: string // original payment ID (reversals only)
}
],
creditAdjustments: [ // Credit dispositions; outside the payments ledger
{
id: string,
memberId: number, // the Household's Primary Member
type: string, // "refund" | "carry_forward"
amount: number,
method: string, // refunds only
reason: string,
status: string, // "recorded" | "cancelled"
toYear: string, // carry_forward only
createdAt: string // ISO 8601
}
],
owedAdjustments: [ // signed modifiers of what a member owes
{
id: string,
memberId: number,
kind: string, // "usage_charge" | "service_credit" | "carry_opening"
amount: number,
description: string, // usage_charge
reason: string, // service_credit
billId: number, // service_credit
incurredDate: string,
status: string, // usage_charge: "deferred" | "billed" | "carried_forward"
// service_credit: "active"; carry_opening: "carried_in"
chargeNoticeId: string, // billed usage charges
createdAt: string // ISO 8601
}
],
billingEvents: [ // append-only event ledger
{
id: string,
timestamp: string, // ISO 8601
actor: { type, userId },
eventType: string, // BILL_CREATED, PAYMENT_RECORDED, REFUND_ISSUED, etc.
payload: object,
note: string,
source: string // "ui" | "system" | "migration"
}
],
settings: {
emailMessage: string, // plain-text template
emailMessageDocument: object, // TipTap document for the template
emailSubject: string,
paymentLinks: [], // legacy
paymentMethods: [ // structured payment methods (QR image stripped; hasQrCode flag kept)
{ id, type, label, enabled, handle, url, phone, email, instructions, name, address, hasQrCode }
]
},
updatedAt: Timestamp
}
Shares are computed from each bill's annual total divided by the number of members on it. Dollar amounts are rounded to cents only for display, so a monthly figure × 12 can differ from the annual figure by a few cents.
T-Mobile Bill ($300/month):
Apple One ($37.95/month):
/users/{userId} and its subcollections; everything not explicitly allowed is deniedshareTokens and publicShares documents are keyed by its SHA-256 hash. Anyone can fetch a publicShares document by that hash, but only the owner can list them, and guests may update only the access counterssummary:read, paymentMethods:read, usageCharges:read and payments:read. Links minted before usageCharges:read or payments:read existed keep their original scopes and omit those sections until they are regenerated; disputes:create and disputes:read are optional, and refunds:read is granted only on Refund Notice confirmation links. The Cloud Functions reject revoked, expired or out-of-scope requests.env.local (gitignored) as VITE_FIREBASE_* environment variables; scripts/generate-firebase-config.js turns it into the gitignored firebase-config.local.js bridge that sets window.__FIREBASE_CONFIG__.__/firebase/init.js..env.local, redeploy Hosting, verify the live build uses the new key only, then delete the old key.npm test includes a tracked-file secret scan so committed API keys, OAuth tokens, and private keys fail before deployment.firebase-tools, gcloud, and the canonical helper scripts from ../mergepath/scripts/ (see DEPLOYMENT.md § First-Time Setup).Private/GCP ADC source credential through the 1Password CLI, so routine deploy work does not need browser login once that item exists.op-firebase-setup friends-and-family-billing creates the deployer service account, grants deploy roles, and grants the current maintainer impersonation rights.npm run deploy, npm run deploy:functions, and npm run deploy:all use op-firebase-deploy, which creates a temporary impersonated credential for firebase-deployer@friends-and-family-billing.iam.gserviceaccount.com..env.tpl or config.runtime.tpl with op://Private/<item>/<field> references, then materialize gitignored runtime files with op inject -i <template> -o <runtime-file> -f.plans/ffb-wireframes.html is a self-contained wireframe document proposing a full visual and interaction redesign of the app. It draws every screen and its main dialogs at three widths—desktop 1280, tablet 834 and phone 390—lists the remaining dialogs that reuse the same components with different copy, and includes a token legend with computed WCAG contrast, plus numbered Redesign changes, each with a rationale, and a summary table. It adds no app code.
To view it, open the file directly in a browser (there is no build step), or use the rendered copy at raw.githack.com. Implementation is tracked in epic #524.
# macOS (use xdg-open on Linux, start on Windows)
open plans/ffb-wireframes.html
What the document covers, in order:
| Section | Screens drawn |
|---|---|
| Visual system | Tokens, type scale and the status palette, with computed WCAG contrast |
| Entry | Sign in and the email-verification notice |
| Dashboard | Year pill, lifecycle stepper and gate, KPIs, deferred charges, settlement board |
| Manage | Members, Bills, Invoicing, Review Requests, Refund Notices |
| Settings | Billing controls and payment methods |
| Share page | The member's billing summary (/share) |
| Dialogs | Email invoice, Text invoice, Share links, Review Request detail, Payment history, Edit payment, Record payment, Issue refund, Issue service credit, Add usage charge, Bill charges (Charge Notice), Bill history, Start new billing year, Confirmation, and the share page's QR, Request a review and Reject a resolution dialogs |
| System surfaces | Toasts and the update prompt |
| Summary | Every numbered Redesign change (01–25) with its home screen, the source file it touches and its rationale |
Each screen is drawn with the sample 2026 year (five Households) so the states line up across screens, and each Redesign change names the component it would change (for example, Redesign 01 targets src/app/components/SettlementBoard.jsx).
Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)firebase-config.local.js is missing or incomplete).env.local has correct Firebase config values, then regenerate firebase-config.local.jsThe app runs on Firebase Hosting, Cloud Firestore, Firebase Storage (Review Request evidence) and Cloud Functions, and sends email through Resend. What it costs depends on your Firebase plan and usage. Resend's free-tier limits are listed in DEPLOYMENT.md § Resend Free Tier Limits, and the app itself caps client-sent email at 100 messages per account per 24 hours.
plans/ffb-wireframes.html, self-contained wireframes proposing a full visual and interaction redesign: every screen and its main dialogs at desktop 1280, tablet 834 and phone 390, a token legend with computed WCAG contrast, and numbered Redesign changes with rationales and a summary table/site/) is removed from source, build, hosting, and testsbuild:react → build:assemble)tests/functions.test.jsclosedAt field is now preserved by the full-document save (see ADR 0008)/; legacy retained at /site/publicShares collection)MIT License - feel free to use this for your own bill-splitting needs!
Built with ❤️ for friends and families who split bills