Skip to content

Fix provider switching, fresh install defaults, and health dashboard improvements#33

Merged
atomantic merged 7 commits intomainfrom
dev
Feb 27, 2026
Merged

Fix provider switching, fresh install defaults, and health dashboard improvements#33
atomantic merged 7 commits intomainfrom
dev

Conversation

@atomantic
Copy link
Owner

@atomantic atomantic commented Feb 27, 2026

Summary

  • Fix "Provider not found" when changing default AI providerPUT /active was caught by the wildcard PUT /:id Express route, treating "active" as a provider ID and returning 404. Added explicit PUT /active route before the wildcard. (fixes changing default AI provider to LM Studio - error #32)
  • Fix CoS auto-starting on fresh installs — Sample state now sets alwaysOn: false so the Chief of Staff daemon doesn't immediately queue agent tasks on new machines without configured CLIs. Also cleared the sample pending task and removed placeholder example apps from data.sample/apps.json.
  • Health dashboard features — Config-driven metric cards with 50+ metrics across 8 categories, Apple Health clinical records (FHIR) import, and split Blood & Body into separate Age/Blood/Body/Eyes pages.
  • Apple Health ZIP upload — Upload the export ZIP directly instead of manually extracting export.xml.

Test plan

  • All 1206 server tests pass
  • Client builds successfully
  • Verify switching default AI provider no longer returns 404
  • Verify fresh install with data.sample/ copies correct defaults (no example apps, CoS not auto-starting)
  • Verify health dashboard loads with metric categories and clinical records

Only the root package-lock.json should be committed — client/, server/,
and browser/ lock files are now gitignored to reduce merge noise.
Accept Apple Health export ZIP files directly — server extracts
export.xml via streaming (unzipper) without loading the full archive
into memory. Remove unused meatspace TSV import (endpoint, service,
tests, validation, UI).
Replace hardcoded StepsCard/HeartRateCard/HrvCard with a generic
MetricCard + registry of 50+ metrics across 8 collapsible categories.
Adds available metrics discovery endpoint that samples across full
history to catch sparse data (e.g. Withings body composition).
Deep-linkable via useSearchParams. Move Alcohol/HRV correlation to
Alcohol tab. Fix units to match Apple Health's actual stored format.
…w latest metric values

Parse FHIR R4 JSON files from Apple Health ZIP exports to extract lab
observations (CBC, metabolic panel, lipids, thyroid) with LOINC code
mapping. Non-destructive merge preserves existing manual blood test entries.

Also adds latest-value fallback for health metric cards and uses Apple
Health body composition data in the overview when more recent than manual
entries.
Each health domain now has its own dedicated tab and sidebar entry
with distinct icons, improving navigation and reducing page bloat.
)

PUT /active was caught by the wildcard PUT /:id route, treating "active"
as a provider ID. Added explicit PUT /active route before the wildcard.
Also added error handling to handleSetActive on the client side.
- Set alwaysOn: false in sample state.json so CoS doesn't immediately
  spawn agents on new machines without configured CLIs
- Remove example pending task from sample TASKS.md
- Remove placeholder example apps from sample apps.json, keep only PortOS
Copilot AI review requested due to automatic review settings February 27, 2026 18:56
@atomantic atomantic merged commit d7bcba3 into main Feb 27, 2026
7 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes AI provider switching by correcting an Express route collision, improves fresh-install defaults to avoid unintended Chief-of-Staff activity, and significantly expands Apple Health/MeatSpace health dashboard capabilities (ZIP upload + many more metrics + new focused pages).

Changes:

  • Fix provider switching (PUT /providers/active) by adding an explicit route before the wildcard provider route.
  • Improve Apple Health ingestion: accept ZIP uploads, expand XML metric mappings, add available/latest metrics endpoints, and add clinical records (FHIR) import into blood-tests.json.
  • Refactor MeatSpace UI navigation by splitting “Blood & Body” into separate Age/Blood/Body/Eyes tabs and updating overview tile routing; adjust sample data defaults for fresh installs.

Reviewed changes

