feat: Windows download link + self-served update feed + install tracking

Downloads/updates:
- config: DOWNLOADS_DIR (override to a mounted volume in prod).
- static.js: serves /downloads/* (installer, latest.yml, .blockmap) with range support for
  resumable + differential auto-updates; /download/windows redirects to the current .exe
  (stable link). Landing page gets a "Download the Windows desktop app" button (hidden in-app).

Install tracking (who installed the app):
- db app_installs + repos.appInstalls (upsert by install_id, fills in the user on sign-in).
- POST /api/v1/telemetry/install (records install + user once authenticated);
  GET /api/v1/admin/installs (admin: list installs with user/version/os/last-seen).
- desktop main.js: stable per-install id in userData, exposed via preload
  (bizConnectNative.installId/version/os); home.html reportInstall() posts it after login.
- e2e: +2 checks (telemetry recorded, admin sees it). 119/119. build batch20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:28:53 +05:30
parent e5c94ebf6d
commit 899770ed02
11 changed files with 143 additions and 4 deletions
+17 -1
View File
@@ -299,4 +299,20 @@ const deviceTokens = {
removeByToken: (token) => db.prepare('DELETE FROM device_tokens WHERE token=?').run(token),
};
module.exports = { teams, users, authSessions, machines, audit, sessionsLog, refreshTokens, apiKeys, webhooks, messages, reactions, attachments, conversations, scheduledMeetings, recordings, polls, pollVotes, pushSubs, favorites, deviceTokens };
const appInstalls = {
// Upsert by install_id: each launch refreshes version/os/last_seen and fills in the user once
// they sign in (COALESCE keeps a known user if a later anonymous ping arrives).
record: ({ id, installId, userId, userEmail, tenantId, platform, appVersion, os }) =>
db.prepare(`INSERT INTO app_installs (id,install_id,user_id,user_email,tenant_id,platform,app_version,os,first_seen,last_seen)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(install_id) DO UPDATE SET
user_id=COALESCE(excluded.user_id, app_installs.user_id),
user_email=COALESCE(excluded.user_email, app_installs.user_email),
tenant_id=COALESCE(excluded.tenant_id, app_installs.tenant_id),
platform=excluded.platform, app_version=excluded.app_version, os=excluded.os,
last_seen=excluded.last_seen`)
.run(id, installId, userId || null, userEmail || null, tenantId || null, platform || null, appVersion || null, os || null, now(), now()),
listForTenant: (tenantId) => db.prepare('SELECT * FROM app_installs WHERE tenant_id=? ORDER BY last_seen DESC').all(tenantId),
};
module.exports = { teams, users, authSessions, machines, audit, sessionsLog, refreshTokens, apiKeys, webhooks, messages, reactions, attachments, conversations, scheduledMeetings, recordings, polls, pollVotes, pushSubs, favorites, deviceTokens, appInstalls };