Auto-trigger locked-files restoration exactly at the 60s deadline
Previously the restore check only ran lazily on a player's next message, so files could stay wrongly-removed for an arbitrary amount of time after the window expired. The frontend now schedules a setTimeout (using the server-provided deadline, mirroring the terminal lock's reveal timer) that pings the backend right at the deadline so the check runs promptly regardless of player activity. Restored via data-files-removal-deadline on page load too, so a refresh mid-window doesn't lose the timer.
This commit is contained in:
@@ -177,6 +177,42 @@ function applyLock(lockData, apiEchoUrl, messageContainer) {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filesRemovalTimer = null;
|
||||||
|
let scheduledFilesRemovalDeadline = null;
|
||||||
|
|
||||||
|
async function pingFilesRemovalDeadline(apiEchoUrl) {
|
||||||
|
if (!apiEchoUrl) return;
|
||||||
|
try {
|
||||||
|
// A no-op message is enough to make the server evaluate the deadline server-side;
|
||||||
|
// the actual restore notice (if any) arrives for everyone via the Mercure broadcast.
|
||||||
|
await fetchJson(apiEchoUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { message: '', ts: new Date().toISOString() },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Game1] Failed to ping files-removal deadline:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFilesRemovalCheck(deadline, apiEchoUrl) {
|
||||||
|
if (!deadline || scheduledFilesRemovalDeadline === deadline) {
|
||||||
|
return; // nothing to (re)schedule
|
||||||
|
}
|
||||||
|
scheduledFilesRemovalDeadline = deadline;
|
||||||
|
|
||||||
|
if (filesRemovalTimer) {
|
||||||
|
clearTimeout(filesRemovalTimer);
|
||||||
|
filesRemovalTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = Math.max(0, deadline * 1000 - Date.now());
|
||||||
|
filesRemovalTimer = setTimeout(() => {
|
||||||
|
filesRemovalTimer = null;
|
||||||
|
scheduledFilesRemovalDeadline = null;
|
||||||
|
pingFilesRemovalDeadline(apiEchoUrl);
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchJson(url, options = {}) {
|
async function fetchJson(url, options = {}) {
|
||||||
const opts = { ...options };
|
const opts = { ...options };
|
||||||
const headers = new Headers(opts.headers || {});
|
const headers = new Headers(opts.headers || {});
|
||||||
@@ -236,6 +272,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
const lockLockedAt = cfgEl.dataset.lockLockedAt;
|
const lockLockedAt = cfgEl.dataset.lockLockedAt;
|
||||||
const lockRevealAt = cfgEl.dataset.lockRevealAt;
|
const lockRevealAt = cfgEl.dataset.lockRevealAt;
|
||||||
const lockUnlockAt = cfgEl.dataset.lockUnlockAt;
|
const lockUnlockAt = cfgEl.dataset.lockUnlockAt;
|
||||||
|
const filesRemovalDeadline = cfgEl.dataset.filesRemovalDeadline;
|
||||||
|
|
||||||
|
// Resume the auto-restore timer after a page refresh, if a window is already running
|
||||||
|
if (filesRemovalDeadline) {
|
||||||
|
scheduleFilesRemovalCheck(parseInt(filesRemovalDeadline, 10), apiEchoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (mercurePublicUrl && topic) {
|
if (mercurePublicUrl && topic) {
|
||||||
subscribeToMercure(mercurePublicUrl, topic, screen);
|
subscribeToMercure(mercurePublicUrl, topic, screen);
|
||||||
@@ -391,6 +433,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
} else if (response.result.locked === false) {
|
} else if (response.result.locked === false) {
|
||||||
clearLock();
|
clearLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.result.filesRemovalDeadline) {
|
||||||
|
scheduleFilesRemovalCheck(response.result.filesRemovalDeadline, apiEchoUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[API][game1] Failed to send message:', err);
|
console.error('[API][game1] Failed to send message:', err);
|
||||||
|
|||||||
@@ -160,12 +160,14 @@ final class GameController extends AbstractController
|
|||||||
$screen = $player ? $player->getScreen() : 0;
|
$screen = $player ? $player->getScreen() : 0;
|
||||||
$session_id = $session->getId();
|
$session_id = $session->getId();
|
||||||
$lock = $player ? $gameResponseService->getPublicLockState($player) : null;
|
$lock = $player ? $gameResponseService->getPublicLockState($player) : null;
|
||||||
|
$filesRemovalDeadline = $gameResponseService->getPublicLockedFilesDeadline($session);
|
||||||
|
|
||||||
return $this->render('game/index.html.twig', [
|
return $this->render('game/index.html.twig', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
'screen' => $screen,
|
'screen' => $screen,
|
||||||
'session_id' => $session_id,
|
'session_id' => $session_id,
|
||||||
'lock' => $lock,
|
'lock' => $lock,
|
||||||
|
'filesRemovalDeadline' => $filesRemovalDeadline,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class GameResponseService
|
|||||||
return ['result' => ['You are not allowed to remove this file.']];
|
return ['result' => ['You are not allowed to remove this file.']];
|
||||||
|
|
||||||
$this->playerService->addDeletedFileToSession($player, $fullPath);
|
$this->playerService->addDeletedFileToSession($player, $fullPath);
|
||||||
$this->startLockedFilesRemovalDeadline($player->getSession(), $fullPath);
|
$filesRemovalDeadline = $this->startLockedFilesRemovalDeadline($player->getSession(), $fullPath);
|
||||||
$lock = $this->triggerLock($player);
|
$lock = $this->triggerLock($player);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -243,6 +243,7 @@ class GameResponseService
|
|||||||
'lockedAt' => $lock['lockedAt'],
|
'lockedAt' => $lock['lockedAt'],
|
||||||
'revealAt' => $lock['revealAt'],
|
'revealAt' => $lock['revealAt'],
|
||||||
'unlockAt' => $lock['unlockAt'],
|
'unlockAt' => $lock['unlockAt'],
|
||||||
|
'filesRemovalDeadline' => $filesRemovalDeadline,
|
||||||
];
|
];
|
||||||
case 'sudo':
|
case 'sudo':
|
||||||
if(!in_array('sudo', $rechten))
|
if(!in_array('sudo', $rechten))
|
||||||
@@ -1031,17 +1032,19 @@ class GameResponseService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts (if not already running) the 60-second window within which all locked files
|
* Starts (if not already running) the 60-second window within which all locked files
|
||||||
* must be removed, or the ones already removed get restored by the virus.
|
* must be removed, or the ones already removed get restored by the virus. Returns the
|
||||||
|
* (new or already-running) deadline as a unix timestamp, or null if the removed file
|
||||||
|
* wasn't a locked one.
|
||||||
*/
|
*/
|
||||||
private function startLockedFilesRemovalDeadline(Session $session, string $removedFile): void
|
private function startLockedFilesRemovalDeadline(Session $session, string $removedFile): ?int
|
||||||
{
|
{
|
||||||
if (!in_array($removedFile, $this->getLockedFiles())) {
|
if (!in_array($removedFile, $this->getLockedFiles())) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE);
|
$setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE);
|
||||||
if ($setting && $setting->getValue()) {
|
if ($setting && $setting->getValue()) {
|
||||||
return; // Window already running
|
return (int)$setting->getValue(); // Window already running
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$setting) {
|
if (!$setting) {
|
||||||
@@ -1050,9 +1053,28 @@ class GameResponseService
|
|||||||
$setting->setName(SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE);
|
$setting->setName(SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
$setting->setValue((string)(time() + 60));
|
$deadline = time() + 60;
|
||||||
|
$setting->setValue((string)$deadline);
|
||||||
$this->entityManager->persist($setting);
|
$this->entityManager->persist($setting);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return $deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public, session-wide view of the active locked-files removal deadline (if any), safe
|
||||||
|
* to expose on page load so the UI can schedule its auto-check timer after a refresh.
|
||||||
|
*/
|
||||||
|
public function getPublicLockedFilesDeadline(Session $session): ?int
|
||||||
|
{
|
||||||
|
$setting = $this->sessionSettingRepository->getSetting($session, SessionSettingType::LOCKED_FILES_REMOVAL_DEADLINE);
|
||||||
|
if (!$setting || !$setting->getValue()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deadline = (int)$setting->getValue();
|
||||||
|
|
||||||
|
return $deadline > time() ? $deadline : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,9 @@
|
|||||||
data-lock-reveal-at="{{ lock.revealAt }}"
|
data-lock-reveal-at="{{ lock.revealAt }}"
|
||||||
data-lock-unlock-at="{{ lock.unlockAt }}"
|
data-lock-unlock-at="{{ lock.unlockAt }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if filesRemovalDeadline %}
|
||||||
|
data-files-removal-deadline="{{ filesRemovalDeadline }}"
|
||||||
|
{% endif %}
|
||||||
style="display:none">
|
style="display:none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user