Money-Log
A lightweight desktop expense tracker built with Electron + React + TypeScript. Same OpenLogos methodology, different AI tool and tech stack.
.opencode/plugins/openlogos.js with commands/ hooks/ for methodology-driven development in the terminal. Requirements — Who needs this and why?
Source: logos/resources/prd/1-product-requirements/01-requirements.md
User Persona & Pain Points
Working professionals, age 24-50 — monthly income 5K-30K CNY. No active bookkeeping habit, only passively check Alipay/WeChat transaction history. Need quick recording, categorized spending, and consumption trend insights.
Scenario Overview
| ID | Scenario | Trigger | Pain | Priority |
|---|---|---|---|---|
| S01 | Quick expense logging | User opens app to record | P02 | P0 |
| S02 | View statistics report | User enters statistics page | P01, P03 | P0 |
| S03 | Manage expense categories | User manages categories | P01 | P1 |
| S04 | Set password lock | User enables/disables lock | P04 | P1 |
Acceptance Criteria — S01: Quick Expense Logging
Product Design — What will users see and do?
Source: logos/resources/prd/2-product-design/1-feature-specs/01-main-design.md
Page Architecture
┌─────────────────────────────────────────┐
│ Money-Log Logo │
├─────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Log │ │ Stats │ │ Settings│ │
│ │(default)│ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ [Page Content Area] │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
Window: 800 x 600 px (min: 600 x 500)
3 tabs: Log / Statistics / Settings UI Prototype — Desktop App
Source: logos/resources/prd/2-product-design/2-page-design/01-prototype.html
Interactive HTML prototype with all 3 tabs (Log / Statistics / Settings) — designed before any code is written.
S01 Interaction — Quick Logging Flow
- App opens → display default logging page with category grid (8 preset categories with emoji icons)
- User taps a category → highlight selected, others dim
- User enters amount using numeric keyboard → real-time validation, filter non-numeric chars
- User optionally adds a remark (max 200 chars)
- Click "Log" → save to SQLite → show "Recorded" toast → clear amount, keep category
| Field | Type | Required | Validation |
|---|---|---|---|
| Category | Icon grid selection | Yes | Must select one |
| Amount | Numeric input | Yes | > 0, max 999999.99 |
| Remark | Text input | No | Max 200 chars |
Implementation — From architecture to verified code
Architecture Overview
Source: logos/resources/prd/3-technical-plan/1-architecture/01-architecture-overview.md
graph TB
subgraph Renderer["Renderer Process (React)"]
UI["React Components
UI Layer"]
State["Zustand Store
State Management"]
IPC["IPC Bridge
Process Communication"]
end
subgraph Client["Client (Electron Main Process)"]
Main["Main Process
Backend Logic"]
DB["SQLite Database
Local Storage"]
Store["electron-store
Config Storage"]
end
UI --> State
State --> IPC
IPC --> Main
Main --> DB
Main --> Store UI in Renderer (React + Zustand), data in Main Process (better-sqlite3). IPC bridge handles all communication. Password lock config stored in electron-store separately from expense data.
Scenario → Sequence Diagram
Source: logos/resources/prd/3-technical-plan/2-scenario-implementation/S01-快速记账.md
S01: Quick Expense Logging — Sequence Diagram
sequenceDiagram
participant U as User
participant UI as React UI
participant Store as Zustand Store
participant IPC as IPC Bridge
participant DB as SQLite Database
U->>UI: Step 1: Select category
UI->>UI: Step 2: Update selection — highlight active
UI->>U: Step 3: Show selected state
U->>UI: Step 4: Enter amount via keyboard
UI->>UI: Step 5: Validate format — filter invalid chars
UI->>UI: Step 6: Enable/disable button by validity
UI->>U: Step 7: Display amount
U->>UI: Step 8: Click "Log" button
UI->>Store: Step 9: Submit {amount, category_id}
Store->>IPC: Step 10: saveRecord(amount, category_id)
IPC->>DB: Step 11: INSERT INTO records
DB-->>IPC: Step 12: Return write result
IPC-->>Store: Step 13: Return success/failure
Store-->>UI: Step 14: Update state
UI->>U: Step 15: Show "Recorded" toast
UI->>UI: Step 16: Clear amount input
UI->>UI: Step 17: Keep category selected
UI->>U: Step 18: Ready for next entry Steps
- User taps a category label (e.g. "Dining").
- React UI updates selected state, visually highlighting the current category.
- UI shows selection feedback to user.
- User enters amount via keyboard.
- React UI validates input format, filtering non-numeric characters.
- React UI enables/disables the "Log" button based on input validity (disabled when no category selected or amount is 0).
- UI displays the entered amount in real time.
- User clicks the "Log" button to submit.
- Zustand Store receives expense data
amount, category_id. - Store calls
saveRecord()via IPC to the main process. - SQLite executes
INSERT INTO records. - Database returns write result (success or error).
- IPC returns result to Store.
- Store updates state, notifying UI.
- UI shows "Recorded" toast notification.
- React UI clears the amount input field for the next entry.
- React UI keeps category selected to reduce repeated selection.
- UI enters ready state, waiting for the next expense entry.
Exception Cases
API Specs + DB Schema
API Specification — Electron IPC Channels
Source: logos/resources/api/local-api.yaml
Database Schema — SQLite Tables
Source: logos/resources/database/schema.yaml
Test Case Design (before code!)
Source: logos/resources/test/S01-test-cases.md
S01 Test Cases — Unit Tests (excerpt)
| ID | Description | Input | Expected |
|---|---|---|---|
UT-S01-01 | Amount is required | {} | 400: missing required field |
UT-S01-02 | Amount must be > 0 | {amount: 0} | 400: amount must be > 0 |
UT-S01-05 | Category ID must exist in DB | category_id: 999 | 400: category not found |
UT-S01-06 | Remark max 200 chars | "a".repeat(201) | 400: remark too long |
UT-S01-10 | Amount truncated to 2 decimals | 12.345 | Stored as 12.34 |
UT-S01-11 | Non-numeric chars filtered | "abc" | Filtered to empty |
S01 Scenario Tests (excerpt)
| ID | Description | Covers |
|---|---|---|
ST-S01-01 | Full logging flow: select → enter → save → toast | Steps 1-18 |
ST-S01-02 | Consecutive logs with same category | Steps 1-18 |
ST-S01-05 | Invalid amount format rejected | EX-5.1 |
Code Generation — Design-driven implementation via OpenCode
Source: src/main/database.js · src/renderer/pages/ · src/__tests__/
OpenCode reads the sequence diagram and API spec via slash commands, then generates Electron main process + React frontend + Vitest tests in batch. Each batch closes the loop: business code → test code → OpenLogos reporter.
Data Layer — Electron Main Process → maps to S01 Steps 10-13
class DatabaseManager {
// S01 Steps 10-13: Save a new expense record
saveRecord(amount, category_id, remark = null, created_at = null) {
const amountInCents = Math.round(amount * 100);
const timestamp = created_at || new Date().toISOString();
this.db.run(
`INSERT INTO records (amount, category_id, remark, created_at)
VALUES (?, ?, ?, ?)`,
[amountInCents, category_id, remark, timestamp]
);
const result = this.db.exec('SELECT last_insert_rowid() as id');
this.save();
return result[0].values[0][0];
}
}Unit + Scenario Tests → verifies S01 acceptance criteria
describe('S01: Quick Logging - Unit Tests', () => {
describe('UT-S01-01: Amount is required', () => {
it('should reject empty amount', async () => {
const data = {};
const hasAmount = data.amount !== undefined;
expect(hasAmount).toBe(false);
});
});
describe('UT-S01-10: Amount truncated to 2 decimals', () => {
it('should auto-truncate to 2 decimal places', () => {
const amount = 12.345;
const formatted = Math.floor(amount * 100) / 100;
expect(formatted).toBe(12.34);
});
});
});
describe('S01: Quick Logging - Scenario Tests', () => {
describe('ST-S01-01: Full logging flow', () => {
it('should correctly convert amount to cents', () => {
const amount = 35.5;
const amountInCents = Math.round(amount * 100);
expect(amountInCents).toBe(3550);
});
});
});openlogos verify — Automated Acceptance
Source: logos/resources/verify/acceptance-report.md
Design-time Coverage Assertions
Change Tracking — Every iteration is impact-aware
Traditional AI coding ("vibe coding") fixes the code but forgets the design doc, the API spec, and the test cases. Over time, documentation drifts from reality and becomes useless. OpenLogos solves this with a structured delta workflow — every change, no matter how small, must evaluate its ripple effect across the entire artifact chain.
openlogos mergeChange Propagation Rules
The change type determines the minimum update scope. A seemingly small feature request can cascade through the entire chain:
Real Example — Feature: Add Remark to Expense Logging
Source: logos/changes/archive/每天记账增加备注功能/proposal.md
Add remark field to expense logging
Request: Users need to add notes to expenses (e.g. "hotpot dinner", "taxi to hospital"). The DB column remark already exists — but the front-end UI had no input field, and the IPC channel didn't pass the parameter through.
Impact Analysis
save-record channel — add remark param (string, optional, max 200 chars)records.remark column already exists (TEXT, nullable)More Archived Proposals
Money-Log has 8 archived change proposals — each following the same structured delta workflow.
Fix: password not persisted
Code-level fix — electron-store write not awaited before close.
Monthly spending detail view
Design-level — new list view under statistics chart.
Want to try it yourself?
Clone the repo and run Money-Log locally, or start your own project with OpenLogos.