Fix: Force pull images in deploy workflow
All checks were successful
Build and Push / build-all (push) Successful in 7m30s

This commit is contained in:
doomtube 2026-01-06 23:56:06 -05:00
parent 7f56f19e94
commit 0bb461498e
7 changed files with 104 additions and 23 deletions

View file

@ -1081,7 +1081,7 @@ void AdminController::approveStickerSubmission(const HttpRequestPtr &req,
// Insert into stickers table
*dbClient << "INSERT INTO stickers (name, file_path) VALUES ($1, $2) RETURNING id"
<< stickerName << filePath
>> [callback, dbClient, id, reviewerId, stickerName](const Result& insertResult) {
>> [callback, dbClient, id, reviewerId, stickerName, filePath](const Result& insertResult) {
if (insertResult.empty()) {
callback(jsonError("Failed to create sticker"));
return;
@ -1093,7 +1093,7 @@ void AdminController::approveStickerSubmission(const HttpRequestPtr &req,
*dbClient << "UPDATE sticker_submissions SET status = 'approved', "
"reviewed_by = $1, reviewed_at = CURRENT_TIMESTAMP WHERE id = $2"
<< reviewerId << id
>> [callback, newStickerId, stickerName](const Result&) {
>> [callback, newStickerId, stickerName, filePath](const Result&) {
// Notify chat-service to refresh sticker cache
notifyChatServiceStickerUpdate();
@ -1102,6 +1102,7 @@ void AdminController::approveStickerSubmission(const HttpRequestPtr &req,
resp["message"] = "Sticker approved and added";
resp["sticker"]["id"] = static_cast<Json::Int64>(newStickerId);
resp["sticker"]["name"] = stickerName;
resp["sticker"]["filePath"] = filePath;
callback(jsonResp(resp));
}
>> [callback](const DrogonDbException& e) {

View file

@ -119,8 +119,29 @@ export function clearMessages() {
}
// Set message history
export function setMessageHistory(messageList) {
messages.set(messageList);
// If merge=true, adds new messages to existing ones (for realm switching in global chat)
// If merge=false (default), replaces all messages (for initial connection)
export function setMessageHistory(messageList, merge = false) {
if (merge) {
messages.update((existing) => {
// Create a Map of existing messages by messageId for O(1) lookup
const existingMap = new Map(existing.map((m) => [m.messageId, m]));
// Add new messages that don't already exist
for (const msg of messageList) {
if (!existingMap.has(msg.messageId)) {
existingMap.set(msg.messageId, msg);
}
}
// Convert back to array and sort by timestamp
return Array.from(existingMap.values()).sort(
(a, b) => new Date(a.timestamp) - new Date(b.timestamp)
);
});
} else {
messages.set(messageList);
}
}
// Toggle channel

View file

@ -132,7 +132,11 @@ class ChatWebSocket {
break;
case 'history':
setMessageHistory(data.messages || []);
// Merge history when switching realms to preserve messages from other realms
// Backend includes realmId only for realm-switch history (from handleJoinRealm)
// Initial connection history (handleNewConnection) has no realmId
const shouldMerge = data.realmId !== undefined;
setMessageHistory(data.messages || [], shouldMerge);
break;
case 'new_message':

View file

@ -38,19 +38,39 @@
// Self-destruct countdown
let countdownSeconds = 0;
let countdownInterval = null;
let countdownStartedAt = null;
let initialCountdown = 0;
$: hasSelfDestruct = message.selfDestructAt && message.selfDestructAt > 0;
function updateCountdown() {
if (!message.selfDestructAt) return;
const now = Date.now();
const remaining = Math.max(0, Math.ceil((message.selfDestructAt - now) / 1000));
if (!countdownStartedAt) return;
// Calculate elapsed time since countdown started (local time only, avoids clock skew)
const elapsedSec = Math.floor((Date.now() - countdownStartedAt) / 1000);
const remaining = Math.max(0, initialCountdown - elapsedSec);
countdownSeconds = remaining;
if (remaining <= 0 && countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
}
function startCountdown() {
if (!message.selfDestructAt || !message.timestamp) return;
// Calculate the intended duration from server timestamps (no clock skew between them)
const intendedDurationSec = Math.ceil((message.selfDestructAt - message.timestamp) / 1000);
// Record when we started counting down locally
countdownStartedAt = Date.now();
initialCountdown = intendedDurationSec;
countdownSeconds = intendedDurationSec;
countdownInterval = setInterval(updateCountdown, 1000);
}
function formatCountdown(seconds) {
if (seconds >= 60) {
const m = Math.floor(seconds / 60);
@ -69,8 +89,7 @@
onMount(async () => {
// Set up self-destruct countdown if applicable
if (message.selfDestructAt && message.selfDestructAt > 0) {
updateCountdown();
countdownInterval = setInterval(updateCountdown, 1000);
startCountdown();
}
// Ensure stickers are loaded (uses shared store - only fetches once across all components)

View file

@ -106,6 +106,18 @@ function createWatchSyncStore() {
repeatCount: data.repeatCount || 0,
isRepeating: true
}));
} else if (data.event === 'locked_restart') {
// Locked video loop - restart from beginning
update(state => ({
...state,
playbackState: 'playing',
currentTime: 0,
serverTime: data.serverTime || Date.now(),
currentVideo: data.currentVideo !== undefined ? data.currentVideo : state.currentVideo,
leadIn: true,
repeatCount: 0,
isRepeating: true // Video is looping (locked)
}));
} else if (data.event === 'skip') {
// Skip resets repeat state
update(state => ({

View file

@ -24,6 +24,21 @@
$: realmName = $page.params.realm;
// Helper to get auth token for WebSocket connections
async function getAuthToken() {
if (!browser) return null;
try {
const response = await fetch('/api/user/token', { credentials: 'include' });
if (response.ok) {
const data = await response.json();
return data.token || null;
}
} catch (e) {
console.error('Failed to get auth token:', e);
}
return null;
}
// Re-check ownership and reconnect WebSocket when auth state changes (login/logout)
let lastAuthUserId = undefined; // undefined = not yet initialized
$: {
@ -37,11 +52,10 @@
// Auth changed - reconnect WebSocket to get updated permissions
lastAuthUserId = currentUserId;
checkOwnership();
const token = browser ? localStorage.getItem('token') : null;
watchSync.disconnect();
setTimeout(() => {
getAuthToken().then(token => {
watchSync.connect(realm.id, token);
}, 100);
});
}
}
}
@ -62,7 +76,7 @@
}
// Connect to watch sync WebSocket
const token = localStorage.getItem('token');
const token = await getAuthToken();
watchSync.connect(realm.id, token);
// Load playlist
@ -129,16 +143,14 @@
// Only run this check once after WebSocket is connected and we know we're the owner
if (!permissionCheckDone && isOwner && !loading && $auth.user) {
// Give the WebSocket a moment to receive welcome message
setTimeout(() => {
setTimeout(async () => {
if (isOwner && !$canControl && realm) {
console.log('Permission mismatch detected: owner but no control. Reconnecting...');
const token = browser ? localStorage.getItem('token') : null;
if (token) {
watchSync.disconnect();
setTimeout(() => {
watchSync.connect(realm.id, token);
}, 100);
}
const token = await getAuthToken();
watchSync.disconnect();
setTimeout(() => {
watchSync.connect(realm.id, token);
}, 100);
}
permissionCheckDone = true;
}, 1500);

View file

@ -907,8 +907,20 @@
});
if (response.ok) {
const data = await response.json();
message = 'Sticker approved and added';
await Promise.all([loadStickers(), loadStickerSubmissions()]);
// Optimistically add the new sticker to the list using response data
if (data.sticker) {
stickers = [...stickers, {
id: data.sticker.id,
name: data.sticker.name,
filePath: data.sticker.filePath
}].sort((a, b) => a.name.localeCompare(b.name));
}
// Remove from pending submissions
await loadStickerSubmissions();
} else {
const data = await response.json();
error = data.error || 'Failed to approve sticker';