Skip to content

Commit 7fd151e

Browse files
Implement opt-in countdown feature and add to image recipe
1 parent 0d08316 commit 7fd151e

File tree

5 files changed

+167
-1
lines changed

5 files changed

+167
-1
lines changed

stack/lab/Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ RUN jupyter nbextension enable --py --sys-prefix appmode && \
4747
# Swap appmode icon for AiiDAlab gears icon, shown during app load
4848
COPY --chown=${NB_UID}:${NB_GID} gears.svg ${CONDA_DIR}/share/jupyter/nbextensions/appmode/gears.svg
4949

50+
# Set up opt-in countdown feature
51+
ARG PYTHON_MINOR_VERSION
52+
ENV PYTHON_MINOR_VERSION=${PYTHON_MINOR_VERSION}
53+
COPY --chown=${NB_UID}:${NB_GID} countdown/ ${CONDA_DIR}/lib/python${PYTHON_MINOR_VERSION}/site-packages/notebook/static/custom/
54+
5055
# Copy start-up scripts for AiiDAlab.
5156
COPY before-notebook.d/* /usr/local/bin/before-notebook.d/
5257

@@ -99,5 +104,4 @@ ENV NOTEBOOK_ARGS=\
99104
"--TerminalManager.cull_interval=300"
100105

101106
# Set up the logo of notebook interface
102-
ARG PYTHON_MINOR_VERSION
103107
COPY --chown=${NB_UID}:${NB_GID} aiidalab-wide-logo.png ${CONDA_DIR}/lib/python${PYTHON_MINOR_VERSION}/site-packages/notebook/static/base/images/logo.png
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
set -e
3+
4+
CUSTOM_DIR="${CONDA_DIR}/lib/python${PYTHON_MINOR_VERSION}/site-packages/notebook/static/custom"
5+
6+
if [ "$LIFETIME" ]; then
7+
EPHEMERAL=true
8+
9+
# Convert LIFETIME from HH:MM:SS to seconds
10+
IFS=: read -r H M S <<<"$LIFETIME"
11+
LIFETIME_SEC=$((10#$H * 3600 + 10#$M * 60 + 10#$S))
12+
13+
# Calculate expiry timestamp in UTC
14+
EXPIRY=$(date -u -d "+${LIFETIME_SEC} seconds" +"%Y-%m-%dT%H:%M:%SZ")
15+
export EXPIRY
16+
else
17+
EPHEMERAL=false
18+
fi
19+
20+
export EPHEMERAL
21+
envsubst <"${CUSTOM_DIR}/config.json.template" >"$CUSTOM_DIR/config.json"
22+
rm "${CUSTOM_DIR}/config.json.template"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"ephemeral": ${EPHEMERAL},
3+
"expiry": "${EXPIRY}"
4+
}

stack/lab/countdown/custom.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#culling-countdown {
2+
position: sticky;
3+
top: 0;
4+
left: 0;
5+
width: 100%;
6+
background-color: #0078d4;
7+
color: white;
8+
text-align: center;
9+
padding: 8px;
10+
font-size: 18px;
11+
font-weight: bold;
12+
z-index: 9999;
13+
}
14+
15+
#shutdown-warning,
16+
#save-info {
17+
display: none;
18+
}
19+
20+
#shutdown-warning {
21+
font-size: 20px;
22+
}
23+
24+
#save-info {
25+
font-size: 16px;
26+
text-align: center;
27+
font-weight: normal;
28+
font-size: 18px;
29+
}
30+
31+
#culling-timer {
32+
margin-left: 5px;
33+
}

stack/lab/countdown/custom.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
require(["base/js/namespace", "base/js/events"], (Jupyter, events) => {
2+
const parseLifetimeToMs = (str) => {
3+
const parts = str.split(":").map(Number);
4+
if (parts.length !== 3 || parts.some(isNaN)) {
5+
return null;
6+
}
7+
const [h, m, s] = parts;
8+
return ((h * 60 + m) * 60 + s) * 1000;
9+
};
10+
11+
const insertCountdown = (remainingMs) => {
12+
if (document.getElementById("culling-countdown")) {
13+
return;
14+
}
15+
16+
const banner = document.createElement("div");
17+
banner.id = "culling-countdown";
18+
19+
const shutdownWarning = document.createElement("div");
20+
shutdownWarning.id = "shutdown-warning";
21+
shutdownWarning.innerHTML = "⚠️ Shutdown imminent! ⚠️";
22+
banner.appendChild(shutdownWarning);
23+
24+
const countdown = document.createElement("div");
25+
countdown.id = "countdown";
26+
countdown.innerHTML = `Session time remaining: `;
27+
const timer = document.createElement("span");
28+
timer.id = "culling-timer";
29+
timer.innerHTML = "Calculating...";
30+
countdown.appendChild(timer);
31+
banner.appendChild(countdown);
32+
33+
const saveInfo = document.createElement("div");
34+
saveInfo.id = "save-info";
35+
saveInfo.innerHTML = `
36+
Consider saving your work using the <b>File Manager</b> or the <b>Terminal</b>
37+
`;
38+
banner.appendChild(saveInfo);
39+
40+
const endTime = new Date(Date.now() + remainingMs);
41+
42+
const formatTime = (seconds) => {
43+
const hrs = `${Math.floor(seconds / 3600)}`.padStart(2, "0");
44+
const mins = `${Math.floor((seconds % 3600) / 60)}`.padStart(2, "0");
45+
const secs = `${Math.floor(seconds % 60)}`.padStart(2, "0");
46+
return `${hrs}:${mins}:${secs}`;
47+
};
48+
49+
const updateTimer = () => {
50+
const now = new Date();
51+
const timeLeft = (endTime - now) / 1000;
52+
if (timeLeft < 0) {
53+
clearInterval(interval);
54+
return;
55+
}
56+
if (timeLeft < 1800) {
57+
banner.style.backgroundColor = "#DAA801";
58+
saveInfo.style.display = "block";
59+
}
60+
if (timeLeft < 300) {
61+
banner.style.backgroundColor = "red";
62+
shutdownWarning.style.display = "block";
63+
shutdownWarning.innerHTML = "⚠️ Shutdown imminent ⚠️";
64+
}
65+
timer.innerHTML = formatTime(timeLeft);
66+
};
67+
68+
updateTimer();
69+
const interval = setInterval(updateTimer, 1000);
70+
71+
const container = document.getElementById("header");
72+
if (container) {
73+
container.parentNode.insertBefore(banner, container);
74+
}
75+
};
76+
77+
loadCountdown = async () => {
78+
try {
79+
const response = await fetch("/static/custom/config.json");
80+
const config = await response.json();
81+
82+
// Opt-in point for deployments
83+
if (!config.ephemeral) {
84+
return;
85+
}
86+
87+
if (!config.expiry) {
88+
console.warn("Missing `expiry` in config file");
89+
return;
90+
}
91+
92+
const expiry = new Date(config.expiry).getTime();
93+
const remaining = expiry - Date.now();
94+
95+
insertCountdown(Math.max(0, remaining));
96+
} catch (err) {
97+
console.error("Countdown init failed:", err);
98+
}
99+
};
100+
101+
events.on("app_initialized.NotebookApp", loadCountdown);
102+
loadCountdown();
103+
});

0 commit comments

Comments
 (0)