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: if enable_plugin:
response.set_cookie( update_plugins_requiring_launch(response, add=name)
f"{constants.WARNING_COOKIE_PREFIX}-{name}",
"requires launch",
max_age=constants.ONE_MONTH,
)
else: else:
response.delete_cookie(f"{constants.WARNING_COOKIE_PREFIX}-{name}") update_plugins_requiring_launch(response, remove=name)
return response return response
@ -354,3 +350,41 @@ async def command() -> WerkzeugResponse:
command_args = command_string.split() command_args = command_string.split()
tutorclient.CliPool.run_parallel(app, command_args) tutorclient.CliPool.run_parallel(app, command_args)
return redirect(url_for("advanced")) 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 SHORT_SLEEP_SECONDS = 0.1
ONE_MONTH = 60 * 60 * 24 * 30 PLUGINS_REQUIRE_LAUNCH_COOKIE_NAME = "plugins-require-launch"
WARNING_COOKIE_PREFIX = "warning-cookie"
ITEMS_PER_PAGE = 100 ITEMS_PER_PAGE = 100

View File

@ -1,26 +1,20 @@
function setCookie(name, value, days) { // Handle plugins requiring launch based on the values in the corresponding cookie
let expires = ""; const pluginsRequireLaunchCookieName = "plugins-require-launch";
if (days) { async function displayPluginsRequireLaunchWarning() {
let date = new Date(); const cookie = await cookieStore.get(pluginsRequireLaunchCookieName);
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); if (cookie && cookie.value) {
expires = "; expires=" + date.toUTCString(); 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 // Handle modal
const modalContainer = document.getElementById("modal_container"); const modalContainer = document.getElementById("modal_container");
@ -43,15 +37,11 @@ closeToastButtons.forEach((button) => {
hideToast(toast); hideToast(toast);
}); });
}); });
function showToast() { function showLaunchSuccessfulToast() {
// TODO this is very brittle because it relies on static variables and string values.
if (toast) { if (toast) {
if (toastTitle === "Launch platform was successfully executed") { if (toastTitle === "Launch platform was successfully executed") {
document.cookie.split(";").forEach((cookie) => { cookieStore.delete(pluginsRequireLaunchCookieName);
let name = cookie.split("=")[0].trim();
if (name.startsWith("warning-cookie")) {
eraseCookie(name);
}
});
} }
toast.style.display = "flex"; toast.style.display = "flex";
setTimeout(() => { setTimeout(() => {

View File

@ -51,10 +51,11 @@ htmx.on("htmx:sseBeforeMessage", function (evt) {
activateInputs(); activateInputs();
// There are certain commands for which we do not show the toast message // 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 // 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!")) { if (data.stdout.includes("Success!")) {
setToastContent(command); setToastContent(command);
if (toastTitle.textContent.trim()) { if (toastTitle.textContent.trim()) {
showToast("info"); showLaunchSuccessfulToast();
} }
} }
if (onPluginPage) { if (onPluginPage) {

View File

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

View File

@ -14,12 +14,12 @@
<div class="search-and-button"> <div class="search-and-button">
<div class="search"> <div class="search">
<img src="{{ url_for('static', filename='img/search.svg') }}"/> <img src="{{ url_for('static', filename='img/search.svg') }}"/>
<input <input
id="search-input" id="search-input"
type="text" type="text"
class="form-control search-input" class="form-control search-input"
name="search" name="search"
placeholder="Search..." placeholder="Search..."
hx-get="{{ search_endpoint }}" hx-get="{{ search_endpoint }}"
hx-trigger="input changed delay:300ms, search" hx-trigger="input changed delay:300ms, search"
hx-target="#plugins-list"> hx-target="#plugins-list">
@ -30,7 +30,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block warning %} {% block warning %}
<div id="warning-main"> <div id="warning-launch-required-main">
<img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt=""> <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> <span>Changes have been made to some plugins that will only take effect after running launch platform.</span>
</div> </div>

View File

@ -1,13 +1,13 @@
{% from '_switch.html' import switch %} {% from '_switch.html' import switch %}
{% for plugin in plugins %} {% 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="details">
<div class="name"><a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a></div> <div class="name"><a href="{{ url_for('plugin', name=plugin.name) }}">{{ plugin.name }}</a></div>
<div class="author">By {{ plugin.author }}</div> <div class="author">By {{ plugin.author }}</div>
<div class="description">{{ plugin.description|safe }}</div> <div class="description">{{ plugin.description|safe }}</div>
</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"> <img src="{{ url_for('static', filename='img/Featured icon.svg')}}" alt="" title="Run launch platform for changes to this plugin to take effect">
</div> </div>

View File

@ -22,17 +22,6 @@ View all your installed plugins in one place.
{% block scripts %} {% block scripts %}
<script> <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) { document.body.addEventListener('htmx:afterOnLoad', function(event) {
let toggleSwitches = document.querySelectorAll(".switch"); let toggleSwitches = document.querySelectorAll(".switch");
toggleSwitches.forEach(toggleSwitch => { toggleSwitches.forEach(toggleSwitch => {
@ -42,7 +31,6 @@ View all your installed plugins in one place.
event.stopPropagation(); event.stopPropagation();
} }
}); });
SetWarning();
}); });
</script> </script>
{% endblock %} {% endblock %}