u
This commit is contained in:
@@ -12,7 +12,7 @@ console.log(`[Migrate] Connected to ${dbPath}`);
|
|||||||
console.log('[Migrate] Applying schema migrations...');
|
console.log('[Migrate] Applying schema migrations...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
@@ -30,9 +30,20 @@ try {
|
|||||||
FOREIGN KEY(created_by) REFERENCES users(id)
|
FOREIGN KEY(created_by) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
console.log('[Migrate] Schema applied successfully.');
|
|
||||||
|
// Add position column if not exists
|
||||||
|
try {
|
||||||
|
db.exec("ALTER TABLE views ADD COLUMN position INTEGER DEFAULT 0");
|
||||||
|
console.log('[Migrate] Added position column to views table.');
|
||||||
|
} catch (e) {
|
||||||
|
if (!e.message.includes("duplicate column name")) {
|
||||||
|
console.log('[Migrate] NOTE: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Migrate] Schema applied successfully.');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Migrate] Error applying schema:', err.message);
|
console.error('[Migrate] Error applying schema:', err.message);
|
||||||
} finally {
|
} finally {
|
||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ class ViewManager extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
parseViewData(view) {
|
parseViewData(view) {
|
||||||
// Flatten config for display/editing
|
|
||||||
let channels = [];
|
let channels = [];
|
||||||
let axes = { left: { min: '', max: '' }, right: { min: '', max: '' } };
|
let axes = { left: { min: '', max: '' }, right: { min: '', max: '' } };
|
||||||
|
|
||||||
@@ -109,28 +108,20 @@ class ViewManager extends Component {
|
|||||||
if (!config) return { channels, axes };
|
if (!config) return { channels, axes };
|
||||||
|
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
// Very old legacy (array of channels)
|
|
||||||
channels = config;
|
channels = config;
|
||||||
} else if (config.groups) {
|
} else if (config.groups) {
|
||||||
// Group format (Recent) - Flatten it!
|
|
||||||
config.groups.forEach(g => {
|
config.groups.forEach(g => {
|
||||||
if (g.channels) {
|
if (g.channels) channels = [...channels, ...g.channels];
|
||||||
channels = [...channels, ...g.channels];
|
|
||||||
}
|
|
||||||
// Merge axes? Just take first group's axes or default?
|
|
||||||
// Let's defer to default or first group if present
|
|
||||||
if (g.axes) {
|
if (g.axes) {
|
||||||
if (g.axes.left) axes.left = { ...axes.left, ...g.axes.left };
|
if (g.axes.left) axes.left = { ...axes.left, ...g.axes.left };
|
||||||
if (g.axes.right) axes.right = { ...axes.right, ...g.axes.right };
|
if (g.axes.right) axes.right = { ...axes.right, ...g.axes.right };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (config.channels) {
|
} else if (config.channels) {
|
||||||
// Standard Legacy
|
|
||||||
channels = config.channels;
|
channels = config.channels;
|
||||||
if (config.axes) axes = config.axes;
|
if (config.axes) axes = config.axes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize channels
|
|
||||||
channels = channels.map((c, i) => ({
|
channels = channels.map((c, i) => ({
|
||||||
...c,
|
...c,
|
||||||
color: c.color || this.getNextColor(i)
|
color: c.color || this.getNextColor(i)
|
||||||
@@ -180,6 +171,31 @@ class ViewManager extends Component {
|
|||||||
this.refreshViews();
|
this.refreshViews();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
moveView = async (idx, dir) => {
|
||||||
|
const newViews = [...this.state.views];
|
||||||
|
const target = idx + dir;
|
||||||
|
if (target < 0 || target >= newViews.length) return;
|
||||||
|
|
||||||
|
[newViews[idx], newViews[target]] = [newViews[target], newViews[idx]];
|
||||||
|
this.setState({ views: newViews });
|
||||||
|
|
||||||
|
const order = newViews.map((v, i) => ({ id: v.id, position: i }));
|
||||||
|
const { user } = this.props;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/api/views/reorder', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${user.token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ order })
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save order", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleSave = async () => {
|
handleSave = async () => {
|
||||||
const { viewName, viewConfig, axisConfig, editingId } = this.state;
|
const { viewName, viewConfig, axisConfig, editingId } = this.state;
|
||||||
const { user } = this.props;
|
const { user } = this.props;
|
||||||
@@ -189,7 +205,6 @@ class ViewManager extends Component {
|
|||||||
const url = editingId ? `/api/views/${editingId}` : '/api/views';
|
const url = editingId ? `/api/views/${editingId}` : '/api/views';
|
||||||
const method = editingId ? 'PUT' : 'POST';
|
const method = editingId ? 'PUT' : 'POST';
|
||||||
|
|
||||||
// Save as flat format again
|
|
||||||
const finalConfig = {
|
const finalConfig = {
|
||||||
channels: viewConfig,
|
channels: viewConfig,
|
||||||
axes: axisConfig
|
axes: axisConfig
|
||||||
@@ -274,7 +289,6 @@ class ViewManager extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Navigation ---
|
|
||||||
handleRangeChange = (e, newVal) => {
|
handleRangeChange = (e, newVal) => {
|
||||||
if (newVal) this.setState({ rangeLabel: newVal });
|
if (newVal) this.setState({ rangeLabel: newVal });
|
||||||
};
|
};
|
||||||
@@ -315,7 +329,6 @@ class ViewManager extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{ mt: 4 }}>
|
<Container maxWidth="xl" sx={{ mt: 4 }}>
|
||||||
{/* Global Time Controls */}
|
|
||||||
<Paper sx={{ position: 'sticky', top: 10, zIndex: 1000, p: 2, mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between', border: '1px solid #504945' }}>
|
<Paper sx={{ position: 'sticky', top: 10, zIndex: 1000, p: 2, mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between', border: '1px solid #504945' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<ToggleButtonGroup value={rangeLabel} exclusive onChange={this.handleRangeChange} size="small">
|
<ToggleButtonGroup value={rangeLabel} exclusive onChange={this.handleRangeChange} size="small">
|
||||||
@@ -330,14 +343,21 @@ class ViewManager extends Component {
|
|||||||
{isAdmin && <Button variant="contained" startIcon={<AddIcon />} onClick={this.handleOpenCreate}>Create View</Button>}
|
{isAdmin && <Button variant="contained" startIcon={<AddIcon />} onClick={this.handleOpenCreate}>Create View</Button>}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* View List */}
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||||
{views.map(view => {
|
{views.map((view, vIdx) => {
|
||||||
const { channels, axes } = this.parseViewData(view);
|
const { channels, axes } = this.parseViewData(view);
|
||||||
return (
|
return (
|
||||||
<Paper key={view.id} sx={{ p: 2, display: 'flex', flexDirection: 'column' }}>
|
<Paper key={view.id} sx={{ p: 2, display: 'flex', flexDirection: 'column' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||||
<Typography variant="h4">{view.name}</Typography>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography variant="h4">{view.name}</Typography>
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<IconButton size="small" onClick={() => this.moveView(vIdx, -1)} disabled={vIdx === 0}><ArrowUpwardIcon /></IconButton>
|
||||||
|
<IconButton size="small" onClick={() => this.moveView(vIdx, 1)} disabled={vIdx === views.length - 1}><ArrowDownwardIcon /></IconButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Box>
|
<Box>
|
||||||
<IconButton onClick={(e) => this.handleOpenEdit(view, e)}><EditIcon /></IconButton>
|
<IconButton onClick={(e) => this.handleOpenEdit(view, e)}><EditIcon /></IconButton>
|
||||||
@@ -364,7 +384,6 @@ class ViewManager extends Component {
|
|||||||
{views.length === 0 && <Typography>No views available.</Typography>}
|
{views.length === 0 && <Typography>No views available.</Typography>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Edit Dialog */}
|
|
||||||
<Dialog open={open} onClose={() => this.setState({ open: false })} maxWidth="md" fullWidth>
|
<Dialog open={open} onClose={() => this.setState({ open: false })} maxWidth="md" fullWidth>
|
||||||
<DialogTitle>{editingId ? 'Edit View' : 'Create New View'}</DialogTitle>
|
<DialogTitle>{editingId ? 'Edit View' : 'Create New View'}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -373,7 +392,6 @@ class ViewManager extends Component {
|
|||||||
onChange={(e) => this.setState({ viewName: e.target.value })} sx={{ mb: 2 }}
|
onChange={(e) => this.setState({ viewName: e.target.value })} sx={{ mb: 2 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Axis Config */}
|
|
||||||
<Box sx={{ p: 2, border: '1px solid #444', borderRadius: 1, mb: 2 }}>
|
<Box sx={{ p: 2, border: '1px solid #444', borderRadius: 1, mb: 2 }}>
|
||||||
<Typography variant="subtitle2" gutterBottom>Axis Configuration</Typography>
|
<Typography variant="subtitle2" gutterBottom>Axis Configuration</Typography>
|
||||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
@@ -410,7 +428,6 @@ class ViewManager extends Component {
|
|||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
{/* Add Channel */}
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 1, bgcolor: '#32302f', p: 1, borderRadius: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mt: 1, bgcolor: '#32302f', p: 1, borderRadius: 1 }}>
|
||||||
<Select size="small" value={paramSelDevice} displayEmpty onChange={e => this.setState({ paramSelDevice: e.target.value })} sx={{ minWidth: 120 }}>
|
<Select size="small" value={paramSelDevice} displayEmpty onChange={e => this.setState({ paramSelDevice: e.target.value })} sx={{ minWidth: 120 }}>
|
||||||
<MenuItem value=""><em>Device</em></MenuItem>
|
<MenuItem value=""><em>Device</em></MenuItem>
|
||||||
@@ -435,7 +452,6 @@ class ViewManager extends Component {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Color Picker Dialog */}
|
|
||||||
<Dialog open={colorPickerOpen} onClose={() => this.setState({ colorPickerOpen: false })}>
|
<Dialog open={colorPickerOpen} onClose={() => this.setState({ colorPickerOpen: false })}>
|
||||||
<DialogTitle>Select Color</DialogTitle>
|
<DialogTitle>Select Color</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ module.exports = {
|
|||||||
|
|
||||||
app.get('/api/views/:id', (req, res) => {
|
app.get('/api/views/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare('SELECT * FROM views WHERE id = ?');
|
const stmt = db.prepare('SELECT * FROM views ORDER BY position ASC, id ASC');
|
||||||
const view = stmt.get(req.params.id);
|
const view = stmt.get(req.params.id);
|
||||||
if (view) {
|
if (view) {
|
||||||
view.config = JSON.parse(view.config);
|
view.config = JSON.parse(view.config);
|
||||||
@@ -192,6 +192,24 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reorder Views
|
||||||
|
app.post('/api/views/reorder', requireAdmin, (req, res) => {
|
||||||
|
const { order } = req.body;
|
||||||
|
if (!Array.isArray(order)) return res.status(400).json({ error: 'Invalid format' });
|
||||||
|
|
||||||
|
const updateStmt = db.prepare('UPDATE views SET position = ? WHERE id = ?');
|
||||||
|
const updateMany = db.transaction((items) => {
|
||||||
|
for (const item of items) updateStmt.run(item.position, item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateMany(order);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/devices
|
// GET /api/devices
|
||||||
// Returns list of unique device/channel pairs
|
// Returns list of unique device/channel pairs
|
||||||
app.get('/api/devices', (req, res) => {
|
app.get('/api/devices', (req, res) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user