Files
maptool/public/js/plugins.js
T
randy d9cc468478 Show the actual plugin run error instead of a generic message
A failed run showed "Failed — see server log" regardless of why —
misleading, since a Plugins::RunError never actually reaches any log
(app.rb turns it straight into a 502 JSON response). Found this the
hard way debugging the p4n2gpx plugin, having to SSH in and reproduce
the run by hand each time just to see why it failed. Now shows the
response's actual error message (e.g. "plugin exited with status 1"),
wrapping instead of overflowing the flyout for a longer one.
2026-09-09 18:17:24 +02:00

108 lines
4.3 KiB
JavaScript

import { PLUGIN_ICON } from "/js/icons.js";
import { notifyGpxStoreChanged } from "/js/panels.js";
// A sidebar flyout listing whatever import plugins are on disk (see
// lib/maptool/plugins.rb for the contract) — each one just a "Run for
// this area" button using the current map viewport as the bbox, so there's
// no separate area-picker to build: pan/zoom to where you want, click Run.
// Only ever mounted when logged in (see map.js) — same "hide the
// write-only affordance" story as NotesControl, since the only thing this
// panel does is a login-gated write.
export class PluginsControl {
constructor(map) {
this._map = map;
}
mount(sidebar) {
this._container = document.createElement("div");
this._container.className = "sidebar-item";
this._container.innerHTML = `
<button type="button" class="sidebar-icon" title="Import plugins" aria-label="Import plugins">${PLUGIN_ICON}</button>
<div class="sidebar-flyout" hidden>
<p class="plugins-panel__hint">Runs for whatever area is currently on screen.</p>
<ul class="plugins-panel__list"></ul>
</div>
`;
this._icon = this._container.querySelector(".sidebar-icon");
this._flyout = this._container.querySelector(".sidebar-flyout");
this._list = this._container.querySelector(".plugins-panel__list");
this._icon.addEventListener("click", () => {
const opening = this._flyout.hidden;
this._flyout.hidden = !opening;
this._icon.classList.toggle("active", opening);
if (opening) this._loadPlugins();
});
sidebar.appendChild(this._container);
}
async _loadPlugins() {
this._list.innerHTML = `<li class="plugins-panel__empty">Loading…</li>`;
let plugins;
try {
plugins = await (await fetch("/api/plugins")).json();
} catch (err) {
this._list.innerHTML = `<li class="plugins-panel__empty">Could not load plugins</li>`;
return;
}
if (plugins.length === 0) {
this._list.innerHTML = `<li class="plugins-panel__empty">No plugins installed — see docs/development.adoc</li>`;
return;
}
this._list.innerHTML = "";
plugins.forEach((plugin) => this._list.appendChild(this._renderItem(plugin)));
}
_renderItem(plugin) {
const li = document.createElement("li");
li.className = "plugins-panel__item";
li.innerHTML = `
<span class="plugins-panel__label"></span>
<button type="button" class="plugins-panel__run">Run for this area</button>
<span class="plugins-panel__status"></span>
`;
li.querySelector(".plugins-panel__label").textContent = plugin.label;
const button = li.querySelector(".plugins-panel__run");
const status = li.querySelector(".plugins-panel__status");
button.addEventListener("click", () => this._run(plugin, button, status));
return li;
}
async _run(plugin, button, status) {
button.disabled = true;
status.textContent = "Running…";
try {
const bounds = this._map.getBounds();
const body = new URLSearchParams({
south: bounds.getSouth(),
west: bounds.getWest(),
north: bounds.getNorth(),
east: bounds.getEast(),
});
const response = await fetch(`/api/plugins/${encodeURIComponent(plugin.name)}/run`, { method: "POST", body });
// Every app.rb response on this route is JSON, success or error —
// the catch below is only for an infra-level failure in front of it
// (a proxy's own HTML error page, a dropped connection) that never
// reached app.rb at all.
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(data.error || `run failed: ${response.status}`);
notifyGpxStoreChanged(data.path);
status.textContent = "Done";
status.classList.remove("plugins-panel__status--error");
} catch (err) {
// Plugins::RunError's message (e.g. "plugin exited with status 1")
// reaches here as `data.error` above — nothing about a plugin run
// failure gets logged server-side (app.rb turns it into a plain 502
// JSON response, not an exception), so showing that message directly
// is the only way to see why, short of SSHing in to reproduce it.
status.textContent = err.message;
status.classList.add("plugins-panel__status--error");
} finally {
button.disabled = false;
}
}
}