Hide unclaimed devices from dashboard, add unassigned API, add upgrade docs

- Filter out devices with no user_id from the main device listing
- Add GET /api/devices/unassigned endpoint (admin only) for unclaimed devices
- Add "Updating" section to README with upgrade instructions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-08 13:13:46 -05:00
parent d18045a386
commit 4ae7533b85
2 changed files with 49 additions and 1 deletions

View file

@ -192,6 +192,40 @@ server {
} }
``` ```
### Updating
To update a running instance to the latest version:
```bash
cd /opt/screentinker
# Back up the database first
sqlite3 server/db/remote_display.db ".backup server/db/backup-$(date +%F).db"
# Pull latest code
git pull origin main
# Install any new dependencies
cd server && npm install --production
# Restart the service
sudo systemctl restart screentinker
```
If you deployed without git, you can initialize it:
```bash
cd /opt/screentinker
git init
git remote add origin https://github.com/screentinker/screentinker.git
git fetch origin main
git checkout origin/main -- .
cd server && npm install --production
sudo systemctl restart screentinker
```
Your database, uploads, and configuration are preserved — only code files are updated.
### Backups ### Backups
The SQLite database is at `server/db/remote_display.db`. Back it up regularly: The SQLite database is at `server/db/remote_display.db`. Back it up regularly:

View file

@ -24,13 +24,27 @@ router.get('/', (req, res) => {
INNER JOIN (SELECT device_id, MAX(captured_at) as max_at FROM screenshots GROUP BY device_id) latest INNER JOIN (SELECT device_id, MAX(captured_at) as max_at FROM screenshots GROUP BY device_id) latest
ON sc.device_id = latest.device_id AND sc.captured_at = latest.max_at ON sc.device_id = latest.device_id AND sc.captured_at = latest.max_at
) s ON d.id = s.device_id ) s ON d.id = s.device_id
${isAdmin ? '' : 'WHERE (d.user_id = ? OR d.team_id IN (SELECT team_id FROM team_members WHERE user_id = ?))'} ${isAdmin ? 'WHERE d.user_id IS NOT NULL' : 'WHERE d.user_id IS NOT NULL AND (d.user_id = ? OR d.team_id IN (SELECT team_id FROM team_members WHERE user_id = ?))'}
ORDER BY d.created_at ASC ORDER BY d.created_at ASC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`).all(...(isAdmin ? [] : [req.user.id, req.user.id]), Math.min(parseInt(req.query.limit) || 100, 500), parseInt(req.query.offset) || 0); `).all(...(isAdmin ? [] : [req.user.id, req.user.id]), Math.min(parseInt(req.query.limit) || 100, 500), parseInt(req.query.offset) || 0);
res.json(devices); res.json(devices);
}); });
// List unclaimed provisioning devices (admin only)
router.get('/unassigned', (req, res) => {
if (!['admin', 'superadmin'].includes(req.user.role)) {
return res.status(403).json({ error: 'Admin access required' });
}
const devices = db.prepare(`
SELECT id, pairing_code, status, ip_address, android_version, app_version,
screen_width, screen_height, created_at, last_heartbeat
FROM devices WHERE user_id IS NULL
ORDER BY created_at DESC
`).all();
res.json(devices);
});
// Get single device with telemetry history // Get single device with telemetry history
router.get('/:id', (req, res) => { router.get('/:id', (req, res) => {
const device = db.prepare('SELECT d.*, u.email as owner_email, u.name as owner_name FROM devices d LEFT JOIN users u ON d.user_id = u.id WHERE d.id = ?').get(req.params.id); const device = db.prepare('SELECT d.*, u.email as owner_email, u.name as owner_name FROM devices d LEFT JOIN users u ON d.user_id = u.id WHERE d.id = ?').get(req.params.id);