` : ''}
`;
}
// ===== Repeated-Setting Change Detection =====
/**
* Track a settings change for repeated-change detection
*/
function trackSettingChange(settingKey, settingValue) {
if (!QUALIFYING_SETTINGS.has(settingKey)) {
return; // Not a qualifying setting
}
const now = Date.now();
const windowMs = 24 * 60 * 60 * 1000; // 24 hours
// Initialize history for this setting if needed
if (!settingChangeHistory[settingKey]) {
settingChangeHistory[settingKey] = [];
}
// Add new change
settingChangeHistory[settingKey].push({
timestamp: now,
value: settingValue
});
// Remove old changes outside the 24h window
const cutoff = now - windowMs;
settingChangeHistory[settingKey] = settingChangeHistory[settingKey].filter(
change => change.timestamp > cutoff
);
// Save to localStorage
saveSettingHistory();
// Check if we should show a hint
checkSettingChangeHint(settingKey);
}
/**
* Check if a setting change hint should be shown
*/
function checkSettingChangeHint(settingKey) {
// Check if hint was already shown for this setting
if (settingChangeHintShown[settingKey]) {
const lastShown = settingChangeHintShown[settingKey];
const cooldownMs = 24 * 60 * 60 * 1000; // 24 hour cooldown
if (Date.now() - lastShown < cooldownMs) {
return; // Still in cooldown
}
}
const changes = settingChangeHistory[settingKey] || [];
if (changes.length >= 3) {
// Show the hint
showSettingChangeHint(settingKey);
settingChangeHintShown[settingKey] = Date.now();
saveSettingHintShown();
}
}
/**
* Show a hint for repeated setting changes
*/
function showSettingChangeHint(settingKey) {
const settingName = formatSettingName(settingKey);
// Remove existing hint if present
const existing = document.getElementById('setting-change-hint');
if (existing) {
existing.remove();
}
const hint = document.createElement('div');
hint.id = 'setting-change-hint';
hint.className = 'setting-change-hint';
hint.innerHTML = `
💡Fine-tuning assistance
Looks like you're fine-tuning the ${settingName}. Would you like help finding the right value for your space?
`;
document.body.appendChild(hint);
}
/**
* Dismiss the setting change hint
*/
function dismissSettingHint() {
const hint = document.getElementById('setting-change-hint');
if (hint) {
hint.remove();
}
}
/**
* Start the guided calibration flow for a setting
*/
function startCalibrationFlow(settingKey) {
// Remove the hint
dismissSettingHint();
// Show the calibration flow modal
showCalibrationFlow(settingKey);
}
/**
* Show the guided calibration flow modal
*/
function showCalibrationFlow(settingKey) {
// Remove existing calibration modal if present
const existing = document.getElementById('calibration-flow-modal');
if (existing) {
existing.remove();
}
const settingName = formatSettingName(settingKey);
const modal = document.createElement('div');
modal.id = 'calibration-flow-modal';
modal.className = 'calibration-flow-modal';
modal.innerHTML = `
Guided Calibration: ${settingName}
`;
document.body.appendChild(modal);
// Start with the introduction step
renderCalibrationStep(settingKey, 'intro');
}
/**
* Render a calibration step
*/
function renderCalibrationStep(settingKey, step, data = {}) {
const content = document.getElementById('calibration-flow-content');
if (!content) return;
const steps = {
intro: `
🎯
Let's find the optimal value together
I'll guide you through two quick tests to analyze your space and suggest the best ${formatSettingName(settingKey)} value.
1Walk around your room to test for false positives
2Sit still to test for missed motion detection
3I'll suggest an optimal value based on your space
`,
'false-positive-test': `
Step 1 of 2
🚶
Walk around your room
For the next 15 seconds, walk around your room normally. This helps me understand your space's baseline signal patterns.
15
Getting ready...
`,
'missed-motion-test': `
Step 2 of 2
🧘
Sit perfectly still
For the next 10 seconds, sit or stand perfectly still. This helps me detect motion sensitivity issues.
10
Getting ready...
`,
analyzing: `
Analyzing your space...
Collecting diurnal baseline data and link health metrics to calculate the optimal value.
`,
suggestion: `
✨
Here's my recommendation
Suggested ${formatSettingName(settingKey)}:--
Calculating...
`
};
content.innerHTML = steps[step] || steps.intro;
// Execute step-specific logic
if (step === 'false-positive-test') {
runCalibrationTest(settingKey, 'false-positive', 15);
} else if (step === 'missed-motion-test') {
runCalibrationTest(settingKey, 'missed-motion', 10);
} else if (step === 'analyzing') {
analyzeAndSuggest(settingKey);
}
}
/**
* Run a calibration test with countdown timer
*/
function runCalibrationTest(settingKey, testType, duration) {
const timerEl = document.getElementById('calibration-timer');
const statusEl = document.getElementById('calibration-status');
const progressFill = document.getElementById('calibration-progress-fill');
const startTime = Date.now();
// Update progress bar fill
if (progressFill) {
const targetWidth = testType === 'false-positive' ? '0%' : '50%';
progressFill.style.width = targetWidth;
}
// Start collecting test data
collectTestData(settingKey, testType);
const interval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
const remaining = Math.max(0, Math.ceil(duration - elapsed));
if (timerEl) {
timerEl.textContent = remaining;
}
// Update progress
if (progressFill) {
const baseWidth = testType === 'false-positive' ? 0 : 50;
const progress = (elapsed / duration) * 50;
progressFill.style.width = (baseWidth + progress) + '%';
}
if (elapsed >= 1 && elapsed < 3) {
if (statusEl) statusEl.textContent = 'Recording baseline...';
} else if (elapsed >= 3 && elapsed < duration - 2) {
if (statusEl) statusEl.textContent = testType === 'false-positive' ? 'Walking... keep moving!' : 'Holding still...';
} else if (elapsed >= duration - 2) {
if (statusEl) statusEl.textContent = 'Finishing up...';
}
if (elapsed >= duration) {
clearInterval(interval);
finalizeTestData(settingKey, testType);
// Move to next step
if (testType === 'false-positive') {
renderCalibrationStep(settingKey, 'missed-motion-test');
} else {
renderCalibrationStep(settingKey, 'analyzing');
}
}
}, 100);
}
/**
* Collect test data from the system
*/
function collectTestData(settingKey, testType) {
// Store test metadata for later analysis
if (!window.calibrationTestData) {
window.calibrationTestData = {};
}
window.calibrationTestData[testType] = {
startTime: Date.now(),
settingKey: settingKey
};
// Notify the system to start recording for this test
window.dispatchEvent(new CustomEvent('spaxel:calibration-start', {
detail: { testType: testType, settingKey: settingKey }
}));
}
/**
* Finalize test data collection
*/
function finalizeTestData(settingKey, testType) {
if (!window.calibrationTestData || !window.calibrationTestData[testType]) {
return;
}
window.calibrationTestData[testType].endTime = Date.now();
// Notify the system to stop recording
window.dispatchEvent(new CustomEvent('spaxel:calibration-end', {
detail: { testType: testType, settingKey: settingKey }
}));
}
/**
* Analyze test data and suggest optimal value
*/
function analyzeAndSuggest(settingKey) {
// Fetch diurnal baseline status and link health data
Promise.all([
fetch('/api/diurnal/status').then(r => r.json()).catch(() => null),
fetch('/api/links').then(r => r.json()).catch(() => null)
]).then(([diurnalData, linksData]) => {
const suggestion = calculateSuggestedValue(settingKey, diurnalData, linksData);
renderSuggestion(settingKey, suggestion);
}).catch(err => {
console.error('Failed to fetch calibration data:', err);
// Show fallback suggestion
renderSuggestion(settingKey, getFallbackSuggestion(settingKey));
});
}
/**
* Calculate suggested value based on system data
*/
function calculateSuggestedValue(settingKey, diurnalData, linksData) {
const suggestion = {
value: null,
reason: '',
metrics: []
};
const now = new Date();
const currentHour = now.getHours();
// Get average health score across all links (higher is better, 0-1 range)
let avgHealthScore = 0.5; // default fallback
let activeLinkCount = 0;
if (Array.isArray(linksData)) {
const healthScores = linksData.map(l => l.health_score || 0.5).filter(s => s >= 0);
if (healthScores.length > 0) {
avgHealthScore = healthScores.reduce((a, b) => a + b, 0) / healthScores.length;
activeLinkCount = linksData.length;
}
}
// Get diurnal baseline readiness/learning status
let diurnalReady = false;
let learningProgress = 0;
if (Array.isArray(diurnalData) && diurnalData.length > 0) {
// diurnalData is an array of DiurnalLearningStatus objects
const readyCount = diurnalData.filter(d => d.is_ready).length;
diurnalReady = readyCount > (diurnalData.length / 2); // Majority ready
// Average learning progress (backend returns 'progress' field, 0-100 range)
const progressValues = diurnalData.map(d => (d.progress || 0) / 100).filter(p => p >= 0);
if (progressValues.length > 0) {
learningProgress = progressValues.reduce((a, b) => a + b, 0) / progressValues.length;
}
}
// Calculate suggestion based on setting type
// Note: avgHealthScore is 0-1 where higher is better (unlike raw SNR)
switch (settingKey) {
case 'delta_rms_threshold':
// Lower threshold for high health scores, higher for low health scores
// Range: 0.01 - 0.10
if (avgHealthScore > 0.8) {
suggestion.value = 0.02;
suggestion.reason = 'Your space has excellent link health. A lower threshold will detect subtle movements while minimizing false positives.';
} else if (avgHealthScore > 0.6) {
suggestion.value = 0.03;
suggestion.reason = 'Your space has good link health. This threshold balances sensitivity with noise immunity.';
} else {
suggestion.value = 0.05;
suggestion.reason = 'Your space has lower link health. A higher threshold reduces false positives from environmental noise.';
}
// Adjust based on diurnal readiness
if (diurnalReady && learningProgress < 0.5) {
suggestion.value *= 1.3; // Still learning needs higher threshold
suggestion.reason += ' Adjusted up since your baselines are still learning.';
}
break;
case 'breathing_sensitivity':
// Range: 0.001 - 0.02
if (avgHealthScore > 0.75) {
suggestion.value = 0.005;
suggestion.reason = 'High link health enables fine-grained breathing detection.';
} else if (avgHealthScore > 0.55) {
suggestion.value = 0.008;
suggestion.reason = 'Good link health for reliable breathing detection.';
} else {
suggestion.value = 0.015;
suggestion.reason = 'Lower link health requires higher sensitivity for breathing detection.';
}
break;
case 'tau_s':
// Baseline time constant: 5 - 120 seconds
if (diurnalReady && learningProgress > 0.8) {
suggestion.value = 30;
suggestion.reason = 'Your diurnal baselines are well-calibrated. A 30-second time constant adapts quickly to real changes.';
} else if (diurnalReady || learningProgress > 0.5) {
suggestion.value = 60;
suggestion.reason = 'Moderate baseline stability. A 60-second time constant provides stable adaptation.';
} else {
suggestion.value = 120;
suggestion.reason = 'Your environment is still learning baselines. A longer time constant (120s) provides more stable detection.';
}
break;
case 'fresnel_decay':
// Zone decay rate: 1.0 - 4.0
if (avgHealthScore > 0.75) {
suggestion.value = 2.5;
suggestion.reason = 'Strong link health supports tighter zone focus for better localization accuracy.';
} else if (avgHealthScore > 0.55) {
suggestion.value = 2.0;
suggestion.reason = 'Balanced decay rate for your link health.';
} else {
suggestion.value = 1.5;
suggestion.reason = 'Lower link health benefits from broader zone contribution.';
}
break;
case 'n_subcarriers':
// Subcarrier count: 8 - 16
if (avgHealthScore > 0.75) {
suggestion.value = 16;
suggestion.reason = 'Excellent link health supports using all 16 subcarriers for maximum detail.';
} else if (avgHealthScore > 0.55) {
suggestion.value = 12;
suggestion.reason = 'Good link health. 12 subcarriers balance detail with noise immunity.';
} else {
suggestion.value = 8;
suggestion.reason = 'Lower link health. Fewer subcarriers reduce noise impact.';
}
break;
default:
suggestion.value = 0.03;
suggestion.reason = 'Default suggestion based on average system performance.';
}
// Build metrics display
const healthPercent = Math.round(avgHealthScore * 100);
suggestion.metrics = [
{ label: 'Link Health', value: healthPercent + '%' },
{ label: 'Diurnal Ready', value: diurnalReady ? 'Yes' : 'Learning' },
{ label: 'Active Links', value: activeLinkCount.toString() }
];
return suggestion;
}
/**
* Get fallback suggestion when API data is unavailable
*/
function getFallbackSuggestion(settingKey) {
const defaults = {
'delta_rms_threshold': { value: 0.03, reason: 'Default value for typical home environments.' },
'breathing_sensitivity': { value: 0.008, reason: 'Default sensitivity for reliable breathing detection.' },
'tau_s': { value: 60, reason: 'Default 60-second time constant for stable adaptation.' },
'fresnel_decay': { value: 2.0, reason: 'Default inverse-square decay for balanced localization.' },
'n_subcarriers': { value: 12, reason: 'Default 12 subcarriers for balanced performance.' }
};
return defaults[settingKey] || { value: 0.03, reason: 'Default value.' };
}
/**
* Render the suggestion step with calculated values
*/
function renderSuggestion(settingKey, suggestion) {
const content = document.getElementById('calibration-flow-content');
if (!content) return;
const formattedValue = formatSuggestedValue(settingKey, suggestion.value);
content.innerHTML = `
`;
}
/**
* Format a suggested value for display
*/
function formatSuggestedValue(settingKey, value) {
if (settingKey === 'tau_s') {
return value + ' seconds';
} else if (settingKey === 'n_subcarriers') {
return value + ' subcarriers';
} else if (settingKey === 'fresnel_decay') {
return value.toFixed(1);
}
return value.toFixed(3);
}
/**
* Apply the suggested value to the setting
*/
function applySuggestedValue(settingKey, value) {
// Show loading state
const applyBtn = document.querySelector('.calibration-btn-primary');
if (applyBtn) {
applyBtn.disabled = true;
applyBtn.textContent = 'Applying...';
}
// Build settings payload
const settings = {};
settings[settingKey] = value;
// Send to API
fetch('/api/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(settings)
})
.then(res => res.json())
.then(data => {
// Show success message
showCalibrationSuccess(settingKey, value);
// Notify other components of settings change
window.dispatchEvent(new CustomEvent('spaxel:settings-changed', {
detail: { key: settingKey, value: value }
}));
})
.catch(err => {
console.error('Failed to apply setting:', err);
showCalibrationError();
});
}
/**
* Show calibration success message
*/
function showCalibrationSuccess(settingKey, value) {
const content = document.getElementById('calibration-flow-content');
if (!content) return;
const formattedValue = formatSuggestedValue(settingKey, value);
content.innerHTML = `
✓
Value applied!
Your ${formatSettingName(settingKey)} has been set to ${formattedValue}.
The system will now use this new value for detection. If you experience issues, you can adjust it again in Settings.
`;
}
/**
* Show calibration error message
*/
function showCalibrationError() {
const content = document.getElementById('calibration-flow-content');
if (!content) return;
content.innerHTML = `
⚠
Couldn't apply the value
There was a problem saving the setting. Please try again or adjust it manually in Settings.
`;
}
/**
* Close the calibration flow modal
*/
function closeCalibrationFlow() {
const modal = document.getElementById('calibration-flow-modal');
if (modal) {
modal.classList.add('calibration-closing');
setTimeout(() => modal.remove(), 300);
}
// Clean up test data
if (window.calibrationTestData) {
delete window.calibrationTestData;
}
}
/**
* Save setting change history to localStorage
*/
function saveSettingHistory() {
try {
localStorage.setItem('spaxel_setting_changes', JSON.stringify(settingChangeHistory));
} catch (e) {
console.warn('Failed to save setting history:', e);
}
}
/**
* Save setting hint shown timestamps
*/
function saveSettingHintShown() {
try {
localStorage.setItem('spaxel_setting_hints_shown', JSON.stringify(settingChangeHintShown));
} catch (e) {
console.warn('Failed to save hint shown state:', e);
}
}
/**
* Format a setting key for display
*/
function formatSettingName(key) {
const names = {
'delta_rms_threshold': 'Motion Threshold',
'breathing_sensitivity': 'Breathing Sensitivity',
'tau_s': 'Baseline Time Constant',
'fresnel_decay': 'Fresnel Weight Decay',
'n_subcarriers': 'Subcarrier Count'
};
return names[key] || key;
}
// ===== Post-Feedback Explanations =====
/**
* Show explanation after false positive feedback
*/
function showFeedbackExplanation(eventData) {
if (!eventData || !eventData.explainability) {
return;
}
const explanation = eventData.explainability;
const contributingLinks = explanation.contributing_links || [];
const allLinks = explanation.all_links || [];
// Find the primary contributing link
const primaryLink = contributingLinks.length > 0 ? contributingLinks[0] : null;
let explanationText = '';
if (primaryLink) {
const linkName = formatLinkID(primaryLink.link_id);
const deltaRMS = primaryLink.delta_rms?.toFixed(4) || 'N/A';
const threshold = 0.02; // Standard threshold
const ratio = (primaryLink.delta_rms / threshold).toFixed(1);
explanationText = `The system detected motion here because: ${linkName}'s signal (deltaRMS: ${deltaRMS}) exceeded the motion threshold by ${ratio}x.`;
// Add root cause from diagnostic if available
if (primaryLink.diagnosis) {
const diagnosis = primaryLink.diagnosis;
explanationText += `
Possible cause: ${diagnosis.detail}`;
if (diagnosis.advice) {
explanationText += ` What to do: ${diagnosis.advice}`;
}
} else {
explanationText += `
Possible cause: Ambient RF interference or environmental changes. We've noted this and will apply corrections.`;
}
} else {
explanationText = 'The system detected motion based on signal patterns across multiple links. We\'ve noted this feedback to improve accuracy.';
}
// Create explanation element
const explanationDiv = document.createElement('div');
explanationDiv.className = 'feedback-explanation';
explanationDiv.innerHTML = `