cf6623de67
CI / test (push) Has been cancelled
Adds two settings exposed in the PWA frame-settings sheet:
- rotationMode (enum: random | least_recently_shown | oldest_upload |
newest_upload). Default oldest_upload preserves the legacy
hard-coded sort, so existing devices behave identically until the
user changes it.
- prioritizeNeverShown (bool). When set, the candidate set is narrowed
to never-shown images first (if any exist) before the mode runs —
useful for "burn through new uploads before re-shuffling the catalog."
RotationService pipeline:
1. Pull approved/ready pool.
2. Drop the last `uniquenessWindow` served (existing).
3. If prioritizeNeverShown AND any candidates have never been served,
narrow to those.
4. Apply the selection mode.
Backend: enum, entity columns + accessors, migration, serializer,
PATCH validator. Frontend: types, stores, settings sheet section
(dropdown + checkbox), test fixtures, save-flow test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import type { Device } from '@/types'
|
|
|
|
export const useDevicesStore = defineStore('devices', () => {
|
|
const devices = ref<Device[]>([])
|
|
const loading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
/**
|
|
* Fetch the device list. Pass `silent: true` from background refreshes
|
|
* (pull-to-refresh, visibility-change polling) so the loading spinner
|
|
* doesn't blink and replace the existing cards mid-fetch.
|
|
*/
|
|
async function fetchDevices(opts: { silent?: boolean } = {}) {
|
|
if (!opts.silent) loading.value = true
|
|
error.value = null
|
|
try {
|
|
const res = await fetch('/api/devices')
|
|
if (!res.ok) throw new Error('Failed to load devices')
|
|
devices.value = await res.json()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Unknown error'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function updateDevice(id: number, patch: Partial<Pick<Device, 'name' | 'orientation' | 'rotationIntervalMinutes' | 'wakeTimes' | 'timezone' | 'uniquenessWindow' | 'rotationMode' | 'prioritizeNeverShown'>>) {
|
|
const res = await fetch(`/api/devices/${id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(patch),
|
|
})
|
|
if (!res.ok) throw new Error('Failed to update device')
|
|
const updated: Device = await res.json()
|
|
const idx = devices.value.findIndex(d => d.id === id)
|
|
if (idx !== -1) devices.value[idx] = updated
|
|
return updated
|
|
}
|
|
|
|
async function lockImage(deviceId: number, imageId: number): Promise<Device> {
|
|
const res = await fetch(`/api/devices/${deviceId}/lock`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ imageId }),
|
|
})
|
|
if (!res.ok) throw new Error('Failed to lock image')
|
|
const updated: Device = await res.json()
|
|
const idx = devices.value.findIndex(d => d.id === deviceId)
|
|
if (idx !== -1) devices.value[idx] = updated
|
|
return updated
|
|
}
|
|
|
|
async function unlockImage(deviceId: number): Promise<Device> {
|
|
const res = await fetch(`/api/devices/${deviceId}/lock`, { method: 'DELETE' })
|
|
if (!res.ok) throw new Error('Failed to unlock')
|
|
const updated: Device = await res.json()
|
|
const idx = devices.value.findIndex(d => d.id === deviceId)
|
|
if (idx !== -1) devices.value[idx] = updated
|
|
return updated
|
|
}
|
|
|
|
return { devices, loading, error, fetchDevices, updateDevice, lockImage, unlockImage }
|
|
})
|