Copilot reviewed 41 out of 46 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
server/services/meatspaceImport.test.js Removed TSV import tests (feature removal).
server/services/meatspaceImport.js Removed TSV import service implementation.
server/services/meatspaceHealth.js Adds helper to persist blood-tests data.
server/services/appleHealthXml.js Expands XML→metric mapping and adds batch flushing to disk.
server/services/appleHealthQuery.js Adds sum-metric aggregation set; adds available/latest metric discovery endpoints.
server/services/appleHealthClinical.test.js Adds unit tests for clinical (FHIR) parsing/merge logic.
server/services/appleHealthClinical.js Implements clinical records import + merge into blood-tests.json.
server/routes/providers.js Adds explicit PUT /active route to prevent wildcard capture.
server/routes/meatspace.js Removes TSV import endpoint wiring.
server/routes/appleHealth.js Adds ZIP upload support, clinical record extraction, and new metric discovery endpoints.
server/package.json Adds unzipper dependency for ZIP import.
server/lib/meatspaceValidation.js Removes TSV import request schema.
package.json Bumps version to 1.17.0.
package-lock.json Updates lockfile version metadata for 1.17.0.
ecosystem.config.cjs Raises PM2 memory restart thresholds to 2G.
data.sample/cos/state.json Sets alwaysOn: false for safer fresh installs.
data.sample/apps.json Removes placeholder/example apps from sample state.
data.sample/TASKS.md Clears sample pending task to avoid agent spawning on first boot.
client/src/services/api.js Adds health metrics available/latest API helpers; removes TSV import client call.
client/src/pages/MeatSpace.jsx Adds new tabs (Age/Body/Eyes) to MeatSpace router view.
client/src/pages/AIProviders.jsx Avoids unhandled rejection and only updates active provider on success.
client/src/components/meatspace/tabs/OverviewTab.jsx Uses latest Apple Health body metrics when newer; fixes tile navigation targets.
client/src/components/meatspace/tabs/ImportTab.jsx Removes TSV import UI; allows Apple Health ZIP upload with updated instructions.
client/src/components/meatspace/tabs/HealthTab.jsx Replaces hardcoded cards with config-driven category sections + URL params.
client/src/components/meatspace/tabs/EyesTab.jsx Adds dedicated Eyes page for prescriptions CRUD.
client/src/components/meatspace/tabs/BodyTab.jsx Adds dedicated Body page (composition chart).
client/src/components/meatspace/tabs/BloodTab.jsx Narrows Blood tab to blood tests only after page split.
client/src/components/meatspace/tabs/AlcoholTab.jsx Adds HRV correlation chart alongside alcohol chart with synced range.
client/src/components/meatspace/tabs/AgeTab.jsx Adds dedicated Age page for epigenetic results.
client/src/components/meatspace/healthMetrics.js Adds metric registry/config (categories, units, formatting).
client/src/components/meatspace/constants.js Splits tab definitions and extends blood reference ranges.
client/src/components/meatspace/StepsCard.jsx Removed legacy hardcoded metric card.
client/src/components/meatspace/HrvCard.jsx Removed legacy hardcoded metric card.
client/src/components/meatspace/HeartRateCard.jsx Removed legacy hardcoded metric card.
client/src/components/meatspace/MetricCard.jsx Adds generic metric card renderer for most metrics.
client/src/components/meatspace/HealthCategorySection.jsx Adds collapsible/lazy-loaded metric category sections.
client/src/components/meatspace/BloodTestCard.jsx Improves blood test categorization and labeling.
client/src/components/meatspace/AlcoholChart.jsx Exposes view changes so correlation chart can stay in sync.
client/src/components/Layout.jsx Adds sidebar entries for new MeatSpace sub-pages.
browser/package-lock.json Removed subproject lockfile.
.gitignore Ignores subdirectory lockfiles; root lockfile remains tracked.
.changelog/v1.17.x.md Adds changelog entry for v1.17.x series.
.changelog/v1.15.x.md Adds changelog entry for v1.15.x series.
.changelog/v1.14.x.md Adds changelog entry for v1.14.x series.
Files not reviewed (1)
  • browser/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +137 to +142
} else if (entry.path.includes('clinical_records/') && entry.path.endsWith('.json')) {
// Buffer clinical record JSON files (~1-5KB each)
const chunks = [];
entry.on('data', (chunk) => chunks.push(chunk));
entry.on('end', () => clinicalJsons.push(Buffer.concat(chunks).toString('utf-8')));
} else {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clinical record JSON files are fully buffered into memory (chunksBuffer.concat → push into clinicalJsons). A large or malicious ZIP could contain very large JSON entries or huge counts and cause high memory usage/OOM. Consider enforcing per-entry and total byte limits, and/or streaming/processing records incrementally instead of storing all JSON strings at once.

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +169
await fs.unlink(filePath);
filePath = xmlPath;
console.log(`📋 Found ${clinicalJsons.length} clinical record files in ZIP`);
}

const result = await importAppleHealthXml(filePath, io);
res.json(result);

// Import clinical records if any were found
let clinicalResult = null;
if (clinicalJsons.length > 0) {
clinicalResult = await importClinicalRecords(clinicalJsons, io);
}

res.json({ ...result, ...(clinicalResult && { clinical: clinicalResult }) });
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temp file cleanup is not guaranteed on failures: after extracting ZIP you unlink the uploaded ZIP, but the extracted xmlPath is only deleted inside importAppleHealthXml on success. If XML import throws, the temp XML can be left behind in the OS temp dir. Consider wrapping the import in a try/finally that removes xmlPath (and/or the uploaded file) on error as well.

Copilot uses AI. Check for mistakes.
Comment on lines +256 to +261
// Use the last point's qty as the value
const lastPoint = points[points.length - 1];
const value = lastPoint.qty ?? lastPoint.Avg ?? lastPoint.value ?? null;
if (value != null) {
result[name] = { date: dateStr, value: Math.round(value * 100) / 100 };
remaining.delete(name);
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getLatestMetricValues returns lastPoint.qty for a metric, but XML batch flushing can create multiple aggregated points per day for sum-metrics (e.g., step_count), so the last point may be only the last batch’s subtotal. For SUM_METRICS (and potentially sleep_analysis), compute the latest value by aggregating all points in the newest day file (sum/avg as appropriate) rather than taking the last point.

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +296
// Count what changed
const newDates = merged.tests.filter(t => !existingDates.has(t.date)).length;
const newValues = fhirData.totalParsed;

await ensureDir(PATHS.meatspace);
await writeFile(BLOOD_TESTS_FILE, JSON.stringify(merged, null, 2));

const summary = {
clinicalRecords: jsonStrings.length,
labsParsed: fhirData.totalParsed,
labsSkipped: fhirData.totalSkipped,
newDates,
totalDates: merged.tests.length
};
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newValues is computed but never used (const newValues = fhirData.totalParsed;). Either include it in the returned summary or remove it to avoid dead code and keep the import summary consistent with what’s actually reported.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +46
'1920-8': 'sgot_ast',
'1742-6': 'alt',
'4548-4': 'hba1c',
'17856-6': 'hba1c',
'33037-3': 'anion_gap',
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOINC_TO_KEY defines the key '4548-4' more than once. Duplicate keys are silently overwritten in object literals, which is easy to miss during future edits. Consider deduplicating to a single entry (or adding a comment if intentional).

Copilot uses AI. Check for mistakes.
Comment on lines 254 to 256

// =============================================================================
// IMPORT
// =============================================================================

/**
* POST /api/meatspace/import/tsv
* Import TSV spreadsheet (5MB limit handled by express.json middleware)
*/
router.post('/import/tsv', asyncHandler(async (req, res) => {
const { content } = validateRequest(tsvImportSchema, req.body);
const stats = await importTSV(content);
if (stats.error) {
throw new ServerError(stats.error, { status: 400, code: 'IMPORT_FAILED' });
}
res.json(stats);
}));

export default router;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR removes the MeatSpace TSV import API/UI (endpoint and validation/service/test removal), which is a user-visible breaking change. The PR description/test plan don’t mention TSV import removal explicitly—please confirm it’s intended and update the PR description/test plan (or changelog/upgrade notes) accordingly so reviewers/users aren’t surprised.

Copilot uses AI. Check for mistakes.

{eyeExams.length === 0 ? (
<div className="bg-port-card border border-port-border rounded-xl p-6">
<p className="text-gray-500 text-sm">No eye exam data. Import your health spreadsheet or add exams manually.</p>
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy references the removed “health spreadsheet” import (“Import your health spreadsheet…”), but TSV import was removed in this PR. Consider updating this empty-state message to point users at the currently supported import paths (Apple Health ZIP/XML, Health Auto Export JSON, or manual entry).

Suggested change
<p className="text-gray-500 text-sm">No eye exam data. Import your health spreadsheet or add exams manually.</p>
<p className="text-gray-500 text-sm">No eye exam data. Import from Apple Health (ZIP/XML), Health Auto Export (JSON), or add exams manually.</p>

Copilot uses AI. Check for mistakes.
Comment on lines +146 to +152
.on('close', () => {
if (!foundXml) return settle(reject)(new ServerError('ZIP does not contain export.xml', { status: 400, code: 'BAD_REQUEST' }));
// Wait briefly for XML write stream to finish if close fires first
if (xmlWriteFinished) return settle(resolve)();
const check = setInterval(() => { if (xmlWriteFinished) { clearInterval(check); settle(resolve)(); } }, 50);
setTimeout(() => { clearInterval(check); settle(resolve)(); }, 5000);
})
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZIP extraction promise resolves after 5s even if export.xml hasn’t finished writing (setTimeout(... settle(resolve) ...)). That can cause importAppleHealthXml to parse a partially written XML file and produce corrupted/partial imports. Consider awaiting the XML write stream completion deterministically (e.g., using stream/promises.finished/pipeline) and rejecting on timeout instead of resolving, so the import only starts once the file is fully written.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

changing default AI provider to LM Studio - error

2 participants