Fix: Force pull images in deploy workflow
All checks were successful
Build and Push / build-all (push) Successful in 8m52s
All checks were successful
Build and Push / build-all (push) Successful in 8m52s
This commit is contained in:
parent
5430e434c3
commit
7f56f19e94
10 changed files with 164 additions and 30 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -150,3 +150,4 @@ temp/
|
||||||
# Claude Code
|
# Claude Code
|
||||||
# ======================================================
|
# ======================================================
|
||||||
.claude/
|
.claude/
|
||||||
|
nul
|
||||||
|
|
|
||||||
|
|
@ -2536,7 +2536,7 @@ void AdminController::uploadDefaultAvatars(const HttpRequestPtr &req,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create avatars directory if it doesn't exist
|
// Create avatars directory if it doesn't exist
|
||||||
std::string avatarsDir = "/data/uploads/avatars";
|
std::string avatarsDir = "/app/uploads/avatars";
|
||||||
std::filesystem::create_directories(avatarsDir);
|
std::filesystem::create_directories(avatarsDir);
|
||||||
|
|
||||||
Json::Value uploaded(Json::arrayValue);
|
Json::Value uploaded(Json::arrayValue);
|
||||||
|
|
@ -2667,7 +2667,7 @@ void AdminController::deleteDefaultAvatar(const HttpRequestPtr &req,
|
||||||
<< id
|
<< id
|
||||||
>> [callback, filePath](const Result&) {
|
>> [callback, filePath](const Result&) {
|
||||||
// Delete file from filesystem
|
// Delete file from filesystem
|
||||||
std::string fullPath = "/data" + filePath;
|
std::string fullPath = "/app" + filePath;
|
||||||
std::filesystem::remove(fullPath);
|
std::filesystem::remove(fullPath);
|
||||||
|
|
||||||
Json::Value resp;
|
Json::Value resp;
|
||||||
|
|
|
||||||
|
|
@ -562,20 +562,87 @@ void WatchController::removeFromPlaylist(const HttpRequestPtr &req,
|
||||||
<< iId << rId
|
<< iId << rId
|
||||||
>> [callback, dbClient, rId, isDeletingCurrentVideo](const Result&) {
|
>> [callback, dbClient, rId, isDeletingCurrentVideo](const Result&) {
|
||||||
if (isDeletingCurrentVideo) {
|
if (isDeletingCurrentVideo) {
|
||||||
// Clear the current video state
|
// FIX: Instead of just clearing the state, try to advance to next video
|
||||||
*dbClient << "UPDATE watch_room_state SET current_video_id = NULL, "
|
// Get next queued video (prioritize locked, then by created_at)
|
||||||
"playback_state = 'paused', current_time_seconds = 0, "
|
*dbClient << "SELECT id, youtube_video_id, title, duration_seconds, thumbnail_url, is_locked "
|
||||||
"last_sync_at = CURRENT_TIMESTAMP WHERE realm_id = $1"
|
"FROM watch_playlist "
|
||||||
|
"WHERE realm_id = $1 AND status = 'queued' "
|
||||||
|
"ORDER BY is_locked DESC NULLS LAST, created_at ASC LIMIT 1"
|
||||||
<< rId
|
<< rId
|
||||||
>> [callback](const Result&) {
|
>> [callback, dbClient, rId](const Result& nextResult) {
|
||||||
Json::Value resp;
|
if (nextResult.empty()) {
|
||||||
resp["success"] = true;
|
// No more videos - clear the state
|
||||||
resp["clearedCurrentVideo"] = true;
|
*dbClient << "UPDATE watch_room_state SET current_video_id = NULL, "
|
||||||
callback(jsonResp(resp));
|
"playback_state = 'paused', current_time_seconds = 0, "
|
||||||
|
"last_sync_at = CURRENT_TIMESTAMP WHERE realm_id = $1"
|
||||||
|
<< rId
|
||||||
|
>> [callback](const Result&) {
|
||||||
|
Json::Value resp;
|
||||||
|
resp["success"] = true;
|
||||||
|
resp["clearedCurrentVideo"] = true;
|
||||||
|
resp["currentVideo"] = Json::nullValue;
|
||||||
|
resp["playbackState"] = "paused";
|
||||||
|
callback(jsonResp(resp));
|
||||||
|
}
|
||||||
|
>> [callback](const DrogonDbException& e) {
|
||||||
|
LOG_ERROR << "Failed to clear room state: " << e.base().what();
|
||||||
|
Json::Value resp;
|
||||||
|
resp["success"] = true;
|
||||||
|
callback(jsonResp(resp));
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Advance to next video
|
||||||
|
int64_t nextVideoId = nextResult[0]["id"].as<int64_t>();
|
||||||
|
std::string youtubeVideoId = nextResult[0]["youtube_video_id"].as<std::string>();
|
||||||
|
std::string title = nextResult[0]["title"].as<std::string>();
|
||||||
|
int durationSeconds = nextResult[0]["duration_seconds"].as<int>();
|
||||||
|
std::string thumbnailUrl = nextResult[0]["thumbnail_url"].isNull() ? "" : nextResult[0]["thumbnail_url"].as<std::string>();
|
||||||
|
bool isLocked = nextResult[0]["is_locked"].isNull() ? false : nextResult[0]["is_locked"].as<bool>();
|
||||||
|
|
||||||
|
// Update next video to playing
|
||||||
|
*dbClient << "UPDATE watch_playlist SET status = 'playing', "
|
||||||
|
"started_at = CURRENT_TIMESTAMP WHERE id = $1"
|
||||||
|
<< nextVideoId
|
||||||
|
>> [callback, dbClient, rId, nextVideoId, youtubeVideoId, title, durationSeconds, thumbnailUrl, isLocked](const Result&) {
|
||||||
|
// Update room state to point to new video
|
||||||
|
*dbClient << "UPDATE watch_room_state SET current_video_id = $1, "
|
||||||
|
"playback_state = 'playing', current_time_seconds = 0, "
|
||||||
|
"last_sync_at = CURRENT_TIMESTAMP WHERE realm_id = $2"
|
||||||
|
<< nextVideoId << rId
|
||||||
|
>> [callback, nextVideoId, youtubeVideoId, title, durationSeconds, thumbnailUrl, isLocked](const Result&) {
|
||||||
|
Json::Value resp;
|
||||||
|
resp["success"] = true;
|
||||||
|
resp["advancedToNext"] = true;
|
||||||
|
resp["playbackState"] = "playing";
|
||||||
|
resp["currentTime"] = 0.0;
|
||||||
|
|
||||||
|
Json::Value video;
|
||||||
|
video["id"] = static_cast<Json::Int64>(nextVideoId);
|
||||||
|
video["youtubeVideoId"] = youtubeVideoId;
|
||||||
|
video["title"] = title;
|
||||||
|
video["durationSeconds"] = durationSeconds;
|
||||||
|
video["thumbnailUrl"] = thumbnailUrl;
|
||||||
|
video["isLocked"] = isLocked;
|
||||||
|
resp["currentVideo"] = video;
|
||||||
|
callback(jsonResp(resp));
|
||||||
|
}
|
||||||
|
>> [callback](const DrogonDbException& e) {
|
||||||
|
LOG_ERROR << "Failed to update room state: " << e.base().what();
|
||||||
|
Json::Value resp;
|
||||||
|
resp["success"] = true;
|
||||||
|
callback(jsonResp(resp));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>> [callback](const DrogonDbException& e) {
|
||||||
|
LOG_ERROR << "Failed to update next video: " << e.base().what();
|
||||||
|
Json::Value resp;
|
||||||
|
resp["success"] = true;
|
||||||
|
callback(jsonResp(resp));
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
>> [callback](const DrogonDbException& e) {
|
>> [callback](const DrogonDbException& e) {
|
||||||
LOG_ERROR << "Failed to clear room state: " << e.base().what();
|
LOG_ERROR << "Failed to get next video: " << e.base().what();
|
||||||
// Still return success since video was deleted
|
|
||||||
Json::Value resp;
|
Json::Value resp;
|
||||||
resp["success"] = true;
|
resp["success"] = true;
|
||||||
callback(jsonResp(resp));
|
callback(jsonResp(resp));
|
||||||
|
|
|
||||||
|
|
@ -148,5 +148,6 @@ private:
|
||||||
static constexpr int64_t SKIP_DEBOUNCE_MS = 1000;
|
static constexpr int64_t SKIP_DEBOUNCE_MS = 1000;
|
||||||
|
|
||||||
// Database sync freshness threshold (refresh from DB if older than this)
|
// Database sync freshness threshold (refresh from DB if older than this)
|
||||||
static constexpr int64_t DB_SYNC_STALE_MS = 5000;
|
// Reduced to 2 seconds for faster response to state changes (video deletion, etc.)
|
||||||
|
static constexpr int64_t DB_SYNC_STALE_MS = 2000;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -330,12 +330,6 @@
|
||||||
{/if}
|
{/if}
|
||||||
<button class="username-btn" style="color: {safeUserColor}" on:click={handleUsernameClick} on:dblclick={handleUsernameDoubleClick}>
|
<button class="username-btn" style="color: {safeUserColor}" on:click={handleUsernameClick} on:dblclick={handleUsernameDoubleClick}>
|
||||||
{message.username}
|
{message.username}
|
||||||
{#if message.isStreamer}
|
|
||||||
<span class="badge streamer">STREAMER</span>
|
|
||||||
{/if}
|
|
||||||
{#if message.isGuest}
|
|
||||||
<span class="badge guest">GUEST</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
</button>
|
||||||
{#if message.usedRoll}
|
{#if message.usedRoll}
|
||||||
<span class="cmd-tag roll-tag">R</span>
|
<span class="cmd-tag roll-tag">R</span>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
} from '$lib/chat/chatStore';
|
} from '$lib/chat/chatStore';
|
||||||
import { chatWebSocket } from '$lib/chat/chatWebSocket';
|
import { chatWebSocket } from '$lib/chat/chatWebSocket';
|
||||||
import { chatLayout } from '$lib/stores/chatLayout';
|
import { chatLayout } from '$lib/stores/chatLayout';
|
||||||
|
import { auth } from '$lib/stores/auth';
|
||||||
import {
|
import {
|
||||||
ttsEnabled,
|
ttsEnabled,
|
||||||
ttsSettings,
|
ttsSettings,
|
||||||
|
|
@ -238,7 +239,19 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSendMessage(event) {
|
function handleSendMessage(event) {
|
||||||
const { message, selfDestructSeconds } = event.detail;
|
let { message, selfDestructSeconds } = event.detail;
|
||||||
|
|
||||||
|
// Handle /graffiti command
|
||||||
|
if (message.trim().toLowerCase() === '/graffiti') {
|
||||||
|
const graffitiUrl = $auth.user?.graffitiUrl;
|
||||||
|
if (graffitiUrl) {
|
||||||
|
message = `[graffiti]${graffitiUrl}[/graffiti]`;
|
||||||
|
} else {
|
||||||
|
alert('You don\'t have a graffiti yet. Create one in Settings > Appearance.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chatWebSocket.sendMessage(message, userColor, selfDestructSeconds || 0);
|
chatWebSocket.sendMessage(message, userColor, selfDestructSeconds || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,14 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
dispatch('videoRemoved', { itemId });
|
const data = await response.json();
|
||||||
|
// Pass the full response data so the parent can update player state
|
||||||
|
dispatch('videoRemoved', {
|
||||||
|
itemId,
|
||||||
|
advancedToNext: data.advancedToNext || false,
|
||||||
|
currentVideo: data.currentVideo || null,
|
||||||
|
playbackState: data.playbackState || null
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to remove video:', e);
|
console.error('Failed to remove video:', e);
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,25 @@
|
||||||
|
|
||||||
$: realmName = $page.params.realm;
|
$: realmName = $page.params.realm;
|
||||||
|
|
||||||
// Re-check ownership when auth state changes (login/logout)
|
// Re-check ownership and reconnect WebSocket when auth state changes (login/logout)
|
||||||
|
let lastAuthUserId = undefined; // undefined = not yet initialized
|
||||||
$: {
|
$: {
|
||||||
$auth; // Track auth store changes
|
const currentUserId = $auth.user?.id || null;
|
||||||
if (realm) {
|
if (realm && !loading) {
|
||||||
checkOwnership();
|
// Initialize on first run after loading completes
|
||||||
|
if (lastAuthUserId === undefined) {
|
||||||
|
lastAuthUserId = currentUserId;
|
||||||
|
checkOwnership();
|
||||||
|
} else if (currentUserId !== lastAuthUserId) {
|
||||||
|
// Auth changed - reconnect WebSocket to get updated permissions
|
||||||
|
lastAuthUserId = currentUserId;
|
||||||
|
checkOwnership();
|
||||||
|
const token = browser ? localStorage.getItem('token') : null;
|
||||||
|
watchSync.disconnect();
|
||||||
|
setTimeout(() => {
|
||||||
|
watchSync.connect(realm.id, token);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +122,29 @@
|
||||||
isOwner = (user?.id === realm.ownerId) || user?.isAdmin;
|
isOwner = (user?.id === realm.ownerId) || user?.isAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect permission mismatch: if user is owner but WebSocket doesn't have control
|
||||||
|
// This can happen if the WebSocket connected before auth was ready
|
||||||
|
let permissionCheckDone = false;
|
||||||
|
$: {
|
||||||
|
// 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(() => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permissionCheckDone = true;
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleVideoAdded(event) {
|
function handleVideoAdded(event) {
|
||||||
loadPlaylist();
|
loadPlaylist();
|
||||||
// Request sync to get updated current video state (video may have auto-started)
|
// Request sync to get updated current video state (video may have auto-started)
|
||||||
|
|
@ -118,10 +155,16 @@
|
||||||
|
|
||||||
function handleVideoRemoved(event) {
|
function handleVideoRemoved(event) {
|
||||||
loadPlaylist();
|
loadPlaylist();
|
||||||
// Request sync to get updated state (current video may have been cleared)
|
// If the backend advanced to next video, request sync immediately to update player
|
||||||
setTimeout(() => {
|
if (event.detail?.advancedToNext || event.detail?.currentVideo) {
|
||||||
|
// Request sync immediately to get the new video playing
|
||||||
watchSync.requestSync();
|
watchSync.requestSync();
|
||||||
}, 500);
|
} else {
|
||||||
|
// Request sync after a short delay for other cases
|
||||||
|
setTimeout(() => {
|
||||||
|
watchSync.requestSync();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePlayerReady() {
|
function handlePlayerReady() {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Nakama server modules for realms.india",
|
"description": "Nakama server modules for realms.india",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx esbuild src/main.ts --bundle --outfile=build/index.js --format=cjs --target=es2020 --platform=neutral --main-fields=main,module"
|
"build": "npx esbuild src/main.ts --bundle --outfile=build/index.js --format=cjs --target=es2020 --platform=node --main-fields=main,module"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.19.0",
|
"esbuild": "^0.19.0",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,14 @@ local function get_redis_connection()
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Select the correct Redis database (db 1, matching backend config)
|
||||||
|
local db = tonumber(os.getenv("REDIS_DB")) or 1
|
||||||
|
local res, err = red:select(db)
|
||||||
|
if not res then
|
||||||
|
ngx.log(ngx.ERR, "Failed to select Redis database: ", err)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Authenticate if password is set
|
-- Authenticate if password is set
|
||||||
if REDIS_PASSWORD and REDIS_PASSWORD ~= "" then
|
if REDIS_PASSWORD and REDIS_PASSWORD ~= "" then
|
||||||
local res, err = red:auth(REDIS_PASSWORD)
|
local res, err = red:auth(REDIS_PASSWORD)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue