fix: reduce default sync polling interval to improve responsiveness in PreferencesManager

fix: update placeholder in SettingsScreen to reflect new sync interval and enhance user clarity
fix: implement date formatting function in webRoutes for consistent timestamp display
This commit is contained in:
2025-10-26 15:49:28 +01:00
parent 540c8d4800
commit c0c1c0bf35
4 changed files with 82 additions and 30 deletions
@@ -151,7 +151,7 @@ class PreferencesManager(context: Context) {
private const val KEY_SORT_ORDER = "sort_order" private const val KEY_SORT_ORDER = "sort_order"
private const val KEY_SYNC_POLL_INTERVAL_MS = "sync_poll_interval_ms" private const val KEY_SYNC_POLL_INTERVAL_MS = "sync_poll_interval_ms"
private const val DEFAULT_SERVER_URL = "http://10.0.2.2:3000/api/" private const val DEFAULT_SERVER_URL = "http://10.0.2.2:3000/api/"
private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 5000L private const val DEFAULT_SYNC_POLL_INTERVAL_MS = 250L
private const val MIN_SYNC_POLL_INTERVAL_MS = 250L private const val MIN_SYNC_POLL_INTERVAL_MS = 250L
private const val MAX_SYNC_POLL_INTERVAL_MS = 600_000L // 10 minutes private const val MAX_SYNC_POLL_INTERVAL_MS = 600_000L // 10 minutes
const val THEME_MODE_SYSTEM = "system" const val THEME_MODE_SYSTEM = "system"
@@ -153,9 +153,24 @@ fun ListsScreen(
// Maintain a working list as a mutable state list for smoother edge animations // Maintain a working list as a mutable state list for smoother edge animations
val working = remember { androidx.compose.runtime.mutableStateListOf<CollabList>() } val working = remember { androidx.compose.runtime.mutableStateListOf<CollabList>() }
var dragging by remember { mutableStateOf(false) } var dragging by remember { mutableStateOf(false) }
// Keep working list in sync when not dragging var awaitingDb by remember { mutableStateOf(false) }
LaunchedEffect(lists, dragging) { var pendingOrderIds by remember { mutableStateOf<List<String>?>(null) }
if (!dragging) { // Keep working list in sync, but when we just committed a reorder, wait until DB reflects it
LaunchedEffect(lists) {
val currentIds = lists.map { it.id }
val pending = pendingOrderIds
if (awaitingDb && pending != null) {
if (currentIds == pending) {
// DB has caught up; now allow UI to exit dragging and sync lists
awaitingDb = false
dragging = false
working.clear()
working.addAll(lists)
pendingOrderIds = null
}
// else: still waiting for DB; do not overwrite working yet
} else if (!dragging) {
// Normal path: sync working to latest lists
working.clear() working.clear()
working.addAll(lists) working.addAll(lists)
} }
@@ -170,7 +185,6 @@ fun ListsScreen(
add(to.coerceIn(0, size), removeAt(from)) add(to.coerceIn(0, size), removeAt(from))
} }
val scope = rememberCoroutineScope()
val reorderState = val reorderState =
rememberReorderableLazyListState( rememberReorderableLazyListState(
onMove = { from, to -> onMove = { from, to ->
@@ -179,12 +193,11 @@ fun ListsScreen(
working.move(from.index, to.index) working.move(from.index, to.index)
}, },
onDragEnd = { _, _ -> onDragEnd = { _, _ ->
// Commit first, then keep dragging=true briefly so the UI can animate smoothly // Commit and wait for DB to reflect this order before exiting drag state
viewModel.commitReorder(working.map { it.id }) val ids = working.map { it.id }
scope.launch { pendingOrderIds = ids
delay(150) awaitingDb = true
dragging = false viewModel.commitReorder(ids)
}
}, },
) )
@@ -255,7 +255,7 @@ fun SettingsScreen(
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
singleLine = true, singleLine = true,
placeholder = { Text("5000") }, placeholder = { Text("250") },
) )
Button(onClick = { Button(onClick = {
val parsed = syncIntervalInput.toLongOrNull() val parsed = syncIntervalInput.toLongOrNull()
@@ -278,7 +278,7 @@ fun SettingsScreen(
) )
} else { } else {
Text( Text(
text = "Current: ${'$'}syncIntervalMs ms (min 250, max 600000)", text = "Current: $syncIntervalMs ms (min 250, max 600000)",
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
+55 -16
View File
@@ -187,6 +187,21 @@ router.get('/', (req: Request, res: Response) => {
</div> </div>
<script> <script>
function fmtDate(v) {
if (v === null || v === undefined) return '-';
const n = Number(v);
if (!Number.isNaN(n)) {
const ms = n < 1e12 ? n * 1000 : n; // seconds -> ms if needed
const d = new Date(ms);
return isNaN(d.getTime()) ? '-' : d.toLocaleString();
}
const t = Date.parse(v);
if (!Number.isNaN(t)) {
const d = new Date(t);
return isNaN(d.getTime()) ? '-' : d.toLocaleString();
}
return '-';
}
async function loadData() { async function loadData() {
try { try {
// Add cache-busting timestamp to URL // Add cache-busting timestamp to URL
@@ -237,8 +252,8 @@ router.get('/', (req: Request, res: Response) => {
\${list.isDeleted ? '<span class="badge">DELETED</span>' : ''} \${list.isDeleted ? '<span class="badge">DELETED</span>' : ''}
</div> </div>
<div class="list-id">ID: \${list.id}</div> <div class="list-id">ID: \${list.id}</div>
<div class="list-id">Created: \${new Date(list.createdAt).toLocaleString()}</div> <div class="list-id">Created: \${fmtDate(list.createdAt)}</div>
<div class="list-id">Updated: \${new Date(list.updatedAt).toLocaleString()}</div> <div class="list-id">Updated: \${fmtDate(list.updatedAt)}</div>
</div> </div>
</div> </div>
@@ -269,7 +284,7 @@ router.get('/', (req: Request, res: Response) => {
const value = itemValues.find(v => v.fieldId === field.id); const value = itemValues.find(v => v.fieldId === field.id);
return \`<td>\${value ? value.value : '-'}</td>\`; return \`<td>\${value ? value.value : '-'}</td>\`;
}).join('')} }).join('')}
<td>\${new Date(item.updatedAt).toLocaleString()}</td> <td>\${fmtDate(item.updatedAt)}</td>
</tr> </tr>
\`; \`;
}).join('')} }).join('')}
@@ -313,37 +328,61 @@ router.get('/web/data', async (req: Request, res: Response) => {
const values = await dbAdapter.queryAll('SELECT * FROM item_values'); const values = await dbAdapter.queryAll('SELECT * FROM item_values');
// Convert isDeleted from INTEGER to BOOLEAN // Convert isDeleted from INTEGER to BOOLEAN
// Coerce timestamps to numbers (pg may return strings) // Normalize timestamps to milliseconds since epoch
const toNum = (v: any) => (typeof v === 'string' ? Number(v) : v); const toMillis = (v: any): number | null => {
if (v === null || v === undefined) return null;
if (typeof v === 'number') {
return v < 1e12 ? Math.trunc(v * 1000) : Math.trunc(v);
}
if (typeof v === 'string') {
if (/^\d+$/.test(v)) {
const n = Number(v);
return n < 1e12 ? Math.trunc(n * 1000) : Math.trunc(n);
}
const t = Date.parse(v);
return Number.isNaN(t) ? null : Math.trunc(t);
}
return null;
};
// Normalize key casing across SQLite (camelCase) and Postgres (lowercase)
const pick = (obj: any, a: string, b: string) => obj[a] ?? obj[b];
const formattedLists = (lists as any[]).map(list => ({ const formattedLists = (lists as any[]).map(list => ({
...list, ...list,
isDeleted: !!list.isDeleted, isDeleted: !!pick(list, 'isDeleted', 'isdeleted'),
createdAt: toNum(list.createdAt), createdAt: toMillis(pick(list, 'createdAt', 'createdat')),
updatedAt: toNum(list.updatedAt) updatedAt: toMillis(pick(list, 'updatedAt', 'updatedat'))
})); }));
const formattedFields = (fields as any[]).map(field => ({ const formattedFields = (fields as any[]).map(field => ({
...field, ...field,
isDeleted: !!field.isDeleted, listId: pick(field, 'listId', 'listid'),
createdAt: toNum(field.createdAt), isDeleted: !!pick(field, 'isDeleted', 'isdeleted'),
updatedAt: toNum(field.updatedAt) createdAt: toMillis(pick(field, 'createdAt', 'createdat')),
updatedAt: toMillis(pick(field, 'updatedAt', 'updatedat'))
})); }));
const formattedItems = (items as any[]).map(item => ({ const formattedItems = (items as any[]).map(item => ({
...item, ...item,
isDeleted: !!item.isDeleted, listId: pick(item, 'listId', 'listid'),
createdAt: toNum(item.createdAt), isDeleted: !!pick(item, 'isDeleted', 'isdeleted'),
updatedAt: toNum(item.updatedAt) createdAt: toMillis(pick(item, 'createdAt', 'createdat')),
updatedAt: toMillis(pick(item, 'updatedAt', 'updatedat'))
}));
const formattedValues = (values as any[]).map(v => ({
...v,
itemId: pick(v, 'itemId', 'itemid'),
fieldId: pick(v, 'fieldId', 'fieldid'),
updatedAt: toMillis(pick(v, 'updatedAt', 'updatedat'))
})); }));
res.json({ res.json({
lists: formattedLists, lists: formattedLists,
fields: formattedFields, fields: formattedFields,
items: formattedItems, items: formattedItems,
values, values: formattedValues,
stats: { stats: {
lists: formattedLists.length, lists: formattedLists.length,
fields: formattedFields.length, fields: formattedFields.length,
items: formattedItems.length, items: formattedItems.length,
values: (values as any[]).length values: formattedValues.length
}, },
timestamp: Date.now() // Add timestamp for debugging timestamp: Date.now() // Add timestamp for debugging
}); });