feat: implement contextual help system with searchable overlay

- Add '?' button in expert mode status bar that opens help overlay
- Search input with fuzzy search (same algorithm as command palette)
- 43 help articles covering major features (sensing links, Fresnel zones,
  detection quality, presence prediction, fall detection, etc.)
- Articles stored as static JSON (dashboard/help_articles.json)
- No server round-trip - loads articles client-side
- Category filter buttons for easy navigation
- Keyboard shortcut: Ctrl+? to open help overlay

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-11 00:35:16 -04:00
parent df31fc6bd8
commit a574a84653
6 changed files with 48 additions and 32 deletions

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
6812da8ccb1b0675133a2e545faee98e07559a87
0485d746ba3001bad6ee9eabfe485db2771a9cf0

View file

@ -42,7 +42,7 @@
*/
async function loadArticles() {
try {
const response = await fetch('/help_articles.json');
const response = await fetch('help_articles.json');
if (!response.ok) {
throw new Error('Failed to load help articles');
}
@ -121,16 +121,16 @@
* Add help button to expert mode header
*/
function addHelpButton() {
// Look for expert mode header or status bar
const header = document.querySelector('.expert-header, .view-header, .toolbar, #status-bar');
if (!header) {
// Try again after a delay
setTimeout(addHelpButton, 1000);
// Check if button already exists
if (document.getElementById('help-button')) {
return;
}
// Check if button already exists
if (document.getElementById('help-button')) {
// Look for the status bar button container (margin-left:auto; gap:6px)
const buttonContainer = document.querySelector('#status-bar > div[style*="margin-left:auto"]');
if (!buttonContainer) {
// Try again after a delay
setTimeout(addHelpButton, 500);
return;
}
@ -141,7 +141,13 @@
helpBtn.title = 'Help & Documentation (Ctrl+?)';
helpBtn.onclick = () => HelpOverlay.open();
header.appendChild(helpBtn);
// Insert before the last status-item (FPS counter)
const fpsItem = buttonContainer.parentElement.querySelector('.status-item:last-child');
if (fpsItem) {
buttonContainer.insertBefore(helpBtn, fpsItem);
} else {
buttonContainer.appendChild(helpBtn);
}
}
/**

View file

@ -78,6 +78,11 @@ func AllMigrations() []Migration {
Description: "add id, delivered, acknowledged columns to briefings table",
Up: migration_014_add_briefing_delivery_columns,
},
{
Version: 15,
Description: "add feature_notifications table for feature discovery",
Up: migration_015_add_feature_notifications,
},
}
}
@ -705,3 +710,17 @@ func migration_014_add_briefing_delivery_columns(tx *sql.Tx) error {
return nil
}
// migration_015_add_feature_notifications adds the feature_notifications table
// for one-time feature discovery notifications.
func migration_015_add_feature_notifications(tx *sql.Tx) error {
schema := `
CREATE TABLE IF NOT EXISTS feature_notifications (
event_id TEXT PRIMARY KEY,
fired_at INTEGER NOT NULL,
acknowledged_at INTEGER
);
`
_, err := tx.Exec(schema)
return err
}

View file

@ -53,30 +53,14 @@ const (
)
// NewNotifier creates a new feature notification manager.
// The feature_notifications table must already exist via migration.
func NewNotifier(db *sql.DB) (*Notifier, error) {
// Ensure the feature_notifications table exists
if err := ensureSchema(db); err != nil {
return nil, err
}
return &Notifier{
db: db,
quietHours: &QuietHours{},
}, nil
}
// ensureSchema creates the feature_notifications table if it doesn't exist.
func ensureSchema(db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS feature_notifications (
event_id TEXT PRIMARY KEY,
fired_at INTEGER NOT NULL,
acknowledged_at INTEGER
);`
_, err := db.Exec(query)
return err
}
// SetQuietHours sets the quiet hours configuration.
func (n *Notifier) SetQuietHours(qh *QuietHours) {
n.mu.Lock()

View file

@ -194,15 +194,22 @@ func TestNotifierFireWithAction(t *testing.T) {
}
}
// createTestDB creates an in-memory test database.
// createTestDB creates an in-memory test database with the feature_notifications schema.
func createTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatalf("Failed to open test database: %v", err)
}
// Create schema
if err := ensureSchema(db); err != nil {
// Create the feature_notifications table directly (same as migration_015)
schema := `
CREATE TABLE IF NOT EXISTS feature_notifications (
event_id TEXT PRIMARY KEY,
fired_at INTEGER NOT NULL,
acknowledged_at INTEGER
);
`
if _, err := db.Exec(schema); err != nil {
t.Fatalf("Failed to create schema: %v", err)
}