feat(dashboard): implement dashboard data structure (Task 1.3)

Add dashboard configuration to extensionSettings and create default layout system:

State Management (state.js):
- Added extensionSettings.dashboard with version 2
- gridConfig: columns (12), rowHeight (80px), gap (12px), snapToGrid, showGrid
- tabs: Array of tab objects with widgets
- defaultTab: ID of tab to show on load
- Comprehensive inline documentation of structure

Default Layout Generator (defaultLayout.js):
- generateDefaultDashboard() - Creates 2-tab default layout
  - "Status" tab: userStats, infoBox, presentCharacters (3 widgets)
  - "Inventory" tab: inventory widget (1 widget)
- migrateV1ToV2Dashboard() - Migrates v1.x settings to v2.0
  - Respects user's visibility preferences (showUserStats, etc.)
  - Removes hidden widgets from migrated layout
  - Preserves user data during migration
- validateDashboardConfig() - Validates dashboard structure
- Utility functions: getWidgetCount(), findWidget()

Persistence Layer (persistence.js):
- Auto-migration on loadSettings() for existing users
- Validates dashboard config on load
- Regenerates default if config invalid or missing
- Seamless backward compatibility

Test Suite (defaultLayout.test.html):
- 4 test scenarios with visual verification
- Tests generation, validation, migration, utilities
- Live dashboard JSON preview
- Statistics panel (version, tabs, widgets, grid config)

Features:
- Automatic migration from v1.x hardcoded panel
- Preserves user preferences during migration
- Validates all dashboard configs on load
- Generates sensible defaults for new users

Acceptance Criteria Met:
✓ Dashboard config persists in extensionSettings
✓ Default layout generates on first load
✓ Existing users see migrated layout preserving their preferences
✓ All data structures validated

