2026年5月29日 星期五

一段用 Claude Code 對話式開發的記帳 App 建置紀錄。

緣起

我家人多年來都在用一個 Android 記帳 App「帳務小管家 ZERO v4.7」記帳。資料完整、分類細緻、運作穩定,但有兩個痛點:

  1. 版本停止更新:手機壞了或換手機,只能綁在 Android 手機上
  2. 無法多人共用:家庭開支想一起記就尷尬

15,000+ 筆累積的交易、十幾種帳戶、四十多個專案分類 — 捨不得換、也不想重來。所以決定:自己做一個能完整繼承原資料、加上雲端同步的版本。

App 取名 Song 帳本

設計取捨

幾個關鍵決定:

1. PWA(漸進式網頁應用)而不是原生 App

  • 一個網址就能開,不用上架商店、不用簽名
  • iOS / Android / 桌機平板通吃
  • 「加入主畫面」就有 App 般的全螢幕體驗

2. Offline-first(離線優先)

未登入時 = 純本機記帳,跟以前一樣;登入後 = 雲端同步「疊加」上去,可選哪本帳要同步。這樣家人不熟雲端也照樣能用,熟的可以多裝置同步、跟家人共用一本帳。

3. 單一 HTML 檔部署

最終打包成一個 1.1 MB 的 index.html — 內嵌 SQLite WASM 引擎、所有邏輯、CSS、字型。Cloudflare Pages 拖一下就更新,沒有 build 鏈、沒有 npm 依賴。

4. 後端用 Supabase 免費方案

  • Postgres + Auth + Realtime + Row Level Security 一站搞定
  • 免費額度 500 MB / 月,目前用不到 2%
  • 配 Gmail 自訂 SMTP 寄登入碼

完成的功能

類別 功能
記帳支出 / 收入 / 轉帳、兩層分類(大類 / 子項)、專案歸戶、計算機輸入
多帳本每本帳獨立儲存、互不干擾
多幣別外幣帳戶用原幣記、報表自動換算成台幣
雲端同步Email OTP 登入、Realtime 即時推播、Last-Write-Wins 衝突處理
離線佇列沒網路時照記、連上網自動補上傳
帳本分享擁有者 / 可編輯 / 唯讀三種權限
資料匯入可直接吃原 App 的 .db 檔案
報表月報、年報、趨勢、專案、行為分析(含 MoM / YoY)
雙版面手機單欄、桌機/平板自動切儀表板(左欄首頁常駐、右欄切換)

用 Claude Code 對話式開發

整個專案我幾乎沒有手寫一行程式碼 — 全程透過自然語言對話、由 Claude Code 動手實作。流程大致是:

  1. 我描述問題或丟一張截圖
  2. Claude 定位到對應程式碼、解釋現況、提方案
  3. 我確認方向 / 微調 / 反問
  4. Claude 動手改、跑語法檢查、必要時用瀏覽器 preview 看效果
  5. 我看一下截圖、決定要不要 commit
  6. Claude commit,留下精準的歷史

幾個讓我印象深刻的時刻:

  • 我發現「現金-美金餘額是負的」,貼了原 App 截圖。Claude 看了 .db 結構,發現匯入器把原幣金額跟歷史台幣換算混淆了,重寫成「存原幣 + 當筆匯率,報表用兩者相乘」,最後把基準數對到原 App 截圖到位數(美金 762.76 × 30 = 22,882.80)。
  • 月報「淨退」大類(退款 > 支出)的長條跑出畫面外,數字顯示「-0%」。Claude 找到根因(百分比沒鉗位),順手把其他三條長條也加上防禦性 Math.max(0,…)
  • 凌晨 0–8 點「複製交易到今天」會變成前一天的日期,因為 toISOString() 回的是 UTC 而台灣是 UTC+8。Claude 找到 todayStr() helper,改用本地時區,順便把另一處一樣 bug 的 wsStr 一起修。
  • 我抱怨「手機 vs 桌機螢幕差異很大、桌機兩側空空的」,Claude 幫我做了一個寬螢幕儀表板:CSS Grid 切兩欄、左欄首頁常駐、右欄跟著 tab 切換。實作過程中還抓到一個 bug — boot 時的 inline style display:block 蓋過了 media query 的 display:grid,整個 grid 沒生效。
  • 我問「原 App 的專案有 ID 和說明文字,你的匯入器抓了嗎?」Claude 去 query 了 mymoney.dbPROJECT_SET 表,發現確實有 PROJECT_NOTE 欄位被忽略了,補上之後還做了一個「從原 .db 單獨匯入專案說明」按鈕 — 不洗交易資料就能補上。

整個過程約 30+ commits、跨 4 天集中開發。 我幾乎不用碰程式碼,但每一個技術決策都掌握在自己手上。

