无Backend,无借口:构建不出卖你的Pain Tracker

发布: (2025年12月7日 GMT+8 16:16)
5 min read
原文: Dev.to

Source: Dev.to

概述

Pain Tracker 是一款完全运行在用户设备上的慢性疼痛管理应用。
没有账号、没有云同步,也没有遥测——你的健康数据仅保留在设备上,或根本不存在。

典型的“免费”健康应用会要求你创建账号并将数据同步到外部服务器,暴露你的疼痛程度、用药历史以及最糟糕的日子给保险公司、数据泄露或法律传票。最需要疼痛追踪的人——慢性病患者、残疾赔偿申请者、工伤赔偿争议者以及经历医疗创伤的人——往往也是最容易受到这些风险侵害的群体。

我之所以构建 Pain Tracker,是因为我亲身经历过健康数据在法庭上被用来对付我。这个应用让你掌控自己的数据。


架构

┌─────────────────────────────────────────────────────────────┐
│                    YOUR DEVICE                              │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │  React UI   │ →  │   Zustand   │ →  │  IndexedDB  │     │
│  │             │    │             │    │ (Encrypted) │     │
│  └─────────────┘    └─────────────┘    └─────────────┘     │
│                            ↓                                │
│                   ┌─────────────┐                           │
│                   │  PDF/CSV    │ → Your doctor. Your call. │
│                   └─────────────┘                           │
└─────────────────────────────────────────────────────────────┘

关键点

  • 没有云、没有服务器、没有遥测。 数据永远不会离开设备,除非你手动导出。
  • Zustand 与 Immer 提供不可变更新和完整的审计轨迹。
  • IndexedDB 本地存储加密数据。

状态管理

import { create } from 'zustand';
import { devtools, persist, subscribeWithSelector } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

export const usePainTrackerStore = create()(
  subscribeWithSelector(
    persist(
      devtools(
        immer((set) => ({
          entries: [],

          addEntry: (entryData) =>
            set((state) => {
              state.entries.push({
                id: crypto.randomUUID(),
                timestamp: new Date().toISOString(),
                version: 1,
                ...entryData,
              });
            }),
        }))
      ),
      {
        name: 'pain-tracker-storage',
        storage: createJSONStorage(() => localStorage),
      }
    )
  )
);
  • 每条记录都包含 版本号 和时间戳。
  • 不可变更新使得在任意时间点证明数据的样子变得容易。

客户端加密

// Generate a fresh IV for each encryption
const iv = crypto.getRandomValues(new Uint8Array(12));

const ciphertext = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv },
  encryptionKey,
  plaintextBytes
);
  • 通过 Web Crypto API 实现 AES‑256‑GCM
  • 没有外部依赖。
  • 解密时进行 HMAC 验证。
  • 基于密码的密钥使用 150,000 次 PBKDF2 迭代(这是使暴力攻击不切实际的最低要求)。
    完整的加密实现详见第 2 部分。

疼痛发作的自适应界面

const activateEmergencyMode = useCallback(() => {
  updatePreferences({
    simplifiedMode: true,
    touchTargetSize: 'extra-large', // 72 px
    autoSave: true,
    showMemoryAids: true,
  });
}, [updatePreferences]);

危机检测 监控以下情况:

  • 疼痛等级突升(≥ 7)
  • 高错误率(认知雾)
  • 输入模式异常(压力)

当检测到发作时,UI 会自动:

  • 切换到简化布局
  • 增大按钮尺寸
  • 减少选项
  • 开启每次更改自动保存

所有数据仍然保留在设备上,且可以随时删除。
完整的 hooks 实现详见第 3 部分。

纤维肌痛评估(ACR 2016 标准)

export function calculateFibromyalgiaScore(
  painLocations: string[],
  symptomScores: SymptomScores
): FibromyalgiaAssessment {
  const wpi = painLocations.length; // 0‑19
  const sss =
    symptomScores.fatigue +
    symptomScores.wakingUnrefreshed +
    symptomScores.cognitiveSymptoms; // 0‑12

  const meetsCriteria =
    (wpi >= 7 && sss >= 5) ||
    (wpi >= 4 && wpi = 9);

  return {
    wpiScore: wpi,
    sssScore: sss,
    meetsFibromyalgiaCriteria: meetsCriteria,
    assessmentDate: new Date().toISOString(),
  };
}
  • 提供 经过验证的评估(而非诊断),可生成供临床医生使用的文档。
  • 用来取代“全身疼痛”之类的模糊描述。

为工伤赔偿(WCB)导出

interface WCBExportOptions {
  format: 'csv' | 'json' | 'pdf';
  dateRange: { start: Date; end: Date };
  includeMetadata: boolean;
  wcbClaimNumber?: string;
}

export async function exportForWCB(
  entries: PainEntry[],
  options: WCBExportOptions
): Promise {
  const filtered = entries.filter(
    (e) =>
      new Date(e.timestamp) >= options.dateRange.start &&
      new Date(e.timestamp) 

值得阅读的文件

  • src/services/EmpathyIntelligenceEngine.ts – 启发式疼痛分析(2,076 行)
  • src/services/EncryptionService.ts – 客户端加密实现
  • src/components/accessibility/ – 创伤知情 hooks
  • src/stores/pain-tracker-store.ts – 带审计轨迹的状态管理

结论

仍然处于住房不稳定的阶段。仍在持续发布。
取其有用之处,改进它,构建更好的东西。

Back to Blog

相关文章

阅读更多 »

在 LLM 聊天 UI 中追求 240 FPS

TL;DR 我构建了一个 benchmark suite 来测试在 React UI 中 streaming LLM responses 的各种优化。关键要点:1. 首先构建合适的 state,然后再进行优化……