Skip to content
返回项目教程

个人数据仪表盘

将多 API 个人数据聚合到自托管仪表盘,配备同步、图表和隐私。

设计与数据源

选择数据源、设计同步管道、选图表库。

为什么

个人仪表盘的价值在数据新鲜度和源覆盖。连接 5 个源(银行、健身、日历、GitHub、屏幕时间)配统一同步计划,提供单一应用无法给的视角。选错图表库(重 D3 包 vs 轻 uPlot)拖垮移动端加载性能。

何时使用

构建图表之前。花 1 周列源、测 API、设计统一数据模型。

如何做

1. 列出并优先排序数据源 - 一级(每日):银行交易、步数、屏幕时间 - 二级(每小时):GitHub 提交、日历事件、天气 - 三级(每周):邮件统计、音乐收听、睡眠摘要 - MVP 从 2-3 个一级源开始 2. 测试每个源的 API - 认证:OAuth(Strava、Google)、API key(GitHub)、屏幕抓取(银行无 API 时) - 限流:记录每源限制(Strava 100/15 分钟、GitHub 5000/小时) - 分页和增量同步:支持 ?since=lastSync 游标 3. 设计统一数据模型 - 每源映射到通用事件:{ source, timestamp, metric, value, meta } - 银行 -> { metric: "spend", value: -42.50, meta: { merchant, category } } - 健身 -> { metric: "steps", value: 8234, meta: {} } - 存 SQLite,索引 (source, timestamp) 4. 选图表库 - uPlot:40KB,时序最快,无交互(仅缩放) - ECharts:800KB 但全功能(地图、热力图、交互) - Recharts:React 原生,200KB,适合可组合仪表盘 - 按包预算选,非按功能数 5. 设计同步计划 - 每源按自然频率 cron(银行:6 小时、健身:1 小时) - 错开任务避免同时打所有 API - 每源标记 lastSync 和 nextSync 供 UI 用

示例

// 统一事件模型 interface Event { source: "bank" | "fitness" | "github" | "calendar" | "screentime"; timestamp: number; // unix 毫秒 metric: string; // "spend" | "steps" | "commits" | "focus_minutes" value: number; meta: Record<string, string | number>; } // SQLite schema CREATE TABLE events ( source TEXT NOT NULL, timestamp INTEGER NOT NULL, metric TEXT NOT NULL, value REAL NOT NULL, meta TEXT, -- JSON UNIQUE(source, timestamp, metric) ); CREATE INDEX idx_events_query ON events(source, timestamp); // 同步计划(错开) cron: [ { source: "fitness", every: "1h", offset: "0m" }, { source: "github", every: "1h", offset: "15m" }, { source: "bank", every: "6h", offset: "30m" }, { source: "screentime", every: "6h", offset: "45m" }, ];

常见问题

  • ⚠一次连 10 个源——各有认证怪癖,MVP 永不上线;从 2-3 个一级源开始
  • ⚠移动端用重图表库——ECharts 800KB 包手机加载 3 秒,用户在首图渲染前离开
  • ⚠无增量同步——每次拉全历史,撞限流浪费 API 预算

最佳实践

  • ✓从 2-3 个高价值一级源(银行 + 健身)开始,每周加一个——第 2 周上线 MVP
  • ✓按包预算选图表库(移动端 uPlot < 50KB)非功能数——加载时间决定使用
  • ✓每源支持增量同步(?since=lastSync)以尊重限流并保持同步快

实现与同步

构建同步任务、数据存储和响应式图表,配加载和过期态。

为什么

同步是难点——API 限流、token 过期、部分同步留旧数据。无幂等增量同步和清晰'最后更新'指示,仪表盘静默显示错误数据,用户对每个数字失去信任。

何时使用

数据模型设计后。建同步 + 存储 + 图表,再建调度和过期数据告警。

如何做

1. 建每源同步适配器 - 每适配器:{ auth, fetch(since): Event[], rateLimitInfo } - 适配器内处理 token 刷新(OAuth) - 返回限流 header 让调度器退避 2. 使同步幂等且增量 - 按 (source, timestamp, metric) upsert——重跑同步安全 - 传最后同步时间戳作游标:fetch(since = lastSync) - 每次限 1000 事件;源返回更多则分页 3. 优雅处理失败 - 401:刷新 token,重试一次,然后标记源"auth_expired" - 429:读 Retry-After header,重排任务,指数退避 - 5xx:带抖动重试 3 次,然后标记源"degraded"(显示旧数据带警告) 4. 建响应式图表 - 按时间范围查存储;订阅存储变更获实时更新 - 每源显示"2 分钟前更新"——过期指示建信任 - 首次同步时显示加载骨架;源降级时显示错误态 5. 加同步调度器 - BullMQ + Redis:每源一队列,按源频率重复任务 - 错开偏移使所有源不同秒触发 - 仪表盘:显示下次同步时间和每源"立即同步"按钮

