Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion stack/lab/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ RUN jupyter nbextension enable --py --sys-prefix appmode && \
# Swap appmode icon for AiiDAlab gears icon, shown during app load
COPY --chown=${NB_UID}:${NB_GID} gears.svg ${CONDA_DIR}/share/jupyter/nbextensions/appmode/gears.svg

# Set up opt-in countdown feature
ARG PYTHON_MINOR_VERSION
ENV PYTHON_MINOR_VERSION=${PYTHON_MINOR_VERSION}
COPY --chown=${NB_UID}:${NB_GID} countdown/ ${CONDA_DIR}/lib/python${PYTHON_MINOR_VERSION}/site-packages/notebook/static/custom/

Comment on lines +50 to +54
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines were moved to here when we debug it together. Can you move it to where the logo.png is copied, and see if it still work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't work due to the order of events I believe

# Copy start-up scripts for AiiDAlab.
COPY before-notebook.d/* /usr/local/bin/before-notebook.d/

Expand Down Expand Up @@ -99,5 +104,4 @@ ENV NOTEBOOK_ARGS=\
"--TerminalManager.cull_interval=300"

# Set up the logo of notebook interface
ARG PYTHON_MINOR_VERSION
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
22 changes: 22 additions & 0 deletions stack/lab/before-notebook.d/70_prepare_countdown_config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
set -e

CUSTOM_DIR="${CONDA_DIR}/lib/python${PYTHON_MINOR_VERSION}/site-packages/notebook/static/custom"

if [ "$LIFETIME" ]; then
EPHEMERAL=true

# Convert LIFETIME from HH:MM:SS to seconds
IFS=: read -r H M S <<<"$LIFETIME"
LIFETIME_SEC=$((10#$H * 3600 + 10#$M * 60 + 10#$S))

# Calculate expiry timestamp in UTC
EXPIRY=$(date -u -d "+${LIFETIME_SEC} seconds" +"%Y-%m-%dT%H:%M:%SZ")
export EXPIRY
else
EPHEMERAL=false
fi

export EPHEMERAL
envsubst <"${CUSTOM_DIR}/config.json.template" >"$CUSTOM_DIR/config.json"
rm "${CUSTOM_DIR}/config.json.template"
4 changes: 4 additions & 0 deletions stack/lab/countdown/config.json.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"ephemeral": ${EPHEMERAL},
"expiry": "${EXPIRY}"
}
33 changes: 33 additions & 0 deletions stack/lab/countdown/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#culling-countdown {
position: sticky;
top: 0;
left: 0;
width: 100%;
background-color: #0078d4;
color: white;
text-align: center;
padding: 8px;
font-size: 18px;
font-weight: bold;
z-index: 9999;
}

#shutdown-warning,
#save-info {
display: none;
}

#shutdown-warning {
font-size: 20px;
}

#save-info {
font-size: 16px;
text-align: center;
font-weight: normal;
font-size: 18px;
}

#culling-timer {
margin-left: 5px;
}
103 changes: 103 additions & 0 deletions stack/lab/countdown/custom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
require(["base/js/namespace", "base/js/events"], (Jupyter, events) => {
const parseLifetimeToMs = (str) => {
const parts = str.split(":").map(Number);
if (parts.length !== 3 || parts.some(isNaN)) {
return null;
}
const [h, m, s] = parts;
return ((h * 60 + m) * 60 + s) * 1000;
};

const insertCountdown = (remainingMs) => {
if (document.getElementById("culling-countdown")) {
return;
}

const banner = document.createElement("div");
banner.id = "culling-countdown";

const shutdownWarning = document.createElement("div");
shutdownWarning.id = "shutdown-warning";
shutdownWarning.innerHTML = "⚠️ Shutdown imminent! ⚠️";
banner.appendChild(shutdownWarning);

const countdown = document.createElement("div");
countdown.id = "countdown";
countdown.innerHTML = `Session time remaining: `;
const timer = document.createElement("span");
timer.id = "culling-timer";
timer.innerHTML = "Calculating...";
countdown.appendChild(timer);
banner.appendChild(countdown);

const saveInfo = document.createElement("div");
saveInfo.id = "save-info";
saveInfo.innerHTML = `
Consider saving your work using the <b>File Manager</b> or the <b>Terminal</b>
`;
banner.appendChild(saveInfo);

const endTime = new Date(Date.now() + remainingMs);

const formatTime = (seconds) => {
const hrs = `${Math.floor(seconds / 3600)}`.padStart(2, "0");
const mins = `${Math.floor((seconds % 3600) / 60)}`.padStart(2, "0");
const secs = `${Math.floor(seconds % 60)}`.padStart(2, "0");
return `${hrs}:${mins}:${secs}`;
};

const updateTimer = () => {
const now = new Date();
const timeLeft = (endTime - now) / 1000;
if (timeLeft < 0) {
clearInterval(interval);
return;
}
if (timeLeft < 1800) {
banner.style.backgroundColor = "#DAA801";
saveInfo.style.display = "block";
}
if (timeLeft < 300) {
banner.style.backgroundColor = "red";
shutdownWarning.style.display = "block";
shutdownWarning.innerHTML = "⚠️ Shutdown imminent ⚠️";
}
timer.innerHTML = formatTime(timeLeft);
};

updateTimer();
const interval = setInterval(updateTimer, 1000);

const container = document.getElementById("header");
if (container) {
container.parentNode.insertBefore(banner, container);
}
};

loadCountdown = async () => {
try {
const response = await fetch("/static/custom/config.json");
const config = await response.json();

// Opt-in point for deployments
if (!config.ephemeral) {
return;
}

if (!config.expiry) {
console.warn("Missing `expiry` in config file");
return;
}

const expiry = new Date(config.expiry).getTime();
const remaining = expiry - Date.now();

insertCountdown(Math.max(0, remaining));
} catch (err) {
console.error("Countdown init failed:", err);
}
};

events.on("app_initialized.NotebookApp", loadCountdown);
loadCountdown();
});
Loading