Epic 1, Task 1.3 Complete (1-2 day estimate, <10 min actual)
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-23 09:26:10 +11:00
parent 4f1ea44e74
commit 2edb41ebe6
4 changed files with 679 additions and 1 deletions
@@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Default Layout Test</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a2e;
color: #eee;
padding: 20px;
}
h1 {
margin-bottom: 20px;
color: #e94560;
}
.test-section {
background: #16213e;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
}
.test-section h2 {
color: #4ecca3;
margin-bottom: 10px;
font-size: 18px;
}
pre {
background: #0f3460;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
}
.result {
margin: 10px 0;
padding: 10px;
border-left: 3px solid #4ecca3;
background: #0f3460;
}
.result.pass {
border-color: #4ecca3;
}
.result.fail {
border-color: #e94560;
background: #2a0f1b;
}
button {
background: #e94560;
color: white;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #d63651;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
margin-top: 10px;
}
.stat-box {
background: #0f3460;
padding: 10px;
border-radius: 5px;
}
.stat-label {
font-size: 11px;
opacity: 0.7;
text-transform: uppercase;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #4ecca3;
}
</style>
</head>
<body>
<h1>🏗️ Default Layout Test Suite</h1>
<div class="test-section">
<h2>Test 1: Generate Default Dashboard</h2>
<div id="test1-results"></div>
<button onclick="test1()">Run Test 1</button>
</div>
<div class="test-section">
<h2>Test 2: Validate Dashboard Config</h2>
<div id="test2-results"></div>
<button onclick="test2()">Run Test 2</button>
</div>
<div class="test-section">
<h2>Test 3: Migrate v1.x Settings</h2>
<div id="test3-results"></div>
<button onclick="test3()">Run Test 3</button>
</div>
<div class="test-section">
<h2>Test 4: Find Widget Utility</h2>
<div id="test4-results"></div>
<button onclick="test4()">Run Test 4</button>
</div>
<div class="test-section">
<h2>Generated Dashboard JSON</h2>
<pre id="dashboard-json"></pre>
</div>
<div class="test-section">
<h2>Dashboard Statistics</h2>
<div id="stats" class="stats"></div>
</div>
<div style="margin-top: 20px;">
<button onclick="runAllTests()">🔄 Run All Tests</button>
</div>
<script type="module">
import {
generateDefaultDashboard,
migrateV1ToV2Dashboard,
validateDashboardConfig,
getWidgetCount,
findWidget
} from './defaultLayout.js';
let dashboard = null;
function pass(message) {
return `<div class="result pass">✓ ${message}</div>`;
}
function fail(message) {
return `<div class="result fail">✗ ${message}</div>`;
}
// Test 1: Generate default dashboard
window.test1 = function() {
const container = document.getElementById('test1-results');
container.innerHTML = '';
try {
dashboard = generateDefaultDashboard();
container.innerHTML += pass('Generated default dashboard');
if (dashboard.version === 2) {
container.innerHTML += pass(`Dashboard version: ${dashboard.version}`);
} else {
container.innerHTML += fail(`Wrong version: ${dashboard.version}`);
}
if (dashboard.tabs && dashboard.tabs.length === 2) {
container.innerHTML += pass(`Generated ${dashboard.tabs.length} tabs`);
} else {
container.innerHTML += fail(`Wrong tab count: ${dashboard.tabs?.length || 0}`);
}
const statusTab = dashboard.tabs.find(t => t.id === 'tab-status');
if (statusTab && statusTab.widgets.length === 3) {
container.innerHTML += pass(`Status tab has ${statusTab.widgets.length} widgets`);
} else {
container.innerHTML += fail(`Wrong widget count in status tab`);
}
const inventoryTab = dashboard.tabs.find(t => t.id === 'tab-inventory');
if (inventoryTab && inventoryTab.widgets.length === 1) {
container.innerHTML += pass(`Inventory tab has ${inventoryTab.widgets.length} widget`);
} else {
container.innerHTML += fail(`Wrong widget count in inventory tab`);
}
updateDashboardDisplay();
updateStats();
} catch (error) {
container.innerHTML += fail(`Error: ${error.message}`);
}
};
// Test 2: Validate dashboard config
window.test2 = function() {
const container = document.getElementById('test2-results');
container.innerHTML = '';
if (!dashboard) {
container.innerHTML += fail('No dashboard generated yet (run Test 1 first)');
return;
}
const valid = validateDashboardConfig(dashboard);
if (valid) {
container.innerHTML += pass('Dashboard config is valid');
} else {
container.innerHTML += fail('Dashboard config validation failed');
}
// Test invalid configs
const invalidConfigs = [
{ config: null, name: 'null config' },
{ config: {}, name: 'empty config' },
{ config: { version: 2 }, name: 'missing gridConfig' },
{ config: { version: 2, gridConfig: {}, tabs: 'not-array' }, name: 'tabs not array' }
];
for (const test of invalidConfigs) {
const result = validateDashboardConfig(test.config);
if (!result) {
container.innerHTML += pass(`Correctly rejected ${test.name}`);
} else {
container.innerHTML += fail(`Failed to reject ${test.name}`);
}
}
};
// Test 3: Migrate v1.x settings
window.test3 = function() {
const container = document.getElementById('test3-results');
container.innerHTML = '';
// Simulate v1.x settings with some sections hidden
const v1Settings = {
showUserStats: true,
showInfoBox: false, // Hidden
showCharacterThoughts: true,
showInventory: true
};
try {
const migrated = migrateV1ToV2Dashboard(v1Settings);
container.innerHTML += pass('Migrated v1.x settings');
const statusTab = migrated.tabs.find(t => t.id === 'tab-status');
const hasInfoBox = statusTab?.widgets.some(w => w.type === 'infoBox');
if (!hasInfoBox) {
container.innerHTML += pass('Correctly removed hidden infoBox widget');
} else {
container.innerHTML += fail('Failed to remove hidden infoBox widget');
}
container.innerHTML += `<div class="result">Migrated dashboard has ${migrated.tabs.length} tabs</div>`;
container.innerHTML += `<div class="result">Status tab has ${statusTab?.widgets.length || 0} widgets</div>`;
} catch (error) {
container.innerHTML += fail(`Error: ${error.message}`);
}
};
// Test 4: Find widget utility
window.test4 = function() {
const container = document.getElementById('test4-results');
container.innerHTML = '';
if (!dashboard) {
container.innerHTML += fail('No dashboard generated yet (run Test 1 first)');
return;
}
const found = findWidget(dashboard, 'widget-userstats');
if (found) {
container.innerHTML += pass(`Found widget: ${found.widget.id} at tab ${found.tabIndex}, widget ${found.widgetIndex}`);
container.innerHTML += `<div class="result">Widget type: ${found.widget.type}</div>`;
container.innerHTML += `<div class="result">Position: (${found.widget.x}, ${found.widget.y})</div>`;
container.innerHTML += `<div class="result">Size: ${found.widget.w}×${found.widget.h}</div>`;
} else {
container.innerHTML += fail('Failed to find widget-userstats');
}
const notFound = findWidget(dashboard, 'widget-nonexistent');
if (!notFound) {
container.innerHTML += pass('Correctly returned null for non-existent widget');
} else {
container.innerHTML += fail('Should return null for non-existent widget');
}
};
// Update dashboard JSON display
function updateDashboardDisplay() {
const jsonContainer = document.getElementById('dashboard-json');
if (dashboard) {
jsonContainer.textContent = JSON.stringify(dashboard, null, 2);
} else {
jsonContainer.textContent = '// No dashboard generated yet';
}
}
// Update stats
function updateStats() {
const statsContainer = document.getElementById('stats');
if (!dashboard) {
statsContainer.innerHTML = '<div class="stat-box">No dashboard generated yet</div>';
return;
}
const widgetCount = getWidgetCount(dashboard);
statsContainer.innerHTML = `
<div class="stat-box">
<div class="stat-label">Dashboard Version</div>
<div class="stat-value">${dashboard.version}</div>
</div>
<div class="stat-box">
<div class="stat-label">Total Tabs</div>
<div class="stat-value">${dashboard.tabs.length}</div>
</div>
<div class="stat-box">
<div class="stat-label">Total Widgets</div>
<div class="stat-value">${widgetCount}</div>
</div>
<div class="stat-box">
<div class="stat-label">Grid Columns</div>
<div class="stat-value">${dashboard.gridConfig.columns}</div>
</div>
<div class="stat-box">
<div class="stat-label">Row Height</div>
<div class="stat-value">${dashboard.gridConfig.rowHeight}px</div>
</div>
<div class="stat-box">
<div class="stat-label">Default Tab</div>
<div class="stat-value">${dashboard.defaultTab}</div>
</div>
`;
}
// Run all tests
window.runAllTests = function() {
test1();
setTimeout(() => test2(), 100);
setTimeout(() => test3(), 200);
setTimeout(() => test4(), 300);
};
// Auto-run on load
runAllTests();
</script>
</body>
</html>