示例

// 幂等增量同步(Strava 适配器) async function syncStrava(since: number) { const token = await refreshIfExpired("strava"); const activities = await fetch( `https://api.strava.com/athlete/activities?after=${since}`, { headers: { Authorization: `Bearer ${token}` } } ).then(r => r.json()); const events = activities.map(a => ({ source: "fitness", timestamp: a.start_date * 1000, metric: "steps", value: a.steps, meta: { type: a.type }, })); // upsert——重跑安全(UNIQUE 约束) await db.events.upsertMany(events, ["source", "timestamp", "metric"]); return { synced: events.length, lastSync: Math.max(...events.map(e => e.timestamp)) }; } // UI 中的过期指示 function SourceBadge({ source }) { const lastSync = useLastSync(source); // 响应式 const stale = Date.now() - lastSync > 2 * 3600_000; // 2 小时 return <span className={stale ? "stale" : "fresh"}> {stale ? "⚠️" : "✓"} 更新于 {timeAgo(lastSync)} </span>; }

常见问题

  • ⚠非幂等同步——重试任务插入重复事件,图表显示双倍计数
  • ⚠无过期指示——源静默失败显示 3 天前数据,用户信了错误数字
  • ⚠所有源同一 cron 点同步——5 个 API 同时打,限流触发同步失败

最佳实践

  • ✓按 (source, timestamp, metric) upsert 使重试同步幂等——重复是最常见仪表盘 bug
  • ✓每源显示'X 前更新'并标记过期(> 2 倍预期间隔)——信任需对新鲜度透明
  • ✓每源错开同步偏移使永不同时触发——尊重限流并平滑负载

部署与隐私

部署为自托管应用或本地优先站点,敏感个人数据加密存储。

为什么

个人仪表盘聚合敏感数据(银行、健康、位置)。多租户云主机是泄露目标和责任;自托管(Fly.io 单实例)或本地优先(SQLite + 同步)让数据留在你的基础设施。金融和健康指标的静态加密不可妥协。

何时使用

仪表盘用实时数据工作时。连接敏感源(银行、健康)前部署自托管。

如何做

1. 选部署模型 - 自托管 VPS(Hetzner $5/月):完全控制,数据归你 - Fly.io 单实例:易部署,缩到一,数据在挂载卷 - 本地优先(Tauri 应用 + SQLite):数据不离机,经用户云同步 2. 敏感数据静态加密 - events 表用 SQLCipher(SQLite + AES-256) - 加密密钥来自用户主密码(argon2id KDF) - 非敏感元数据(源名、同步时间)可不加密以快查询 3. 保护 OAuth token - 刷新 token 加密存数据库,绝不存 env 文件或明文 - 用每部署密钥(openssl rand 32)作加密密钥 - 按计划轮换密钥;轮换时重新加密 token 4. 锁定实例 - 前置 Cloudflare:WAF、限流和 TLS - 认证:单用户 Basic auth 或强密码会话 cookie - 无公开注册——这是个人应用非 SaaS 5. 备份和灾难恢复 - 每晚:加密 SQLite 文件上传到 Backblaze B2 或 S3 - 每月测恢复:下载、解密、对备份跑应用 - 文档化恢复流程,使丢 VFS 是 30 分钟恢复非数据丢失

示例

// SQLCipher:加密 SQLite import Database from "better-sqlite3"; const db = new Database("data.db"); db.pragma(`key = "${process.env.DB_ENCRYPTION_KEY}"`); // AES-256 // events 表现在静态加密 // Fly.io 部署(单实例,挂载卷) # fly.toml app = "my-dashboard" [mounts] source = "dashboard_data" destination = "/data" # 数据在卷上跨部署持久 // 每晚加密备份(定时) const dump = db.backup("/data/snapshot.db"); const encrypted = await encrypt(dump, process.env.BACKUP_KEY); await b2.upload("dashboard-backups/snapshot-" + date() + ".enc", encrypted); // 前置 Cloudflare:WAF + 限流 + TLS // 认证:单用户会话 cookie(无公开注册) app.use(session({ secret: process.env.SESSION_SECRET }));

常见问题

  • ⚠多租户云托管无加密——泄露暴露每个用户的银行和健康数据
  • ⚠OAuth 刷新 token 明文存 env 文件——泄露 .env 给银行 API 完全访问
  • ⚠无测试备份——VPS 磁盘死,2 年个人数据无恢复路径丢失

最佳实践

  • ✓events 表用 SQLCipher(AES-256),密钥从用户主密码派生——敏感数据静态加密
  • ✓OAuth 刷新 token 加密存数据库,绝不存 env 文件——泄露配置不得授予 API 访问
  • ✓每月测试备份恢复——未测试的备份是希望非恢复计划

完整开发者生命周期

从想法到维护的 14 阶段开发者路径图 — 可视化依赖关系、追踪进度。

下一步