一些有趣的細節

回沖(負數金額的支出)

原 App 允許「-300 元支出」當作沖回(例:同事退款)。匯入時數值處理正確、但顯示和正常支出沒區別 — 我以為被當成普通支出了。Claude 確認資料層面正確,視覺上改成:

  • 一般支出:-$300 紅色
  • 回沖:+$300 綠色(同一筆支出、用相反符號和顏色標示)

月支出合計自動扣回。

報表 MoM/YoY 邊界

當上月為負數(淨退)時,(curr - prev) / prev × 100 的公式會因為除以負數而符號翻轉,顯示「下降 3728%」之類的鬼話。改成:

  • prev ≤ 0 → 顯示「—」(基期無意義)
  • curr < 0 → 顯示「淨退」(當期是退款月)
  • 其他 → 正常 ▲/▼ 百分比

災難復原?

不需要設定自動備份。多裝置同步本身就是分散式備份 — 每台裝置的本地 IndexedDB 都是一份完整副本,Supabase 真的壞掉,任何一台同步中的裝置「啟用同步」就會 re-push 回去。

加上偶爾手動匯出 JSON 存到 OneDrive,就是雙保險。

結果

  • 主網址:https://song-ledger.song.tw/(自訂網域)
  • 部署:Cloudflare Pages(免費額度日 10 萬請求,用很少)
  • 後端:Supabase 免費方案(用不到 2%)
  • 家人在用、我自己也在用、跨裝置即時同步

順帶生了兩本手冊

  • 家庭使用手冊(PDF,9 頁,附截圖)
  • 管理者維運手冊(PDF + Markdown,記錄部署流程、雲端服務位置、疑難排解)

反思

  1. Offline-first 是正確選擇。家人不一定每天有網路、不一定想登入;不強迫雲端讓 App 對所有人都好用。
  2. 少即是多。沒有用任何 framework — React / Vue / Tailwind 一個都沒有,純手寫 vanilla JS / CSS。1.1 MB 一檔搞定,啟動快、改起來也快。
  3. AI 不會取代你的判斷。所有關鍵決定(要不要做 keep-alive、UI 走向、哪些 bug 先修、要不要寫自動備份)都是我做的;Claude 是執行 + 細節驗證的合作者。決策權永遠在你手上,這也是這套工作流最舒服的地方。
  4. 資料才是核心資產。花最多力氣的不是 UI、不是同步,是「正確匯入 15,000 筆歷史資料」這件事 — 對到位數、處理多幣別、回沖、專案說明,每一項都要驗證。

如果要再做一次,我會更早開始用 Claude Code 對話式開發 — 一開始我自己手寫了一些 code、改了又改、效率反而不高。


Stack 速查

技術
前端vanilla JS / CSS / HTML,單檔 1.1 MB
內嵌sql.js(SQLite WASM)讀原 App .db
後端Supabase(Postgres + Auth + Realtime + RLS)
部署Cloudflare Pages(自訂網域 song.tw)
同步Supabase Realtime postgres_changes + 離線 outbox
開發Claude Code 對話式
文件reportlab 產 PDF、Microsoft JhengHei 中文字型內嵌

寫於 2026 年 5 月底

2023年4月19日 星期三

[Link] Digital Access Adoption Program (DAAP)

Making the Move to Digital Access

Introducing DAAP

The Digital Access Adoption Program was announced at SAPPHIRE Now in May 2019. The DAAP was designed in close collaboration with user groups to provide the insights needed to enable customers to make the decision to move to the Digital Access licensing model with transparency and complete confidence.

https://discover.sap.com/digital-access/en-us/index.html#section_5

[Link] SAP Extends SAP Digital Access Adoption Program Through End of 2022

SAP recently announced that it had extended the Digital Access Adoption Program (DAAP) through December 31, 2022, giving SAP customers more time to make the decision to move to the SAP Digital Access licensing model, which provides customers with a modern, streamlined solution to SAP licensing.

[Link] SAP Insight: Indirect Access, SAP Digital Access and the DAAP

Indirect use of SAP can be a significant licensing risk. Is SAP’s Digital Access model the best way to manage this and should you take advantage of the Digital Access Adoption Program (DAAP) while it’s still available?

2014年8月21日 星期四

Correction of SD Document Status

1472007 - Document status has to be corrected

2014年7月21日 星期一

Reorganization of SD Document Index

Reorganization of SD Document Index

128947 - Correction of SD document indexes with RVV05IVB

2014年4月29日 星期二

How to read Report selection screen field values before PAI field transport

Function: DYNP_VALUES_READ

