fix: more robust cookie management for plugins requiring launch

Instead of storing a cookie for every plugin that requires launch, we
create a single cookie with '+' separated value. We make use of the
cookieStore native API (available everywhere since June 2025) to access
cookie data. The variables are renamed to be more explicit. We now use
class-based SCSS for styling, instead of manually setting style.display
attribute.
This commit is contained in:
Régis Behmo 2025-08-11 18:46:43 +02:00 committed by Régis Behmo
parent 070f3503c5
commit 097be3e3fe
8 changed files with 76 additions and 58 deletions

View File

@ -181,13 +181,9 @@ async def plugin_toggle(name: str) -> Response:
),
)
if enable_plugin:
response.set_cookie(
f"{constants.WARNING_COOKIE_PREFIX}-{name}",
"requires launch",
max_age=constants.ONE_MONTH,
)
update_plugins_requiring_launch(response, add=name)
else:
response.delete_cookie(f"{constants.WARNING_COOKIE_PREFIX}-{name}")
update_plugins_requiring_launch(response, remove=name)
return response
@ -354,3 +350,41 @@ async def command() -> WerkzeugResponse:
command_args = command_string.split()
tutorclient.CliPool.run_parallel(app, command_args)
return redirect(url_for("advanced"))
def update_plugins_requiring_launch(
response: Response, add: str | None = None, remove: str | None = None
) -> None:
"""
Store the list of plugins for which a recent set of changes require running "local launch".
This list is stored as a "+"-separated string in a cookie. Note that flask will automatically put the content in quotes.
"""
# Note that comma, colon and semi-colon are not supported in cookie values
separator = "+"
# Get current plugins
names = set(
[
cookie
for cookie in request.cookies.get(
constants.PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME, ""
).split(separator)
if cookie
]
)
# Add new plugins
if add:
names.add(add)
# Remove plugins
if remove:
names.discard(remove)
# Update the response
response.set_cookie(
constants.PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME,
separator.join(sorted(names)),
max_age=60 * 60 * 24 * 30, # 1 month
)

View File

@ -1,4 +1,3 @@
SHORT_SLEEP_SECONDS = 0.1
ONE_MONTH = 60 * 60 * 24 * 30
WARNING_COOKIE_PREFIX = "warning-cookie"
PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME = "plugins-require-launch"
ITEMS_PER_PAGE = 100

View File

@ -1,26 +1,20 @@
function setCookie(name, value, days) {
let expires = "";
if (days) {
let date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = "; expires=" + date.toUTCString();
// Handle plugins requiring launch based on the values in the corresponding cookie
const pluginsRequireLaunchCookieName = "plugins-require-launch";
async function displayPluginsRequireLaunchWarning() {
const cookie = await cookieStore.get(pluginsRequireLaunchCookieName);
if (cookie && cookie.value) {
const cookieValue = cookie.value.slice(1, -1); // remove quotes
cookieValue.split('+').map(s => s.trim()).forEach(plugin => {
document.querySelectorAll(`[data-plugin="${plugin}"] .warning-launch-required`).forEach(element => {
element.classList.add("visible");
document.getElementById('warning-launch-required-main').classList.add("visible");
});
});
}
document.cookie = `${name}=${value || ""}${expires}; path=/`;
}
function getCookie(name) {
let nameEQ = name + "=";
return (
document.cookie
.split(";")
.map((cookie) => cookie.trim())
.find((cookie) => cookie.startsWith(nameEQ))
?.slice(nameEQ.length) || null
);
}
function eraseCookie(name) {
document.cookie =
name + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
}
document.body.addEventListener('htmx:afterOnLoad', function(event) {
displayPluginsRequireLaunchWarning();
});
// Handle modal
const modalContainer = document.getElementById("modal_container");
@ -43,15 +37,11 @@ closeToastButtons.forEach((button) => {
hideToast(toast);
});
});
function showToast() {
function showLaunchSuccessfulToast() {
// TODO this is very brittle because it relies on static variables and string values.
if (toast) {
if (toastTitle === "Launch platform was successfully executed") {
document.cookie.split(";").forEach((cookie) => {
let name = cookie.split("=")[0].trim();
if (name.startsWith("warning-cookie")) {
eraseCookie(name);
}
});
cookieStore.delete(pluginsRequireLaunchCookieName);
}
toast.style.display = "flex";
setTimeout(() => {

View File

@ -51,10 +51,11 @@ htmx.on("htmx:sseBeforeMessage", function (evt) {
activateInputs();
// There are certain commands for which we do not show the toast message
// Only show the toast if it was set in the `setToastContent` function and if the command ran successfully
// TODO this is brittle because it relies on a hard-coded "Success!" string that is sent from the backend.
if (data.stdout.includes("Success!")) {
setToastContent(command);
if (toastTitle.textContent.trim()) {
showToast("info");
showLaunchSuccessfulToast();
}
}
if (onPluginPage) {

View File

@ -343,8 +343,11 @@ main {
}
}
}
#warning-main {
#warning-launch-required-main {
display: none;
&.visible {
display: flex;
}
border: 1px solid $gray-2;
border-radius: 0.5em;
align-items: center;
@ -445,8 +448,11 @@ main {
}
}
}
.warning {
.warning-launch-required {
display: none;
&.visible {
display: flex;
}
margin-right: 2em;
img {
width: 2.5em;

View File

@ -30,7 +30,7 @@
</div>
{% endblock %}
{% block warning %}
<div id="warning-main">
<div id="warning-launch-required-main">
<img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt="">
<span>Changes have been made to some plugins that will only take effect after running launch platform.</span>
</div>

View File

@ -1,13 +1,13 @@
{% from '_switch.html' import switch %}
{% for plugin in plugins %}
<div class="installed-plugin" hx-get="{{ url_for('plugin', name=plugin.name) }}" hx-push-url="true">
<div class="installed-plugin" hx-get="{{ url_for('plugin', name=plugin.name) }}" hx-push-url="true" data-plugin="{{ plugin.name }}">
<div class="details">
<div class="name"><a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a></div>
<div class="author">By {{ plugin.author }}</div>
<div class="description">{{ plugin.description|safe }}</div>
</div>
<div class="warning" hx-preserve="true" id="warning-cookie-{{plugin.name}}">
<div class="warning-launch-required" hx-preserve="true">
<img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt="" title="Run launch platform for changes to this plugin to take effect">
</div>

View File

@ -22,17 +22,6 @@ View all your installed plugins in one place.
{% block scripts %}
<script>
// Sets the warning that launch platform needs to be executed for plugins to take effect
function SetWarning(){
const warningElements = document.querySelectorAll('[id^="warning-cookie-"]');
const warningMain = document.getElementById('warning-main');
warningElements.forEach(function(warningElement) {
if (document.cookie.includes(warningElement.id)) {
warningElement.style.display = 'flex';
warningMain.style.display = 'flex';
}
});
}
document.body.addEventListener('htmx:afterOnLoad', function(event) {
let toggleSwitches = document.querySelectorAll(".switch");
toggleSwitches.forEach(toggleSwitch => {
@ -42,7 +31,6 @@ View all your installed plugins in one place.
event.stopPropagation();
}
});
SetWarning();
});
</script>
{% endblock %}