How to got the field value when you still in selection screen manipulating, example code as below:
AT SELECTION-SCREEN ON VALUE-REQUEST FOR p_vbeln.
*--- Update P_VSTEL
  dyfields-fieldname 'P_VSTEL'.
  APPEND dyfields.
  CALL FUNCTION 'DYNP_VALUES_READ'
    EXPORTING
      dyname     sy-cprog
      dynumb     sy-dynnr
    TABLES
      dynpfields dyfields.
  READ TABLE dyfields INDEX 1.

  IF dyfields-fieldvalue IS INITIAL.
    MESSAGE i899 WITH 'Please enter Shipping Point first!'.
    EXIT.
  ELSE.
    CLEARgr_vstel,  gr_vstel[],
           gt_lipov,  gt_lipov[],
           gt_dn,     gt_dn[].
    gr_vstel-sign   'I'.
    gr_vstel-option 'EQ'.
    gr_vstel-low    dyfields-fieldvalue.
    APPEND gr_vstel.
  ENDIF.

2014年3月28日 星期五

[Link] Timezone changes best practices

How to check the timezone at different levels

  • Login to SAP system client  
  • Run the report from Tcode SA38   -> TZCUSTHELP
  • Run the report from Tcode SA38   -> RSDBTIME
  • Run this function module in SE37   -> TZ_SYSTEM_GET_TZONE
  • Run this report from Tocde SA38  -> TZONECHECK 

2013年7月3日 星期三

[Link] Foreign Currency Valuation - Resetting Posting on Basis of Valuation History

Foreign Currency Valuation - Resetting Posting on Basis of Valuation History

After postings have been made, you can reset them at a later point in time. For this, the program determines the relevant foreign currency items (line items) and foreign currency balances (accounts) as well as any corresponding valuation differences from the entries in the valuation history. In the case of foreign currency items, the system also creates a reset posting if the items were cleared before or on the specified valuation key date. It is not possible prior to enhancement pack 5 to reset posting, if the items were cleared before or on the specified valuation key date. This new functionality is only available via Enhancement pack 5.
http://wiki.sdn.sap.com/wiki/display/ERPFI/Foreign+Currency+Valuation++-+Resetting+Posting+on+Basis+of+Valuation+History

[Link] Foreign Currency Valuation in SAP ECC 6

Foreign Currency Valuation in SAP ECC 6

This is the process to translate and adjust foreign currency amount of monetary accounts to local amount by a current suitable exchange rate (standard exchange rate).
http://mssmart77-sapfico.blogspot.tw/2012/08/foreign-currency-valuation-in-sap-ecc-6.html

2013年2月4日 星期一

[Code] How to catching selection criteria

How to catching selection criteria

Example: 
  DATAselection_table LIKE rsparams OCCURS 0  WITH HEADER LINE.

  CLEAR selection_tableREFRESH selection_table.

* Get selection criteria
  CALL FUNCTION 'RS_REFRESH_FROM_SELECTOPTIONS'

[Code] How to reference whole itab by name

How to reference whole itab by name

Example: clearing all selection criteria
FORM screen_clearing_all .
  DATAselctab         LIKE rsscr    OCCURS 20 WITH HEADER LINE,
        itabname        TYPE c        LENGTH 30.

  FIELD-SYMBOLS<para> TYPE ANY,
                 <sele> TYPE ANY TABLE.

  CLEAR selctabREFRESH selctab.

* Get selection screen

2013年1月31日 星期四

[Code] How to use subquery to get correct record


Suppose ZSD001 is filled as below:
MATNR         DATAB           APPOINTFEE
----------------------------------------
MA12345       2008/12/01      500
MA12345       2009/12/01      1,000
MA12345       2009/12/15      2,000
Example Code:
SELECT SINGLE APPOINTFEE INTO GT_ITAB-APPOINTFEE
    FROM ZSD001
   WHERE MATNR = 'MA12345'
     AND DATAB = ( SELECT MAX( DATAB )
                       FROM ZSD001
                      WHERE MATNR EQ 'MA12345'
                        AND DATAB LE '20091202'
                 ).

* GT_ITAB-APPOINTFEE == 1,000

Another example: (paste from RVAUFSTA)
SELECT OBJNR FROM JSTO AS APPENDING CORRESPONDING FIELDS OF
                     TABLE ONR_TAB WHERE A~OBJNR LIKE SDCON-OBJNR_VB AND
                                         A~OBJNR IN R_S_OBJNR AND
                                         A~OBTYP EQ SDCON-HEADER AND
                                         A~STSMA NE STSMA_INI AND
             EXISTS SELECT OBJNR FROM JEST WHERE OBJNR EQ A~OBJNR AND
                     STAT LIKE 'E%' AND INACT EQ SPACE ORDER BY OBJNR.