From 8357a87c1664c875fa4a5d7ce221f6f3eb78d15f Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Thu, 1 Oct 2026 11:20:02 -0600 Subject: [PATCH 1/2] K2GO-99 feat(addons): offline Code on the Go add-ons gallery Mirror the published add-ons gallery into the rootfs, serve it offline at /code-addons, and refresh it live via dash-node. - roles/code_addons (upstream-patches patch): mirror_addons.py reference-driven mirror (sha256-verified, Cloudflare stripped, catalog base rewritten) + nginx static serve; iiab-android enables it on medium/large tiers. - dash-node: /addons/refresh{,/status,/cancel} + tools/code-addons-refresh.sh (stage-and-swap, reuses mirror_addons.py); package.json 1.3.8 + CHANGELOG. - app: code_addons module (roster/cards/Home) + "Update add-ons" action (AddonsRefreshClient/AddonsRefresh); UI strings machine-localized to 33 locales. --- .../org/appdevforall/k2go/ModuleRegistry.java | 3 +- .../k2go/addons/data/AddonsRefreshClient.java | 163 ++++++ .../addons/presentation/AddonsRefresh.java | 147 +++++ .../k2go/redesign/LibraryHomeFragment.java | 1 + .../k2go/redesign/ModuleActionSheet.java | 8 + .../k2go/redesign/ModuleCards.java | 4 + .../k2go/redesign/ModuleDetailFragment.java | 10 + .../app/src/main/res/values-ar/strings.xml | 10 + .../app/src/main/res/values-az/strings.xml | 10 + .../app/src/main/res/values-bg/strings.xml | 10 + .../app/src/main/res/values-bn/strings.xml | 10 + .../app/src/main/res/values-cs/strings.xml | 10 + .../app/src/main/res/values-de/strings.xml | 10 + .../app/src/main/res/values-el/strings.xml | 10 + .../app/src/main/res/values-es/strings.xml | 10 + .../app/src/main/res/values-fa/strings.xml | 10 + .../app/src/main/res/values-fr/strings.xml | 10 + .../app/src/main/res/values-gu/strings.xml | 10 + .../app/src/main/res/values-hi/strings.xml | 10 + .../app/src/main/res/values-hu/strings.xml | 10 + .../app/src/main/res/values-in/strings.xml | 10 + .../app/src/main/res/values-it/strings.xml | 10 + .../app/src/main/res/values-ja/strings.xml | 10 + .../app/src/main/res/values-ko/strings.xml | 10 + .../app/src/main/res/values-lt/strings.xml | 10 + .../app/src/main/res/values-nl/strings.xml | 10 + .../app/src/main/res/values-no/strings.xml | 10 + .../app/src/main/res/values-pl/strings.xml | 10 + .../app/src/main/res/values-pt/strings.xml | 10 + .../app/src/main/res/values-ro/strings.xml | 10 + .../src/main/res/values-ru-rRU/strings.xml | 10 + .../app/src/main/res/values-sk/strings.xml | 10 + .../app/src/main/res/values-sr/strings.xml | 10 + .../app/src/main/res/values-sw/strings.xml | 10 + .../app/src/main/res/values-ta/strings.xml | 10 + .../app/src/main/res/values-tr/strings.xml | 10 + .../app/src/main/res/values-uk/strings.xml | 10 + .../app/src/main/res/values-vi/strings.xml | 10 + .../app/src/main/res/values-yo/strings.xml | 10 + .../src/main/res/values-zh-rCN/strings.xml | 10 + .../app/src/main/res/values/strings.xml | 13 + controller/docs/ADR-addons-offline-gallery.md | 150 +++++ iiab-android | 14 + static/dashboard/CHANGELOG.md | 1 + static/dashboard/package.json | 2 +- static/dashboard/routes.ts | 71 +++ tools/code-addons-refresh.sh | 73 +++ .../patches/0004-code-addons-add-role.patch | 538 ++++++++++++++++++ 48 files changed, 1526 insertions(+), 2 deletions(-) create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/addons/data/AddonsRefreshClient.java create mode 100644 controller/app/src/main/java/org/appdevforall/k2go/addons/presentation/AddonsRefresh.java create mode 100644 controller/docs/ADR-addons-offline-gallery.md create mode 100644 tools/code-addons-refresh.sh create mode 100644 tools/upstream-patches/patches/0004-code-addons-add-role.patch diff --git a/controller/app/src/main/java/org/appdevforall/k2go/ModuleRegistry.java b/controller/app/src/main/java/org/appdevforall/k2go/ModuleRegistry.java index 84606c55..cda420d3 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/ModuleRegistry.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/ModuleRegistry.java @@ -38,7 +38,8 @@ public IiabModule(String endpoint, int nameResId, boolean requires64Bit, String new IiabModule("kolibri", R.string.dash_kolibri, false, "kolibri"), // YAML uses kolibri_install new IiabModule("maps", R.string.dash_maps, false, "maps"), // YAML uses maps_install new IiabModule("matomo", R.string.dash_matomo, false, "matomo"), // YAML uses matomo_install - new IiabModule("forgejo", R.string.dash_forgejo, false, "forgejo") // YAML uses forgejo_install + new IiabModule("forgejo", R.string.dash_forgejo, false, "forgejo"), // YAML uses forgejo_install + new IiabModule("code-addons", R.string.dash_code_addons, false, "code_addons") // K2GO-99: offline Code on the Go add-ons gallery; YAML uses code_addons_install, served at /code-addons // ADFA-4842: "dashboard" is intentionally NOT in the roster. It is no longer an // application/module — it became the REST API core of K2Go; without it maps FQR and all // REST content downloads fail. It is part of the core system, not a user-installable module. diff --git a/controller/app/src/main/java/org/appdevforall/k2go/addons/data/AddonsRefreshClient.java b/controller/app/src/main/java/org/appdevforall/k2go/addons/data/AddonsRefreshClient.java new file mode 100644 index 00000000..5f94a655 --- /dev/null +++ b/controller/app/src/main/java/org/appdevforall/k2go/addons/data/AddonsRefreshClient.java @@ -0,0 +1,163 @@ +/* + * ============================================================================ + * Name : AddonsRefreshClient.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : K2GO-99. App-side client of the dash-node add-ons refresh + * (static/dashboard routes.ts). The box re-mirrors the Code on the + * Go add-ons gallery while the server is UP: the device only POSTs + * to start and polls a coarse status, so a dropped app never stops + * the refresh (the box job is detached). Same POST-then-poll shape + * as ForgejoSeedClient.refresh(); the status carries downloaded and + * failed counts instead of per-repo outcomes. + * + * Contract: + * POST /k2go-api/addons/refresh -> 202 { state:"running" } + * GET /k2go-api/addons/refresh/status -> { state, lines, downloaded, failed } + * POST /k2go-api/addons/refresh/cancel -> { state:"cancelled" } + * ============================================================================ + */ +package org.appdevforall.k2go.addons.data; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.appdevforall.k2go.config.BoxEndpoints; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * Drives the box add-ons refresh to a terminal state. {@link #refresh} BLOCKS (poll loop), so callers + * run it on an IO thread. The box job is detached, so a caller that dies mid-run does not stop it: a + * later {@link #refresh} re-attaches by reading the same status (a running refresh is left alone). + */ +public final class AddonsRefreshClient { + + /** Terminal verdict of a refresh. */ + public enum Result { DONE, ERROR, CANCELLED } + + /** Streamed status-tail lines, for a live one-line view. Optional (pass null to ignore). */ + public interface Listener { + void onLine(@NonNull String line); + } + + private static final String REFRESH_URL = BoxEndpoints.API + "/addons/refresh"; + private static final String REFRESH_STATUS_URL = BoxEndpoints.API + "/addons/refresh/status"; + private static final String REFRESH_CANCEL_URL = BoxEndpoints.API + "/addons/refresh/cancel"; + private static final long POLL_MS = 2000L; + private static final int MAX_POLL_ERRORS = 15; // ~30s of transient blips before giving up + // The mirror pulls the full set of .cgp files (hundreds of MB), so the run can take many minutes; + // cap the wait so a wedged box job cannot block forever (the box job keeps running detached). + private static final long MAX_WAIT_MS = 30 * 60 * 1000L; + + /** The last status-tail line handed to the listener, so a poll that did not advance stays quiet. */ + private String lastEmitted; + private int lastDownloaded = -1, lastFailed = -1; + + /** Files downloaded in the last refresh; -1 if the box did not report it. */ + public int lastDownloaded() { return lastDownloaded; } + /** Files the last refresh could not fetch or verify; -1 if the box did not report it. */ + public int lastFailed() { return lastFailed; } + + /** + * Start the refresh if it is not already running, then poll to a terminal state. Each refresh is + * intentional, so there is no "done" short-circuit: unless one is already running (re-attach), it + * POSTs a fresh run. The box refresh is safe to re-run (it mirrors into a staging dir and only + * swaps the live gallery on success). Returns DONE when the box refresh finished, ERROR on an + * unreachable box or timeout, CANCELLED if the user stopped it. + */ + @NonNull + public Result refresh(@Nullable Listener l) { + String state = readState(l); + if (!"running".equals(state)) { + if (!post(REFRESH_URL)) return Result.ERROR; + } + return poll(l); + } + + /** Ask the box to stop a running refresh (best-effort; a poll then reads "cancelled"). */ + public void cancel() { + post(REFRESH_CANCEL_URL); + } + + @NonNull + private Result poll(@Nullable Listener l) { + final long deadline = System.currentTimeMillis() + MAX_WAIT_MS; + int pollErrors = 0; + while (System.currentTimeMillis() < deadline) { + try { Thread.sleep(POLL_MS); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); return Result.ERROR; } + String s = readState(l); + if (s == null) { if (++pollErrors > MAX_POLL_ERRORS) return Result.ERROR; continue; } + pollErrors = 0; + if ("done".equals(s)) return Result.DONE; + if ("error".equals(s)) return Result.ERROR; + if ("cancelled".equals(s)) return Result.CANCELLED; + // "running" (or an unknown transient) -> keep polling. + } + return Result.ERROR; // timed out; the box job may still finish, a later refresh re-checks + } + + /** POST a start/cancel endpoint; true if accepted (2xx) or already running (409). */ + private boolean post(@NonNull String url) { + try { + HttpURLConnection c = open("POST", url); + int code = c.getResponseCode(); + c.disconnect(); + return (code >= 200 && code < 300) || code == 409; + } catch (Exception e) { + return false; + } + } + + /** GET the status endpoint -> the state string, streaming any new tail line; null on a read error. */ + @Nullable + private String readState(@Nullable Listener l) { + try { + HttpURLConnection c = open("GET", REFRESH_STATUS_URL); + int code = c.getResponseCode(); + boolean ok = code >= 200 && code < 400; + String text = readAll(ok ? c.getInputStream() : c.getErrorStream()); + c.disconnect(); + if (!ok) return null; + JSONObject j = new JSONObject(text.isEmpty() ? "{}" : text); + if (j.has("downloaded")) lastDownloaded = j.optInt("downloaded", lastDownloaded); + if (j.has("failed")) lastFailed = j.optInt("failed", lastFailed); + if (l != null) { + JSONArray lines = j.optJSONArray("lines"); + if (lines != null && lines.length() > 0) { + String last = lines.optString(lines.length() - 1, ""); + if (!last.isEmpty() && !last.equals(lastEmitted)) { lastEmitted = last; l.onLine(last); } + } + } + return j.optString("state", ""); + } catch (Exception e) { + return null; + } + } + + private static HttpURLConnection open(String method, String urlStr) throws Exception { + HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection(); + c.setUseCaches(false); + c.setConnectTimeout(4000); + c.setReadTimeout(4000); + c.setRequestMethod(method); + c.setRequestProperty("Accept", "application/json"); + return c; + } + + private static String readAll(InputStream is) throws Exception { + if (is == null) return ""; + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int n; + while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n); + is.close(); + return buf.toString(StandardCharsets.UTF_8.name()); + } +} diff --git a/controller/app/src/main/java/org/appdevforall/k2go/addons/presentation/AddonsRefresh.java b/controller/app/src/main/java/org/appdevforall/k2go/addons/presentation/AddonsRefresh.java new file mode 100644 index 00000000..ad880c2d --- /dev/null +++ b/controller/app/src/main/java/org/appdevforall/k2go/addons/presentation/AddonsRefresh.java @@ -0,0 +1,147 @@ +/* + * ============================================================================ + * Name : AddonsRefresh.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : K2GO-99. The single "Update add-ons" flow, shared by the module detail button and the + * module action sheet row so neither duplicates it. Modeled on ForgejoRepoRefresh: it + * gates like the dashboard update (needs internet, then metered consent), shows minimal + * inline progress (a description, a live one-line output tail, an indeterminate bar and a + * Cancel) injected right after the trigger view, runs the box refresh on an IO thread, and + * reports the outcome in a snackbar. + * + * Lifecycle: there is NO persistent app-side state. The box refresh job is detached + * (setsid), so a host that goes away mid-run just drops the UI updates (guarded by + * View.isAttachedToWindow()); the box finishes on its own and the gallery is swapped in + * only on success. The only state is the box's own status/pid files, which the box manages. + * ============================================================================ + */ +package org.appdevforall.k2go.addons.presentation; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.google.android.material.progressindicator.LinearProgressIndicator; + +import org.appdevforall.k2go.R; +import org.appdevforall.k2go.addons.data.AddonsRefreshClient; +import org.appdevforall.k2go.util.AppExecutors; +import org.appdevforall.k2go.util.Snackbars; + +public final class AddonsRefresh { + + private AddonsRefresh() {} + + /** + * Gate (internet, then metered consent) then run the refresh with progress injected right after + * {@code trigger}. The trigger stays in place (only disabled) as a visible anchor for the snackbar. + */ + public static void start(@NonNull Activity act, @NonNull View trigger) { + if (!org.appdevforall.k2go.networkpolicy.data.AndroidNetworkClassifier.hasInternet(act)) { + Snackbars.make(trigger, act.getString(R.string.k2go_dash_needs_internet)).show(); + return; + } + // Same metered gate as the dashboard update: the box downloads the gallery over the device network. + org.appdevforall.k2go.networkpolicy.presentation.NetworkPolicyGate.guardHeavyStart(act, () -> run(trigger)); + } + + private static void run(@NonNull View trigger) { + final ViewGroup parent = (ViewGroup) trigger.getParent(); + if (parent == null || !trigger.isAttachedToWindow()) return; // host went away during the gate + final Context ctx = trigger.getContext(); + final Handler main = new Handler(Looper.getMainLooper()); + final float d = ctx.getResources().getDisplayMetrics().density; + final int side = Math.round(20 * d); + + final LinearLayout progress = new LinearLayout(ctx); + progress.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams plp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + plp.leftMargin = side; plp.rightMargin = side; plp.topMargin = Math.round(8 * d); + progress.setLayoutParams(plp); + + final TextView label = new TextView(ctx); + label.setText(R.string.k2go_code_addons_updating); + label.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + label.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + progress.addView(label); + + final TextView liveLine = new TextView(ctx); + liveLine.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + liveLine.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_muted)); + liveLine.setMaxLines(1); + liveLine.setEllipsize(TextUtils.TruncateAt.END); + LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + llp.topMargin = Math.round(2 * d); + liveLine.setLayoutParams(llp); + progress.addView(liveLine); + + final LinearLayout barLine = new LinearLayout(ctx); + barLine.setOrientation(LinearLayout.HORIZONTAL); + barLine.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams barLineLp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + barLineLp.topMargin = Math.round(4 * d); + barLine.setLayoutParams(barLineLp); + + final LinearProgressIndicator bar = new LinearProgressIndicator(ctx); + bar.setIndeterminate(true); + LinearLayout.LayoutParams blp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f); + bar.setLayoutParams(blp); + barLine.addView(bar); + + final TextView cancel = new TextView(ctx); + cancel.setText(R.string.k2go_dash_cancel); + cancel.setAllCaps(true); + cancel.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelLarge); + cancel.setTextColor(ContextCompat.getColor(ctx, R.color.k2go_teal)); + int hp = Math.round(12 * d), vp = Math.round(6 * d); + cancel.setPadding(hp, vp, hp, vp); + barLine.addView(cancel); + progress.addView(barLine); + + parent.addView(progress, parent.indexOfChild(trigger) + 1); + trigger.setEnabled(false); // stays in place as an anchor; re-enabled when the refresh settles + + cancel.setOnClickListener(cv -> { + cancel.setEnabled(false); + label.setText(R.string.k2go_code_addons_update_cancelling); + AppExecutors.get().io().execute(() -> new AddonsRefreshClient().cancel()); + }); + + AppExecutors.get().io().execute(() -> { + final AddonsRefreshClient client = new AddonsRefreshClient(); + final AddonsRefreshClient.Result r = client.refresh(rawLine -> { + final String shown = rawLine.trim(); + main.post(() -> { if (liveLine.isAttachedToWindow()) liveLine.setText(shown); }); + }); + final int failed = client.lastFailed(); + main.post(() -> { + if (!trigger.isAttachedToWindow()) return; + parent.removeView(progress); + trigger.setEnabled(true); + Snackbars.make(trigger, ctx.getString(messageFor(r, failed))).show(); + }); + }); + } + + /** Map the refresh outcome to a user message covering every state. */ + private static int messageFor(AddonsRefreshClient.Result r, int failed) { + if (r == AddonsRefreshClient.Result.CANCELLED) return R.string.k2go_code_addons_update_cancelled; + if (r != AddonsRefreshClient.Result.DONE) return R.string.k2go_code_addons_update_failed; // box unreachable + if (failed > 0) return R.string.k2go_code_addons_update_some_failed; // some files could not be fetched + return R.string.k2go_code_addons_update_done; + } +} diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryHomeFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryHomeFragment.java index c4414a4b..b0d7c2c1 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryHomeFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/LibraryHomeFragment.java @@ -181,6 +181,7 @@ private void populateCards() { cards.clear(); cards.add(new Card("books", getString(R.string.k2go_card_books), false, R.drawable.ic_card_book)); cards.add(new Card("code", getString(R.string.k2go_card_code), false, R.drawable.ic_card_code)); + cards.add(new Card("code-addons", getString(R.string.k2go_card_code_addons), false, R.drawable.ic_card_code)); cards.add(new Card("forgejo", getString(R.string.k2go_card_forgejo), false, R.drawable.ic_card_forgejo)); cards.add(new Card("kiwix", getString(R.string.k2go_card_wikipedia), true, R.drawable.ic_card_wikipedia)); cards.add(new Card("kolibri", getString(R.string.k2go_card_courses), false, R.drawable.ic_card_courses)); diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleActionSheet.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleActionSheet.java index 59887d34..2e4e174b 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleActionSheet.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleActionSheet.java @@ -223,6 +223,14 @@ public static void show(Activity act, String endpoint, String title, int iconRes }); }); } + // K2GO-99: an installed add-ons gallery offers a live refresh here (the module action + // menu). No status gate (installed means there is a gallery); the shared flow gates + // internet, then runs the refresh IN the sheet (it does NOT dismiss). + if ("code_addons".equals(key)) { + content.addView(row(ctx, R.drawable.ic_refresh, + act.getString(R.string.k2go_code_addons_update), Emphasis.ACCENT, null, false, + v -> org.appdevforall.k2go.addons.presentation.AddonsRefresh.start(act, v))); + } break; case SCHEDULED: { content.addView(about); diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleCards.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleCards.java index 319afd8a..5900eb66 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleCards.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleCards.java @@ -72,6 +72,9 @@ public boolean runsOnThisRuntime() { new Card("code", R.string.k2go_mod_code_title, R.string.k2go_mod_code_title, R.string.k2go_mod_code_sub, R.string.k2go_mod_code_desc, R.drawable.ic_card_code, false), + new Card("code_addons", R.string.k2go_mod_code_addons_title, R.string.k2go_mod_code_addons_title, + R.string.k2go_mod_code_addons_sub, R.string.k2go_mod_code_addons_desc, + R.drawable.ic_card_code, false), // K2GO-99: offline add-ons gallery (reuses the code icon) new Card("forgejo", R.string.k2go_mod_forgejo_title, R.string.k2go_mod_forgejo_title, R.string.k2go_mod_forgejo_sub, R.string.k2go_mod_forgejo_desc, R.drawable.ic_card_forgejo, false), @@ -133,6 +136,7 @@ public static String license(String key) { public static int sizeLabelRes(String key) { if ("maps".equals(key)) return R.string.k2go_mod_maps_size; if ("matomo".equals(key)) return R.string.k2go_mod_matomo_size; // ADFA-4958: curated (~114 MB, incl. MariaDB + tarball) + if ("code_addons".equals(key)) return R.string.k2go_mod_code_addons_size; // K2GO-99: curated (~600 MB of .cgp; grows with the add-on set) return 0; } diff --git a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleDetailFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleDetailFragment.java index 4389aba7..e385f769 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleDetailFragment.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleDetailFragment.java @@ -112,6 +112,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c final com.google.android.material.checkbox.MaterialCheckBox forgejoRepos = root.findViewById(R.id.k2go_moddet_forgejo_repos); final boolean isForgejo = "forgejo".equals(c.key()); + final boolean isCodeAddons = "code_addons".equals(c.key()); // K2GO-417: the repos opt-in is an INSTALL-TIME choice, so it is shown ONLY in the installable // branch below (default-checked in the layout). Once the module is installed it stays GONE: // toggling it would do nothing (roles are not reinstalled from here, and unchecking cannot remove @@ -197,6 +198,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c }); }); } + if (isCodeAddons) { + // K2GO-99: an installed add-ons gallery offers a live refresh (re-mirror the + // published gallery). Minimal inline progress via the shared flow; no status gate + // (unlike Forgejo there is no sub-state: installed means there is a gallery). + installNowBtn.setText(R.string.k2go_code_addons_update); + installNowBtn.setOnClickListener(v -> + org.appdevforall.k2go.addons.presentation.AddonsRefresh.start(requireActivity(), installNowBtn)); + installNowBtn.setVisibility(View.VISIBLE); + } return; // a module cannot be uninstalled or reinstalled here (repos action aside) } // ADFA-4898: this module's runrole failed in the last finished batch — the SAME per-module diff --git a/controller/app/src/main/res/values-ar/strings.xml b/controller/app/src/main/res/values-ar/strings.xml index b4ef83e5..7cfcc7aa 100644 --- a/controller/app/src/main/res/values-ar/strings.xml +++ b/controller/app/src/main/res/values-ar/strings.xml @@ -1375,4 +1375,14 @@ يتوفر تحديث لـ dashboard يُوصى بتحديث dashboard أولاً حتى تعمل ميزات الوحدة الجديدة بشكل صحيح. التثبيت على أي حال + إضافات وقوالب ومقتطفات للمحرر + معرض غير متصل بالإنترنت للإضافات الخاصة بـ Code on the Go: الإضافات والقوالب والمقتطفات وإجراءات التعليمات البرمجية. تصفّحها وصفّها ونزّلها دون اتصال بالإنترنت. + الإضافات + تحديث الإضافات + جارٍ تحديث الإضافات... + جارٍ الإلغاء... + تم إلغاء تحديث الإضافات + تم تحديث الإضافات + تعذّر تحديث الإضافات + تعذّر تحديث بعض الإضافات diff --git a/controller/app/src/main/res/values-az/strings.xml b/controller/app/src/main/res/values-az/strings.xml index 8be25bf2..576078e8 100644 --- a/controller/app/src/main/res/values-az/strings.xml +++ b/controller/app/src/main/res/values-az/strings.xml @@ -1395,4 +1395,14 @@ Dashboard yenilemesi movcuddur Yeni modul funksiyalarinin duzgun islemesi ucun evvelce dashboard-u yenilemek tovsiye olunur. Yene de qurasdir + Redaktor üçün plaginlər, şablonlar və kod parçaları + Code on the Go üçün oflayn əlavələr qalereyası: plaginlər, şablonlar, kod parçaları və kod əməliyyatları. İnternet bağlantısı olmadan nəzər salın, süzün və endirin. + Əlavələr + Əlavələri yenilə + Əlavələr yenilənir... + Ləğv edilir... + Əlavələrin yenilənməsi ləğv edildi + Əlavələr yeniləndi + Əlavələri yeniləmək mümkün olmadı + Bəzi əlavələri yeniləmək mümkün olmadı diff --git a/controller/app/src/main/res/values-bg/strings.xml b/controller/app/src/main/res/values-bg/strings.xml index 2a523056..a220548a 100644 --- a/controller/app/src/main/res/values-bg/strings.xml +++ b/controller/app/src/main/res/values-bg/strings.xml @@ -1382,4 +1382,14 @@ Налична е актуализация на dashboard Препоръчва се първо да обновите dashboard, за да работят правилно новите функции на модула. Инсталирай въпреки това + Добавки, шаблони и фрагменти за редактора + Офлайн галерия с добавки за Code on the Go: добавки, шаблони, фрагменти и действия с код. Разглеждайте, филтрирайте и изтегляйте без интернет връзка. + Добавки + Актуализиране на добавките + Актуализиране на добавките... + Отказване... + Актуализирането на добавките е отменено + Добавките са актуализирани + Добавките не могат да бъдат актуализирани + Някои добавки не можаха да бъдат актуализирани diff --git a/controller/app/src/main/res/values-bn/strings.xml b/controller/app/src/main/res/values-bn/strings.xml index 140e136e..3cce6b29 100644 --- a/controller/app/src/main/res/values-bn/strings.xml +++ b/controller/app/src/main/res/values-bn/strings.xml @@ -1388,4 +1388,14 @@ dashboard আপডেট উপলব্ধ মডিউলের নতুন বৈশিষ্ট্য ঠিকমতো কাজ করার জন্য প্রথমে dashboard আপডেট করার পরামর্শ দেওয়া হয়। তবুও ইনস্টল করুন + সম্পাদকের জন্য প্লাগইন, টেমপ্লেট এবং স্নিপেট + Code on the Go-এর জন্য অ্যাড-অনের একটি অফলাইন গ্যালারি: প্লাগইন, টেমপ্লেট, স্নিপেট এবং কোড অ্যাকশন। ইন্টারনেট সংযোগ ছাড়াই ব্রাউজ, ফিল্টার এবং ডাউনলোড করুন। + অ্যাড-অন + অ্যাড-অন আপডেট করুন + অ্যাড-অন আপডেট হচ্ছে... + বাতিল করা হচ্ছে... + অ্যাড-অন আপডেট বাতিল করা হয়েছে + অ্যাড-অন আপডেট করা হয়েছে + অ্যাড-অন আপডেট করা যায়নি + কিছু অ্যাড-অন আপডেট করা যায়নি diff --git a/controller/app/src/main/res/values-cs/strings.xml b/controller/app/src/main/res/values-cs/strings.xml index 019882aa..33bd2355 100644 --- a/controller/app/src/main/res/values-cs/strings.xml +++ b/controller/app/src/main/res/values-cs/strings.xml @@ -1382,4 +1382,14 @@ Je dostupna aktualizace dashboardu Doporucujeme nejprve aktualizovat dashboard, aby nove funkce modulu fungovaly spravne. Presto nainstalovat + Pluginy, šablony a úryvky pro editor + Offline galerie doplňků pro Code on the Go: pluginy, šablony, úryvky a akce kódu. Procházejte, filtrujte a stahujte je bez připojení k internetu. + Doplňky + Aktualizovat doplňky + Aktualizace doplňků... + Rušení... + Aktualizace doplňků zrušena + Doplňky aktualizovány + Doplňky se nepodařilo aktualizovat + Některé doplňky se nepodařilo aktualizovat diff --git a/controller/app/src/main/res/values-de/strings.xml b/controller/app/src/main/res/values-de/strings.xml index ab0463d5..1cd62eba 100644 --- a/controller/app/src/main/res/values-de/strings.xml +++ b/controller/app/src/main/res/values-de/strings.xml @@ -1375,4 +1375,14 @@ Ein Dashboard-Update ist verfuegbar Es wird empfohlen, zuerst das Dashboard zu aktualisieren, damit neue Modulfunktionen richtig laufen. Trotzdem installieren + Plugins, Vorlagen und Snippets für den Editor + Eine Offline-Galerie mit Add-ons für Code on the Go: Plugins, Vorlagen, Snippets und Code-Aktionen. Durchsuchen, filtern und ohne Internetverbindung herunterladen. + Add-ons + Add-ons aktualisieren + Add-ons werden aktualisiert... + Wird abgebrochen... + Add-on-Aktualisierung abgebrochen + Add-ons aktualisiert + Add-ons konnten nicht aktualisiert werden + Einige Add-ons konnten nicht aktualisiert werden diff --git a/controller/app/src/main/res/values-el/strings.xml b/controller/app/src/main/res/values-el/strings.xml index abf4a288..2945b31c 100644 --- a/controller/app/src/main/res/values-el/strings.xml +++ b/controller/app/src/main/res/values-el/strings.xml @@ -1382,4 +1382,14 @@ Διαθέσιμη ενημέρωση dashboard Συνιστάται να ενημερώσετε πρώτα το dashboard ώστε οι νέες λειτουργίες της μονάδας να λειτουργούν σωστά. Εγκατάσταση ούτως ή άλλως + Πρόσθετα, πρότυπα και αποσπάσματα για τον επεξεργαστή + Μια συλλογή προσθέτων εκτός σύνδεσης για το Code on the Go: πρόσθετα, πρότυπα, αποσπάσματα και ενέργειες κώδικα. Περιηγηθείτε, φιλτράρετε και κατεβάστε τα χωρίς σύνδεση στο διαδίκτυο. + Πρόσθετα + Ενημέρωση προσθέτων + Ενημέρωση προσθέτων... + Ακύρωση... + Η ενημέρωση προσθέτων ακυρώθηκε + Τα πρόσθετα ενημερώθηκαν + Δεν ήταν δυνατή η ενημέρωση των προσθέτων + Ορισμένα πρόσθετα δεν ήταν δυνατό να ενημερωθούν diff --git a/controller/app/src/main/res/values-es/strings.xml b/controller/app/src/main/res/values-es/strings.xml index 1bc2d834..1f80d2f1 100644 --- a/controller/app/src/main/res/values-es/strings.xml +++ b/controller/app/src/main/res/values-es/strings.xml @@ -1451,4 +1451,14 @@ Hay una actualizacion del dashboard disponible Se recomienda actualizar primero el dashboard para que las nuevas funciones del modulo funcionen bien. Instalar de todos modos + Plugins, plantillas y fragmentos para el editor + Una galería sin conexión de complementos para Code on the Go: plugins, plantillas, fragmentos y acciones de código. Explóralos, fíltralos y descárgalos sin conexión a internet. + Complementos + Actualizar complementos + Actualizando complementos... + Cancelando... + Actualización de complementos cancelada + Complementos actualizados + No se pudieron actualizar los complementos + Algunos complementos no se pudieron actualizar diff --git a/controller/app/src/main/res/values-fa/strings.xml b/controller/app/src/main/res/values-fa/strings.xml index ff487636..4cdb967c 100644 --- a/controller/app/src/main/res/values-fa/strings.xml +++ b/controller/app/src/main/res/values-fa/strings.xml @@ -1375,4 +1375,14 @@ به‌روزرسانی dashboard در دسترس است توصیه می‌شود ابتدا dashboard را به‌روزرسانی کنید تا قابلیت‌های جدید ماژول درست کار کنند. به هر حال نصب شود + افزونه‌ها، قالب‌ها و قطعه‌کدها برای ویرایشگر + یک گالری آفلاین از افزونه‌ها برای Code on the Go: افزونه‌ها، قالب‌ها، قطعه‌کدها و کنش‌های کد. بدون اتصال به اینترنت آن‌ها را مرور، فیلتر و دانلود کنید. + افزونه‌ها + به‌روزرسانی افزونه‌ها + در حال به‌روزرسانی افزونه‌ها... + در حال لغو... + به‌روزرسانی افزونه‌ها لغو شد + افزونه‌ها به‌روزرسانی شدند + به‌روزرسانی افزونه‌ها ممکن نشد + برخی افزونه‌ها به‌روزرسانی نشدند diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml index 9cc3ff59..184129e5 100644 --- a/controller/app/src/main/res/values-fr/strings.xml +++ b/controller/app/src/main/res/values-fr/strings.xml @@ -1462,4 +1462,14 @@ Une mise a jour du dashboard est disponible Il est recommande de mettre a jour le dashboard d\'abord pour que les nouvelles fonctions du module marchent bien. Installer quand meme + Plugins, modèles et extraits pour l\'éditeur + Une galerie hors ligne de modules complémentaires pour Code on the Go : plugins, modèles, extraits et actions de code. Parcourez-les, filtrez-les et téléchargez-les sans connexion internet. + Modules + Mettre à jour les modules + Mise à jour des modules... + Annulation... + Mise à jour des modules annulée + Modules mis à jour + Impossible de mettre à jour les modules + Certains modules n\'ont pas pu être mis à jour diff --git a/controller/app/src/main/res/values-gu/strings.xml b/controller/app/src/main/res/values-gu/strings.xml index 36783002..c2bba2be 100644 --- a/controller/app/src/main/res/values-gu/strings.xml +++ b/controller/app/src/main/res/values-gu/strings.xml @@ -1388,4 +1388,14 @@ dashboard અપડેટ ઉપલબ્ધ છે મૉડ્યૂલની નવી સુવિધાઓ યોગ્ય રીતે કામ કરે તે માટે પહેલાં dashboard અપડેટ કરવાની ભલામણ છે. તોપણ ઇન્સ્ટૉલ કરો + એડિટર માટે પ્લગઇન, ટેમ્પ્લેટ અને સ્નિપેટ + Code on the Go માટે એડ-ઓનની ઑફલાઇન ગેલેરી: પ્લગઇન, ટેમ્પ્લેટ, સ્નિપેટ અને કોડ ક્રિયાઓ. ઇન્ટરનેટ કનેક્શન વિના બ્રાઉઝ કરો, ફિલ્ટર કરો અને ડાઉનલોડ કરો. + એડ-ઓન + એડ-ઓન અપડેટ કરો + એડ-ઓન અપડેટ થઈ રહ્યા છે... + રદ કરી રહ્યું છે... + એડ-ઓન અપડેટ રદ કરાયું + એડ-ઓન અપડેટ થયા + એડ-ઓન અપડેટ કરી શકાયા નહીં + કેટલાક એડ-ઓન અપડેટ કરી શકાયા નહીં diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml index ea2fbd22..3d2dde69 100644 --- a/controller/app/src/main/res/values-hi/strings.xml +++ b/controller/app/src/main/res/values-hi/strings.xml @@ -1452,4 +1452,14 @@ dashboard अपडेट उपलब्ध है अनुशंसित है कि पहले dashboard अपडेट करें ताकि मॉड्यूल की नई सुविधाएँ सही से काम करें। फिर भी इंस्टॉल करें + संपादक के लिए प्लगइन, टेम्पलेट और स्निपेट + Code on the Go के लिए ऐड-ऑन की ऑफ़लाइन गैलरी: प्लगइन, टेम्पलेट, स्निपेट और कोड क्रियाएँ। बिना इंटरनेट कनेक्शन के ब्राउज़ करें, फ़िल्टर करें और डाउनलोड करें। + ऐड-ऑन + ऐड-ऑन अपडेट करें + ऐड-ऑन अपडेट हो रहे हैं... + रद्द किया जा रहा है... + ऐड-ऑन अपडेट रद्द किया गया + ऐड-ऑन अपडेट किए गए + ऐड-ऑन अपडेट नहीं हो सके + कुछ ऐड-ऑन अपडेट नहीं हो सके diff --git a/controller/app/src/main/res/values-hu/strings.xml b/controller/app/src/main/res/values-hu/strings.xml index f0297ed2..88dd3736 100644 --- a/controller/app/src/main/res/values-hu/strings.xml +++ b/controller/app/src/main/res/values-hu/strings.xml @@ -1375,4 +1375,14 @@ Elerheto a dashboard frissitese Ajanlott eloszor frissiteni a dashboardot, hogy az uj modulfunkciok megfeleloen mukodjenek. Telepites mindenkepp + Beépülők, sablonok és kódrészletek a szerkesztőhöz + Kiegészítők offline galériája a Code on the Go-hoz: beépülők, sablonok, kódrészletek és kódműveletek. Böngéssze, szűrje és töltse le őket internetkapcsolat nélkül. + Kiegészítők + Kiegészítők frissítése + Kiegészítők frissítése... + Megszakítás... + Kiegészítők frissítése megszakítva + Kiegészítők frissítve + A kiegészítőket nem sikerült frissíteni + Néhány kiegészítőt nem sikerült frissíteni diff --git a/controller/app/src/main/res/values-in/strings.xml b/controller/app/src/main/res/values-in/strings.xml index b0f71c5a..77116249 100644 --- a/controller/app/src/main/res/values-in/strings.xml +++ b/controller/app/src/main/res/values-in/strings.xml @@ -1382,4 +1382,14 @@ Pembaruan dashboard tersedia Disarankan memperbarui dashboard dulu agar fitur baru modul berfungsi dengan benar. Tetap pasang + Plugin, templat, dan cuplikan untuk editor + Galeri offline add-on untuk Code on the Go: plugin, templat, cuplikan, dan tindakan kode. Jelajahi, filter, dan unduh tanpa koneksi internet. + Add-on + Perbarui add-on + Memperbarui add-on... + Membatalkan... + Pembaruan add-on dibatalkan + Add-on diperbarui + Tidak dapat memperbarui add-on + Beberapa add-on tidak dapat diperbarui diff --git a/controller/app/src/main/res/values-it/strings.xml b/controller/app/src/main/res/values-it/strings.xml index 11991c6e..80236612 100644 --- a/controller/app/src/main/res/values-it/strings.xml +++ b/controller/app/src/main/res/values-it/strings.xml @@ -1375,4 +1375,14 @@ E disponibile un aggiornamento del dashboard Si consiglia di aggiornare prima il dashboard affinche le nuove funzioni del modulo funzionino bene. Installa comunque + Plugin, modelli e snippet per l\'editor + Una galleria offline di componenti aggiuntivi per Code on the Go: plugin, modelli, snippet e azioni di codice. Sfogliali, filtrali e scaricali senza connessione internet. + Componenti aggiuntivi + Aggiorna componenti aggiuntivi + Aggiornamento dei componenti aggiuntivi... + Annullamento... + Aggiornamento dei componenti aggiuntivi annullato + Componenti aggiuntivi aggiornati + Impossibile aggiornare i componenti aggiuntivi + Alcuni componenti aggiuntivi non sono stati aggiornati diff --git a/controller/app/src/main/res/values-ja/strings.xml b/controller/app/src/main/res/values-ja/strings.xml index 0ba9ccf6..faaa547f 100644 --- a/controller/app/src/main/res/values-ja/strings.xml +++ b/controller/app/src/main/res/values-ja/strings.xml @@ -1376,4 +1376,14 @@ dashboard の更新があります モジュールの新機能が正しく動作するよう、先に dashboard を更新することをおすすめします。 それでもインストール + エディター用のプラグイン、テンプレート、スニペット + Code on the Go 用のアドオンのオフラインギャラリー:プラグイン、テンプレート、スニペット、コードアクション。インターネット接続なしで閲覧、絞り込み、ダウンロードできます。 + アドオン + アドオンを更新 + アドオンを更新しています... + キャンセルしています... + アドオンの更新をキャンセルしました + アドオンを更新しました + アドオンを更新できませんでした + 一部のアドオンを更新できませんでした diff --git a/controller/app/src/main/res/values-ko/strings.xml b/controller/app/src/main/res/values-ko/strings.xml index 709600e0..1412bfbf 100644 --- a/controller/app/src/main/res/values-ko/strings.xml +++ b/controller/app/src/main/res/values-ko/strings.xml @@ -1376,4 +1376,14 @@ dashboard 업데이트가 있습니다 모듈의 새 기능이 제대로 작동하도록 먼저 dashboard를 업데이트하는 것이 좋습니다. 무시하고 설치 + 편집기용 플러그인, 템플릿, 스니펫 + Code on the Go용 애드온의 오프라인 갤러리: 플러그인, 템플릿, 스니펫, 코드 작업. 인터넷 연결 없이 찾아보고 필터링하고 다운로드하세요. + 애드온 + 애드온 업데이트 + 애드온 업데이트 중... + 취소 중... + 애드온 업데이트가 취소됨 + 애드온이 업데이트됨 + 애드온을 업데이트할 수 없음 + 일부 애드온을 업데이트하지 못함 diff --git a/controller/app/src/main/res/values-lt/strings.xml b/controller/app/src/main/res/values-lt/strings.xml index 33be7507..77b83424 100644 --- a/controller/app/src/main/res/values-lt/strings.xml +++ b/controller/app/src/main/res/values-lt/strings.xml @@ -1392,4 +1392,14 @@ Yra dashboard atnaujinimas Rekomenduojama pirma atnaujinti dashboard, kad naujos modulio funkcijos veiktu tinkamai. Vis tiek idiegti + Papildiniai, šablonai ir fragmentai redaktoriui + Neprisijungus veikianti Code on the Go priedų galerija: papildiniai, šablonai, fragmentai ir kodo veiksmai. Naršykite, filtruokite ir atsisiųskite juos be interneto ryšio. + Priedai + Atnaujinti priedus + Atnaujinami priedai... + Atšaukiama... + Priedų atnaujinimas atšauktas + Priedai atnaujinti + Nepavyko atnaujinti priedų + Kai kurių priedų nepavyko atnaujinti diff --git a/controller/app/src/main/res/values-nl/strings.xml b/controller/app/src/main/res/values-nl/strings.xml index a29b141f..397b1e8e 100644 --- a/controller/app/src/main/res/values-nl/strings.xml +++ b/controller/app/src/main/res/values-nl/strings.xml @@ -1375,4 +1375,14 @@ Er is een dashboard-update beschikbaar Het wordt aanbevolen eerst het dashboard bij te werken zodat nieuwe modulefuncties goed werken. Toch installeren + Plug-ins, sjablonen en snippets voor de editor + Een offline galerij met add-ons voor Code on the Go: plug-ins, sjablonen, snippets en code-acties. Blader, filter en download ze zonder internetverbinding. + Add-ons + Add-ons bijwerken + Add-ons bijwerken... + Annuleren... + Bijwerken van add-ons geannuleerd + Add-ons bijgewerkt + Kan de add-ons niet bijwerken + Sommige add-ons konden niet worden bijgewerkt diff --git a/controller/app/src/main/res/values-no/strings.xml b/controller/app/src/main/res/values-no/strings.xml index e25e271b..058d0c76 100644 --- a/controller/app/src/main/res/values-no/strings.xml +++ b/controller/app/src/main/res/values-no/strings.xml @@ -1385,4 +1385,14 @@ En dashboard-oppdatering er tilgjengelig Det anbefales a oppdatere dashboard forst slik at nye modulfunksjoner fungerer riktig. Installer likevel + Programtillegg, maler og utdrag for redigereren + Et frakoblet galleri med tillegg for Code on the Go: programtillegg, maler, utdrag og kodehandlinger. Bla gjennom, filtrer og last dem ned uten internettforbindelse. + Tillegg + Oppdater tillegg + Oppdaterer tillegg... + Avbryter... + Oppdatering av tillegg avbrutt + Tillegg oppdatert + Kunne ikke oppdatere tilleggene + Noen tillegg kunne ikke oppdateres diff --git a/controller/app/src/main/res/values-pl/strings.xml b/controller/app/src/main/res/values-pl/strings.xml index 75787a63..3a544ad6 100644 --- a/controller/app/src/main/res/values-pl/strings.xml +++ b/controller/app/src/main/res/values-pl/strings.xml @@ -1385,4 +1385,14 @@ Dostepna jest aktualizacja dashboardu Zalecane jest najpierw zaktualizowanie dashboardu, aby nowe funkcje modulu dzialaly poprawnie. Zainstaluj mimo to + Wtyczki, szablony i fragmenty dla edytora + Galeria offline dodatków do Code on the Go: wtyczki, szablony, fragmenty i akcje kodu. Przeglądaj, filtruj i pobieraj je bez połączenia z internetem. + Dodatki + Zaktualizuj dodatki + Aktualizowanie dodatków... + Anulowanie... + Anulowano aktualizację dodatków + Zaktualizowano dodatki + Nie można zaktualizować dodatków + Niektórych dodatków nie udało się zaktualizować diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml index 770b3c8d..b6d847c7 100644 --- a/controller/app/src/main/res/values-pt/strings.xml +++ b/controller/app/src/main/res/values-pt/strings.xml @@ -1454,4 +1454,14 @@ Ha uma atualizacao do dashboard disponivel Recomenda-se atualizar o dashboard primeiro para que os novos recursos do modulo funcionem corretamente. Instalar mesmo assim + Plugins, modelos e trechos para o editor + Uma galeria offline de complementos para o Code on the Go: plugins, modelos, trechos e ações de código. Navegue, filtre e baixe sem conexão com a internet. + Complementos + Atualizar complementos + Atualizando complementos... + Cancelando... + Atualização de complementos cancelada + Complementos atualizados + Não foi possível atualizar os complementos + Alguns complementos não puderam ser atualizados diff --git a/controller/app/src/main/res/values-ro/strings.xml b/controller/app/src/main/res/values-ro/strings.xml index 13cda9f7..b2e6959d 100644 --- a/controller/app/src/main/res/values-ro/strings.xml +++ b/controller/app/src/main/res/values-ro/strings.xml @@ -1375,4 +1375,14 @@ Este disponibila o actualizare a dashboardului Se recomanda actualizarea dashboardului mai intai, pentru ca noile functii ale modulului sa functioneze corect. Instaleaza oricum + Pluginuri, șabloane și fragmente pentru editor + O galerie offline de suplimente pentru Code on the Go: pluginuri, șabloane, fragmente și acțiuni de cod. Răsfoiește-le, filtrează-le și descarcă-le fără conexiune la internet. + Suplimente + Actualizează suplimentele + Se actualizează suplimentele... + Se anulează... + Actualizarea suplimentelor a fost anulată + Suplimente actualizate + Suplimentele nu au putut fi actualizate + Unele suplimente nu au putut fi actualizate diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml index 5ece3ef1..1c1a2b82 100644 --- a/controller/app/src/main/res/values-ru-rRU/strings.xml +++ b/controller/app/src/main/res/values-ru-rRU/strings.xml @@ -1451,4 +1451,14 @@ Доступно обновление dashboard Рекомендуется сначала обновить dashboard, чтобы новые функции модуля работали правильно. Всё равно установить + Плагины, шаблоны и фрагменты для редактора + Офлайн-галерея дополнений для Code on the Go: плагины, шаблоны, фрагменты и действия с кодом. Просматривайте, фильтруйте и загружайте их без подключения к интернету. + Дополнения + Обновить дополнения + Обновление дополнений... + Отмена... + Обновление дополнений отменено + Дополнения обновлены + Не удалось обновить дополнения + Некоторые дополнения не удалось обновить diff --git a/controller/app/src/main/res/values-sk/strings.xml b/controller/app/src/main/res/values-sk/strings.xml index ab4af362..0c7108fc 100644 --- a/controller/app/src/main/res/values-sk/strings.xml +++ b/controller/app/src/main/res/values-sk/strings.xml @@ -1382,4 +1382,14 @@ Je dostupna aktualizacia dashboardu Odporucame najprv aktualizovat dashboard, aby nove funkcie modulu fungovali spravne. Aj tak nainstalovat + Doplnky, šablóny a úryvky pre editor + Offline galéria doplnkov pre Code on the Go: doplnky, šablóny, úryvky a akcie kódu. Prehliadajte, filtrujte a sťahujte ich bez pripojenia na internet. + Doplnky + Aktualizovať doplnky + Aktualizácia doplnkov... + Ruší sa... + Aktualizácia doplnkov zrušená + Doplnky aktualizované + Doplnky sa nepodarilo aktualizovať + Niektoré doplnky sa nepodarilo aktualizovať diff --git a/controller/app/src/main/res/values-sr/strings.xml b/controller/app/src/main/res/values-sr/strings.xml index c9e43fa7..7cd9bf11 100644 --- a/controller/app/src/main/res/values-sr/strings.xml +++ b/controller/app/src/main/res/values-sr/strings.xml @@ -1388,4 +1388,14 @@ Доступно је ажурирање dashboard-а Препоручује се да прво ажурирате dashboard како би нове функције модула радиле исправно. Ипак инсталирај + Додаци, шаблони и исечци за уређивач + Офлајн галерија додатака за Code on the Go: додаци, шаблони, исечци и радње над кодом. Прегледајте, филтрирајте и преузимајте без интернет везе. + Додаци + Ажурирај додатке + Ажурирање додатака... + Отказивање... + Ажурирање додатака је отказано + Додаци су ажурирани + Није могуће ажурирати додатке + Неки додаци нису могли бити ажурирани diff --git a/controller/app/src/main/res/values-sw/strings.xml b/controller/app/src/main/res/values-sw/strings.xml index c19a1f7b..fa01f31c 100644 --- a/controller/app/src/main/res/values-sw/strings.xml +++ b/controller/app/src/main/res/values-sw/strings.xml @@ -1395,4 +1395,14 @@ Sasisho la dashboard linapatikana Inashauriwa kusasisha dashboard kwanza ili vipengele vipya vya moduli vifanye kazi vizuri. Sakinisha hata hivyo + Programu-jalizi, violezo na vijisehemu kwa kihariri + Ghala la nje ya mtandao la viongezi vya Code on the Go: programu-jalizi, violezo, vijisehemu na vitendo vya msimbo. Vinjari, chuja na upakue bila muunganisho wa intaneti. + Viongezi + Sasisha viongezi + Inasasisha viongezi... + Inaghairi... + Usasishaji wa viongezi umeghairiwa + Viongezi vimesasishwa + Imeshindwa kusasisha viongezi + Baadhi ya viongezi havikuweza kusasishwa diff --git a/controller/app/src/main/res/values-ta/strings.xml b/controller/app/src/main/res/values-ta/strings.xml index 6c5c0fbd..91c8f08c 100644 --- a/controller/app/src/main/res/values-ta/strings.xml +++ b/controller/app/src/main/res/values-ta/strings.xml @@ -1395,4 +1395,14 @@ dashboard புதுப்பிப்பு உள்ளது தொகுதியின் புதிய அம்சங்கள் சரியாக இயங்க முதலில் dashboard ஐப் புதுப்பிக்க பரிந்துரைக்கப்படுகிறது. இருப்பினும் நிறுவு + எடிட்டருக்கான செருகுநிரல்கள், டெம்ப்ளேட்டுகள் மற்றும் துணுக்குகள் + Code on the Go-க்கான ஆஃப்லைன் ஆட்-ஆன்கள் தொகுப்பு: செருகுநிரல்கள், டெம்ப்ளேட்டுகள், துணுக்குகள் மற்றும் குறியீட்டுச் செயல்கள். இணைய இணைப்பு இல்லாமல் உலாவவும், வடிகட்டவும், பதிவிறக்கவும். + ஆட்-ஆன்கள் + ஆட்-ஆன்களைப் புதுப்பிக்கவும் + ஆட்-ஆன்கள் புதுப்பிக்கப்படுகின்றன... + ரத்து செய்யப்படுகிறது... + ஆட்-ஆன் புதுப்பிப்பு ரத்து செய்யப்பட்டது + ஆட்-ஆன்கள் புதுப்பிக்கப்பட்டன + ஆட்-ஆன்களைப் புதுப்பிக்க முடியவில்லை + சில ஆட்-ஆன்களைப் புதுப்பிக்க முடியவில்லை diff --git a/controller/app/src/main/res/values-tr/strings.xml b/controller/app/src/main/res/values-tr/strings.xml index 59e704ae..4a027207 100644 --- a/controller/app/src/main/res/values-tr/strings.xml +++ b/controller/app/src/main/res/values-tr/strings.xml @@ -1375,4 +1375,14 @@ Bir dashboard guncellemesi mevcut Yeni modul ozelliklerinin duzgun calismasi icin once dashboard\'u guncellemeniz onerilir. Yine de yukle + Düzenleyici için eklentiler, şablonlar ve kod parçacıkları + Code on the Go için çevrimdışı eklenti galerisi: eklentiler, şablonlar, kod parçacıkları ve kod eylemleri. İnternet bağlantısı olmadan göz atın, filtreleyin ve indirin. + Eklentiler + Eklentileri güncelle + Eklentiler güncelleniyor... + İptal ediliyor... + Eklenti güncellemesi iptal edildi + Eklentiler güncellendi + Eklentiler güncellenemedi + Bazı eklentiler güncellenemedi diff --git a/controller/app/src/main/res/values-uk/strings.xml b/controller/app/src/main/res/values-uk/strings.xml index 975cd750..885bede9 100644 --- a/controller/app/src/main/res/values-uk/strings.xml +++ b/controller/app/src/main/res/values-uk/strings.xml @@ -1375,4 +1375,14 @@ Доступне оновлення dashboard Рекомендовано спочатку оновити dashboard, щоб нові функції модуля працювали правильно. Усе одно встановити + Плагіни, шаблони та фрагменти для редактора + Офлайн-галерея додатків для Code on the Go: плагіни, шаблони, фрагменти та дії з кодом. Переглядайте, фільтруйте та завантажуйте їх без підключення до інтернету. + Додатки + Оновити додатки + Оновлення додатків... + Скасування... + Оновлення додатків скасовано + Додатки оновлено + Не вдалося оновити додатки + Деякі додатки не вдалося оновити diff --git a/controller/app/src/main/res/values-vi/strings.xml b/controller/app/src/main/res/values-vi/strings.xml index 631f8c13..77bf474b 100644 --- a/controller/app/src/main/res/values-vi/strings.xml +++ b/controller/app/src/main/res/values-vi/strings.xml @@ -1375,4 +1375,14 @@ Da co ban cap nhat dashboard Nen cap nhat dashboard truoc de cac tinh nang moi cua mo-dun hoat dong dung. Van cai dat + Plugin, mẫu và đoạn mã cho trình chỉnh sửa + Thư viện ngoại tuyến các tiện ích bổ sung cho Code on the Go: plugin, mẫu, đoạn mã và thao tác mã. Duyệt, lọc và tải xuống mà không cần kết nối internet. + Tiện ích bổ sung + Cập nhật tiện ích bổ sung + Đang cập nhật tiện ích bổ sung... + Đang hủy... + Đã hủy cập nhật tiện ích bổ sung + Đã cập nhật tiện ích bổ sung + Không thể cập nhật tiện ích bổ sung + Một số tiện ích bổ sung không thể cập nhật diff --git a/controller/app/src/main/res/values-yo/strings.xml b/controller/app/src/main/res/values-yo/strings.xml index deaf4e43..06b0148c 100644 --- a/controller/app/src/main/res/values-yo/strings.xml +++ b/controller/app/src/main/res/values-yo/strings.xml @@ -1395,4 +1395,14 @@ Imudojuiwon dashboard wa A gba o nimoran lati kokoo se imudojuiwon dashboard ki awon eya tuntun ti modulu le sise daradara. Fi sori ero lonakona + Àwọn afikún, àwòṣe, àti àwọn àjákù fún olóòtú + Àkójọ àìlẹ́rọ-àyélujára ti àwọn afikún fún Code on the Go: àwọn afikún, àwòṣe, àjákù, àti àwọn ìṣe kóòdù. Ṣàwárí, sẹ́, kí o sì gbà wọ́n sílẹ̀ láìsí ìsopọ̀ Íńtánẹ́ẹ̀tì. + Àwọn afikún + Ṣe ìmúdójúìwọn àwọn afikún + Ń ṣe ìmúdójúìwọn àwọn afikún... + Ń fagilé... + A fagilé ìmúdójúìwọn àwọn afikún + A ṣe ìmúdójúìwọn àwọn afikún + A kò lè ṣe ìmúdójúìwọn àwọn afikún + Àwọn afikún kan kò lè ṣe ìmúdójúìwọn diff --git a/controller/app/src/main/res/values-zh-rCN/strings.xml b/controller/app/src/main/res/values-zh-rCN/strings.xml index 452a94e6..64c3c914 100644 --- a/controller/app/src/main/res/values-zh-rCN/strings.xml +++ b/controller/app/src/main/res/values-zh-rCN/strings.xml @@ -1376,4 +1376,14 @@ 有可用的 dashboard 更新 建议先更新 dashboard,以便模块的新功能正常运行。 仍然安装 + 编辑器的插件、模板和代码片段 + Code on the Go 的离线附加组件库:插件、模板、代码片段和代码操作。无需联网即可浏览、筛选和下载。 + 附加组件 + 更新附加组件 + 正在更新附加组件... + 正在取消... + 已取消附加组件更新 + 附加组件已更新 + 无法更新附加组件 + 部分附加组件无法更新 diff --git a/controller/app/src/main/res/values/strings.xml b/controller/app/src/main/res/values/strings.xml index 77929288..ad972cc9 100644 --- a/controller/app/src/main/res/values/strings.xml +++ b/controller/app/src/main/res/values/strings.xml @@ -1560,4 +1560,17 @@ A dashboard update is available Updating the dashboard first is recommended so new module features work correctly. Install anyway + Code on the Go Add-ons + Code on the Go Add-ons + ~600 MB + Plugins, templates, and snippets for the editor + An offline gallery of add-ons for Code on the Go: plugins, templates, snippets, and code actions. Browse, filter, and download them with no internet connection. + Add-ons + Update add-ons + Updating add-ons... + Cancelling... + Add-ons update cancelled + Add-ons updated + Could not update the add-ons + Some add-ons could not be updated diff --git a/controller/docs/ADR-addons-offline-gallery.md b/controller/docs/ADR-addons-offline-gallery.md new file mode 100644 index 00000000..0f88494c --- /dev/null +++ b/controller/docs/ADR-addons-offline-gallery.md @@ -0,0 +1,150 @@ +# ADR: Add-ons offline gallery as a static-content role + +Status: Accepted +Ticket: K2GO-99 +Date: 2026-10-01 + +## Context + +Code on the Go (COGO) add-ons are plugins packaged as `.cgp` files. They are distributed as a +static gallery on Cloudflare R2 at `https://addons.appdevforall.org`, produced by the +`appdevforall/addons` "garden" (the `tools/addons` Python tool and the `publish-addons.yml` +workflow). The garden builds each `.cgp`, generates a catalog, and uploads a complete static site. + +A K2Go box is offline. People on the box need to browse, filter, read, and download the add-ons +without internet, and (later) install them in COGO. So the box must host a local copy of that +gallery. + +The published site is a client-rendered static site: + +- `index.html` plus content-hashed shell assets under `assets/` (`styles..css`, + `app..js`, `adfa-logo..svg`). +- `v1/catalog.json` (and `v1/catalog.schema.json`). +- Per add-on: `dl/.cgp`, `p/.html`, `p/.png`, `p/-night.png`, + `src/-src.tar.gz`. + +`app.js` reads `v1/catalog.json` at runtime and renders the cards, descriptions, and filtering on +the client. So a faithful mirror of these files is a fully navigable, filterable gallery, not a bare +list of links. + +This is the first of a family of content repo roles (add-ons now, maven-offline next). The goal is +cheap, localized additions that reuse existing patterns and do not grow a god class. + +## Decision + +Add a new static-distribution Ansible role, shaped on `roles/code`, that mirrors the published +add-ons gallery from R2 into the rootfs and serves it at `http://box:8085/code-addons/`. Refresh a +deployed box through dash-node (the live-content pattern), not through runrole. + +### 1. Evergreen, reference-driven mirror (no fixed file list) + +The mirror follows references from two stable anchors, so it never carries a hardcoded list of +files: + +- `v1/catalog.json` enumerates every per-add-on file for every add-on: `download`, `iconUrl`, + `iconDarkUrl`, `pageUrl`, `sourceTarball`, each with `sha256` and `size`. This is the part that + grows (21 add-ons today, more tomorrow); it lists itself in full. +- `index.html` names its own content-hashed shell assets in its `link`/`script`/`img` tags. Parse + it, follow the references. + +New add-ons and changed or added shell assets are picked up automatically. The only hard-coded names +are the two anchors, which are the site's permanent contract: the catalog is versioned under `v1/`, +so a breaking change to its shape becomes `v2/`, a deliberate signaled change, not silent drift. A +file that is reachable from neither anchor is a file the browser never requests either, so it is not +part of the functioning site. + +### 2. One transform only: the catalog base + +The shell and the per-add-on pages use relative asset paths (`index.html` uses `assets/...`; +`p/.html` uses `../assets/...`), so they serve unchanged at the `/code-addons/` sub-path. The +only rewrite is the catalog base: replace `https://addons.appdevforall.org` with `/code-addons` in +`catalog.json`. `app.js` resolves catalog URLs against `location.origin`, so the catalog base must be +a root-absolute path (`/code-addons`) that carries the mount. It then renders as +`http://:8085/code-addons/...` on any box, with no baked IP. + +The external marketing links and the Google Fonts stylesheet stay absolute; offline they degrade +without affecting the add-on gallery (the font falls back to the system stack; the nav links do not +open). + +### 3. Integrity from the catalog + +Each `.cgp` is verified with the `sha256` the catalog already carries (Ansible `get_url` +`checksum:`), so no custom verification is written. + +### 4. Install is the role; update is dash-node + +- Install (bake time): the role populates the mirror during the rootfs build, where CI has internet. + A fresh box ships with the add-ons. +- Update (deployed box): dash-node re-runs the same reference crawl in-server, durable and resumable, + the ZIM / ADFA-4849 live-content pattern. It is surfaced as an "Update add-ons" action, like + Forgejo's "Update repos". No runrole is needed: the content is pure static files that dash-node can + replace live. The role (Ansible) and the dash-node job share the algorithm, documented here, not + code, the same split Forgejo uses. + +### 5. Arch-portable by construction + +The content is pure static files: no binary, no architecture-specific anything. There is no +per-architecture branch. The only platform branch is the nginx reload (`systemd reload` vs +`pdsm restart nginx`), inherited from `roles/code`. The role runs identically on proot, systemd, +ARM64, and AMD64. + +### Filesystem and serving + +- Mirror into `{{ content_base }}/www/code-addons` (`/library/www/code-addons`), preserving the + published layout: `index.html`, `assets/`, `v1/`, `p/`, `dl/`, `src/`. +- nginx: a static `code-addons-nginx.conf` location block, copied like `code-nginx.conf`, served at + `/code-addons/` on the IIAB nginx (port 8085 under proot). +- Record disk usage and `addons_installed` in `iiab.ini` / `iiab_state.yml`, like every role. + +### How the role is added (upstream-patches) + +The role ships as a new `tools/upstream-patches` patch, the same mechanism as +`0003-forgejo-add-role-4505.patch`: it adds `roles//*` plus the variable wiring +(`default_vars.yml`, `local_vars_android_{small,medium,large}.yml`, `local_vars_large.yml`), the +`0-init` validations, and the `6-generic-apps` include. It ships `install`/`enabled` False by +default; the K2Go bake flips them per tier. + +### Load-bearing guards (what must not regress) + +- One source per fact. The catalog is the single source of the add-on set; no second list is kept. + The serve path `/code-addons` is defined once and reused by the role variable, the catalog base + rewrite, and the nginx location. +- The mirror is reference-driven, never a hardcoded enumeration, so a structure change cannot + silently drop a file. +- Update lifecycle. dash-node writes the new mirror to a temporary directory and swaps it in, so a + refresh interrupted mid-download never serves a half-mirror. The dash-node change names the owner + of that swap. + +## Consequences + +- Hosting the add-ons gallery offline is a small static-content role plus a dash-node refresh job, + reusing `roles/code` and the live-content pattern. No new framework. +- The gallery stays evergreen: each mirror pulls the current shell and the current catalog, coherent + with each other. +- The next content role (maven-offline) is this role with a different base and anchors. + +## Alternatives considered + +- Clone the add-ons repo and prune to what is needed. Rejected. The repo holds the gallery source + and the plugin source, not the built `.cgp` or `catalog.json` (those exist only on R2), so a clone + still needs R2 for the content and mixes two sources that can drift. It also drags submodules + (`llama.cpp`) and Gradle. The reference-driven R2 mirror is cleaner and coherent. +- A hardcoded file list in the role. Rejected. It drifts silently when the site structure changes (a + new asset, a renamed file). +- Map `addons.appdevforall.org` to the box (dnsmasq plus nginx `server_name`) and serve the catalog + unchanged. Viable and needs zero rewrite, but the chosen design uses an explicit local path + (`/code-addons`) and does not shadow the public hostname. Kept as a fallback if COGO hardcodes its + base. +- Update via `runrole --reinstall` (like Forgejo). Unnecessary here. The content is static, so + dash-node replaces files live without Ansible. + +## Open items + +- COGO in-app install. Whether the COGO Plugin Manager can be pointed at + `http://box:8085/code-addons/v1/catalog.json` to install directly, or people download the `.cgp` + from the gallery and sideload it. The gallery works either way; this decides whether a COGO setting + is also wired. Needs a look at the CodeOnTheGo app. +- Device smoke. Confirm the nginx root/alias and that `app.js` `fetch("v1/catalog.json")` resolves + under the `/code-addons/` mount on a real device. +- Page-body images. If an authored per-add-on page body references an image that the garden does not + publish, it is already broken upstream; out of scope here. diff --git a/iiab-android b/iiab-android index bb8fee36..5252996f 100644 --- a/iiab-android +++ b/iiab-android @@ -644,6 +644,20 @@ else echo "forgejo_enabled: True" >> "$LOCAL_VARS_DEST" ;; esac + + # K2GO-99: the code_addons role (offline Code on the Go add-ons gallery) ships install/enabled + # False in every vars template (carried via tools/upstream-patches), so enable it for Standard/Full + # here, not Basic: the mirror pulls a large set of .cgp files (hundreds of MB). + case "$LOCAL_VARS_URL" in + *_small*) + log "Basic tier: Code on the Go add-ons left off." + ;; + *) + sed -i -E '/^[[:space:]]*code_addons_(install|enabled)[[:space:]]*:/d' "$LOCAL_VARS_DEST" + echo "code_addons_install: True" >> "$LOCAL_VARS_DEST" + echo "code_addons_enabled: True" >> "$LOCAL_VARS_DEST" + ;; + esac fi #----------------------------- diff --git a/static/dashboard/CHANGELOG.md b/static/dashboard/CHANGELOG.md index b74de345..e6b978ee 100644 --- a/static/dashboard/CHANGELOG.md +++ b/static/dashboard/CHANGELOG.md @@ -4,6 +4,7 @@ One line per version, newest first. Every REST-facing change bumps the version i (the app surfaces it via `/system/dashboard/update-check` and the "Update available" pill), so this file is the human record of what each bump enables. Keep entries short: `version - change (TICKET)`. +- **1.3.8** - Add-ons offline gallery refresh (K2GO-99). New `POST /addons/refresh` re-mirrors the published Code on the Go add-ons gallery into `/library/www/code-addons` LIVE (box up, no runrole) through a detached wrapper (`tools/code-addons-refresh.sh`) that mirrors into a staging dir and swaps it in only on success (a failed or cancelled refresh never serves a half-mirror), `GET /addons/refresh/status` returns `{state, lines, downloaded, failed}` to poll (downloaded/failed parsed from the mirror's final log line), and `POST /addons/refresh/cancel` stops a running refresh (SIGKILL to the wrapper process group; safe because the live gallery is only replaced after a clean run). The wrapper reuses the role's `mirror_addons.py` (reference-driven mirror, sha256 verify, Cloudflare strip, catalog base rewrite), so install (bake) and refresh (live) share one mechanism. Localhost-only. (K2GO-99) - **1.3.7** - Non-destructive live repo refresh for Forgejo (K2GO-422). New `POST /forgejo/refresh` runs the box orchestration (`refresh_forgejo`) LIVE (box up, no runrole, no stop) through a detached wrapper (`tools/forgejo-refresh.sh`), and `GET /forgejo/refresh/status` returns `{state, lines}` to poll, and `POST /forgejo/refresh/cancel` stops a running refresh (SIGKILL to the wrapper process group; safe because the refresh only ever fast-forwards or writes a side ref). Per seeded example repo it fast-forwards when pristine, does a clean 3-way merge (`git merge-tree`, no worktree) when the owner committed on the served branch, and skips on conflict; the branch only moves forward (every push is a fast-forward through the forge hooks), so a refresh never drops a commit. It also writes a human status board into each repo description (Last Update / Last Attempt / Status), derived from git state. Localhost-only. (K2GO-422) - **1.3.6** - Read-only Forgejo status for the post-install repos action (K2GO-422). New `GET /forgejo/status` returns `{reachable, adminExists, adminAuthenticable, repos, manageable}` from in-process HTTP to the forge API (no CLI, no fork, so it answers even when dash-node is off-proot, unlike the seed). The app uses it to offer "Install repos" (manageable and no repos yet), "Update repos" (repos present), or block (a K2Go admin exists that we cannot authenticate: password changed, so we must not guess or clobber). `manageable` = we authenticate as the admin OR the admin is absent (the seed can create it). Read-only, localhost-only. (K2GO-422) - **1.3.5** - Forgejo repo seed via dash-node (K2GO-417). New `POST /forgejo/seed {includeRepos}` runs the box orchestration (`seed_forgejo`) LIVE (box up, no runrole, no stop) through a detached wrapper (`tools/forgejo-seed.sh`), and `GET /forgejo/seed/status` returns `{state, lines}` for the app to poll. Seeds the admin + org always, plus the full example repo set when includeRepos (default true; false = admin/org only). The forge stays a conventional role (runrole); this only injects content, so it is also the base for live repo updates (K2GO-422). Localhost-only. (K2GO-417) diff --git a/static/dashboard/package.json b/static/dashboard/package.json index 6aec73ca..89a1d17f 100644 --- a/static/dashboard/package.json +++ b/static/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "dashboard-console", - "version": "1.3.7", + "version": "1.3.8", "description": "", "main": "index.js", "scripts": { diff --git a/static/dashboard/routes.ts b/static/dashboard/routes.ts index da167b07..6e02b1e1 100644 --- a/static/dashboard/routes.ts +++ b/static/dashboard/routes.ts @@ -447,6 +447,77 @@ apiRouter.post('/forgejo/refresh/cancel', (_req: Request, res: Response): void = res.json({ ok: true, state: 'cancelled' }); }); +// --- Add-ons offline gallery refresh (K2GO-99) ------------------------------ +// Re-mirror the published Code on the Go add-ons gallery into /library/www/code-addons +// LIVE (box up, no runrole). Same detached-script plus status/log pattern as the Forgejo +// refresh. The wrapper mirrors into a staging dir and swaps it in only on success, so a +// failed or cancelled refresh never replaces the live gallery with a half-mirror. The +// wrapper reuses the role's mirror_addons.py, so install (bake) and refresh (live) share +// one mechanism. Localhost-only. +const ADDONS_REFRESH_SCRIPT = '/opt/iiab-android/tools/code-addons-refresh.sh'; +const ADDONS_REFRESH_STATUS = '/var/run/code-addons-refresh.status'; +const ADDONS_REFRESH_LOG = '/var/log/code-addons-refresh.log'; +const ADDONS_REFRESH_PID = '/var/run/code-addons-refresh.pid'; +const ADDONS_REFRESH_LOG_TAIL = 200; + +apiRouter.post('/addons/refresh', (_req: Request, res: Response): void => { + let running = false; + try { running = fs.readFileSync(ADDONS_REFRESH_STATUS, 'utf8').trim() === 'running'; } catch { /* none */ } + if (running) { res.status(409).json({ error: 'an add-ons refresh is already running' }); return; } + if (!fs.existsSync(ADDONS_REFRESH_SCRIPT)) { res.status(500).json({ error: 'add-ons refresh script not found' }); return; } + try { + // setsid => own session, so a pdsm restart of dash-node cannot kill the refresh mid-run. + const child = spawn('setsid', ['bash', ADDONS_REFRESH_SCRIPT], { + detached: true, stdio: 'ignore', env: { ...process.env }, + }); + child.unref(); + // Mark running synchronously before answering, so an immediate poll cannot read a stale state. + try { fs.writeFileSync(ADDONS_REFRESH_STATUS, 'running'); } catch { /* best effort */ } + res.status(202).json({ ok: true, state: 'running' }); + } catch (e: any) { + res.status(500).json({ error: e?.message || 'could not start add-ons refresh' }); + } +}); + +apiRouter.get('/addons/refresh/status', (_req: Request, res: Response): void => { + res.set('Cache-Control', 'no-store'); + let state = 'idle'; + try { state = fs.readFileSync(ADDONS_REFRESH_STATUS, 'utf8').trim() || 'idle'; } catch { /* no file yet */ } + let lines: string[] = []; + try { + const all = fs.readFileSync(ADDONS_REFRESH_LOG, 'utf8').split('\n'); + if (all.length && all[all.length - 1] === '') all.pop(); + lines = all.slice(-ADDONS_REFRESH_LOG_TAIL); + } catch { /* no log yet */ } + // K2GO-99: mirror_addons.py ends with "done: N downloaded, M head-checked, K failed". + // Surface downloaded/failed so the app reports the outcome without parsing the whole log. + let downloaded = 0; + let failed = 0; + for (const l of lines) { + const m = l.match(/^done: (\d+) downloaded, \d+ head-checked, (\d+) failed/); + if (m) { downloaded = parseInt(m[1], 10); failed = parseInt(m[2], 10); } + } + res.json({ state, lines, downloaded, failed }); +}); + +// Cancel a running refresh. The wrapper runs under setsid (its own process group), so a SIGKILL to +// the group (kill -pid) stops the shell and the mirror child. Safe: the live gallery is only replaced +// after a clean run, so a mid-run kill leaves the served gallery untouched (the partial staging dir is +// cleared on the next refresh). No-op if not running. +apiRouter.post('/addons/refresh/cancel', (_req: Request, res: Response): void => { + let running = false; + try { running = fs.readFileSync(ADDONS_REFRESH_STATUS, 'utf8').trim() === 'running'; } catch { /* none */ } + if (!running) { res.json({ ok: true, state: 'idle' }); return; } + let pid = 0; + try { pid = parseInt(fs.readFileSync(ADDONS_REFRESH_PID, 'utf8').trim(), 10); } catch { /* no pid */ } + if (pid > 0) { + try { process.kill(-pid, 'SIGKILL'); } catch { /* already gone */ } + } + // Mark terminal ourselves: the killed wrapper cannot write its own status. + try { fs.writeFileSync(ADDONS_REFRESH_STATUS, 'cancelled'); } catch { /* best effort */ } + res.json({ ok: true, state: 'cancelled' }); +}); + // Trigger a rebuild. Fire-and-forget: launches the orchestrator DETACHED and returns 202 at once; // the app then polls /system/version + RestReadiness until the API is back on the new version. // ADFA-5339: an optional { site: true } also refreshes the served landing page in the same run. The diff --git a/tools/code-addons-refresh.sh b/tools/code-addons-refresh.sh new file mode 100644 index 00000000..3c34455c --- /dev/null +++ b/tools/code-addons-refresh.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# ============================================================================ +# Name : tools/code-addons-refresh.sh +# Author : AppDevForAll +# Copyright : Copyright (c) 2026 AppDevForAll +# Description : K2GO-99. dash-node-driven refresh of the offline Code on the Go +# add-ons gallery. Re-mirrors the published gallery into a staging +# directory and swaps it in on success, LIVE (box up, no runrole). +# Writes a status file plus a log that the dash-node endpoint polls. +# Mirrors tools/forgejo-refresh.sh (status file plus detached run). +# +# Safe: the mirror runs into ".new"; the live gallery is only replaced +# after a clean run, so a failed or cancelled refresh never serves a half-mirror. +# ============================================================================ +set -u +STATUS=/var/run/code-addons-refresh.status +LOG=/var/log/code-addons-refresh.log +PID=/var/run/code-addons-refresh.pid + +# SERVE must match code_addons_serve_path (roles/code_addons/defaults). The source +# base and the catalog serve-base come from mirror_addons.py's own defaults, so +# they are not restated here. +SERVE=/library/www/code-addons +MIRROR=/opt/iiab/iiab/roles/code_addons/files/mirror_addons.py + +: > "$LOG" 2>/dev/null || true +echo running > "$STATUS" 2>/dev/null || true +# dash-node spawns this with setsid, so this shell leads the process group. Record its +# PID so POST /addons/refresh/cancel can stop the whole group (kill -PID) mid-run. +echo $$ > "$PID" 2>/dev/null || true +# Drop the pid file on a normal exit, so a finished run leaves no stale pid for a later +# cancel to kill (a reused pid). A SIGKILL from cancel skips this, but cancel also sets +# the status to 'cancelled', which guards against that. +trap 'rm -f "$PID" 2>/dev/null' EXIT + +{ + if [ ! -f "$MIRROR" ]; then + echo "code-addons-refresh: mirror script not found at $MIRROR" + echo error > "$STATUS" 2>/dev/null || true + exit 1 + fi + + STAGE="${SERVE}.new" + OLD="${SERVE}.old" + rm -rf "$STAGE" "$OLD" 2>/dev/null || true + + # Mirror into the staging dir. mirror_addons.py follows references from index.html and + # v1/catalog.json, verifies each .cgp by sha256, strips the Cloudflare injections, and + # rewrites the catalog base. A non-zero exit means a download or checksum failed. + if ! python3 "$MIRROR" --out "$STAGE"; then + echo "code-addons-refresh: mirror failed; live gallery left untouched" + rm -rf "$STAGE" 2>/dev/null || true + echo error > "$STATUS" 2>/dev/null || true + exit 1 + fi + + # Swap the fresh mirror in. All three paths live under /library/www, so each mv is a + # same-filesystem rename; the window where is absent is one rename wide, and + # nginx serves either the old tree or the new one, never a partial. + if [ -d "$SERVE" ]; then mv "$SERVE" "$OLD" 2>/dev/null || true; fi + if mv "$STAGE" "$SERVE" 2>/dev/null; then + rm -rf "$OLD" 2>/dev/null || true + echo "code-addons-refresh: swapped in the fresh gallery" + echo done > "$STATUS" 2>/dev/null || true + else + # Could not move the new tree into place: restore the previous one. + [ -d "$OLD" ] && mv "$OLD" "$SERVE" 2>/dev/null || true + rm -rf "$STAGE" 2>/dev/null || true + echo "code-addons-refresh: swap failed; restored the previous gallery" + echo error > "$STATUS" 2>/dev/null || true + exit 1 + fi +} >> "$LOG" 2>&1 diff --git a/tools/upstream-patches/patches/0004-code-addons-add-role.patch b/tools/upstream-patches/patches/0004-code-addons-add-role.patch new file mode 100644 index 00000000..295bb8b8 --- /dev/null +++ b/tools/upstream-patches/patches/0004-code-addons-add-role.patch @@ -0,0 +1,538 @@ +Upstream-PR: not submitted (K2Go-specific: mirrors the Code on the Go add-ons gallery at addons.appdevforall.org) +Upstream-Status: carried (K2Go-specific role; no upstream iiab/iiab PR planned) +Applies-to: roles/code_addons/* (new role); vars/default_vars.yml (code_addons_install/enabled); roles/6-generic-apps/tasks/main.yml (role include) +Summary: Add the 'code_addons' role: an offline mirror of the Code on the Go add-ons gallery (plugins, templates, snippets, code-actions), served by nginx at http://box/code-addons. Static files only, no daemon. The role runs files/mirror_addons.py, which follows references from index.html and v1/catalog.json, downloads and sha256-verifies every add-on file, strips the Cloudflare injections, and rewrites the catalog base to the local path. Update a deployed box via dash-node. K2GO-99. See controller/docs/ADR-addons-offline-gallery.md. + +diff --git a/roles/6-generic-apps/tasks/main.yml b/roles/6-generic-apps/tasks/main.yml +index 6705477..b940fca 100644 +--- a/roles/6-generic-apps/tasks/main.yml ++++ b/roles/6-generic-apps/tasks/main.yml +@@ -62,6 +62,11 @@ + name: wordpress + when: wordpress_install and not is_proot + ++- name: CODE_ADDONS ++ include_role: ++ name: code_addons ++ when: code_addons_install is defined and code_addons_install ++ + - name: Recording STAGE 6 HAS COMPLETED ==================== + lineinfile: + path: "{{ iiab_env_file }}" +diff --git a/roles/code_addons/defaults/main.yml b/roles/code_addons/defaults/main.yml +new file mode 100644 +index 0000000..b8568bf +--- /dev/null ++++ b/roles/code_addons/defaults/main.yml +@@ -0,0 +1,22 @@ ++# code_addons_install: True ++# code_addons_enabled: True ++ ++# Offline mirror of the Code on the Go add-ons gallery (plugins, templates, ++# snippets, code-actions). Sibling of the 'code' role: a static site served by ++# NGINX, no daemon, no database. The gallery is browsed at http://box/code-addons ++# and each add-on downloads from http://box/code-addons/dl/.cgp. ++# Design: controller/docs/ADR-addons-offline-gallery.md (K2GO-99). ++ ++# The published gallery to mirror from (the public R2 site). The role follows ++# references from index.html and v1/catalog.json, so it needs no file list: a new ++# add-on or a new shell asset is picked up on the next (re)install. ++# TO UPDATE a deployed box without a re-bake, dash-node re-runs the same mirror ++# in-server; a re-bake or 'runrole --reinstall code_addons' re-mirrors here. ++code_addons_source_base: "https://addons.appdevforall.org" ++ ++# Local serve path. The catalog base is rewritten to this, so the gallery works ++# at http:///code-addons/ with no host baked into the files. ++code_addons_url: /code-addons ++ ++# Served document root (static files, like roles/code). ++code_addons_serve_path: "{{ content_base }}/www/code-addons" # /library/www/code-addons +diff --git a/roles/code_addons/files/mirror_addons.py b/roles/code_addons/files/mirror_addons.py +new file mode 100644 +index 0000000..e1ab8ae +--- /dev/null ++++ b/roles/code_addons/files/mirror_addons.py +@@ -0,0 +1,319 @@ ++#!/usr/bin/env python3 ++"""Mirror the Code on the Go add-ons gallery for offline serving. ++ ++Download the published static site (shell, catalog, and per-add-on files) from a ++source base (default: the public R2 site) into a local directory, then rewrite the ++catalog base to the local serve path. The download set is discovered by following ++references from two stable anchors, so it never carries a hardcoded file list: ++ ++ - index.html names its own shell assets in its link, script, and img tags. ++ - v1/catalog.json lists every per-add-on file, each with a sha256. ++ ++Add-on binaries are verified against the sha256 the catalog carries. The only ++transform is the catalog base rewrite; the shell and the per-add-on pages use ++relative asset paths, so they serve unchanged under the serve path. ++ ++This is the shared mechanism for the add-ons offline gallery (K2GO-99). The ++Ansible role calls it at bake time; dash-node re-runs the same steps in-server ++for the online update. See controller/docs/ADR-addons-offline-gallery.md. ++""" ++ ++import argparse ++import hashlib ++import json ++import re ++import sys ++import urllib.request ++import urllib.error ++from html.parser import HTMLParser ++from pathlib import Path ++from urllib.parse import urljoin, urlsplit ++ ++USER_AGENT = "k2go-addons-mirror/1" ++TIMEOUT = 60 ++RETRIES = 3 ++ ++# Fixed anchors, relative to the source base. Everything else is discovered. ++INDEX = "index.html" ++CATALOG = "v1/catalog.json" ++CATALOG_SCHEMA = "v1/catalog.schema.json" ++ ++# Tags whose href/src names a static asset of the site (not a nav link). ++ASSET_TAGS = {"link": "href", "script": "src", "img": "src"} ++ ++ ++class AssetRefs(HTMLParser): ++ """Collect href/src values from asset tags (link, script, img).""" ++ ++ def __init__(self): ++ super().__init__() ++ self.refs = [] ++ ++ def handle_starttag(self, tag, attrs): ++ attr = ASSET_TAGS.get(tag) ++ if not attr: ++ return ++ for name, value in attrs: ++ if name == attr and value: ++ self.refs.append(value) ++ ++ ++def is_local_ref(ref): ++ """True for a same-site relative asset path. ++ ++ A real site asset is relative with no scheme and no leading slash ++ (for example "assets/app..js" or "../assets/styles..css"). ++ Absolute URLs (fonts, analytics) and root-absolute paths (the Cloudflare ++ "/cdn-cgi/..." injections) are not part of the authored site and are left out. ++ """ ++ if not ref: ++ return False ++ if urlsplit(ref).scheme: ++ return False ++ if ref.startswith("/"): ++ return False ++ return True ++ ++ ++# Cloudflare rewrites the served HTML in ways that are not part of the authored ++# site and must not ship on the box: ++# - the analytics beacon script (the box does not track anyone), ++# - the email-decode helper script (a dead "/cdn-cgi/..." reference offline), ++# - an inline JS-challenge / bot-detection script that loads ++# "/cdn-cgi/challenge-platform/...", ++# - the footer mailto, replaced by an obfuscated "/cdn-cgi/l/email-protection" ++# link, and any visible email replaced by a data-cfemail hex blob. ++# Remove every Cloudflare script block (inline or external) and restore the real ++# emails (Cloudflare encodes an address with a one-byte XOR, so it decodes with ++# no network call). The Google Fonts link is left in: it fails gracefully offline ++# and works when the box is online. ++SCRIPT_BLOCK = re.compile(r'', re.I | re.S) ++CF_EMAIL_EL = re.compile( ++ r'<(a|span)\b[^>]*\bdata-cfemail="([0-9a-fA-F]+)"[^>]*>.*?', re.I | re.S) ++CF_EMAIL_HREF = re.compile(r'/cdn-cgi/l/email-protection(?:#([0-9a-fA-F]+))?') ++ ++ ++def _cf_decode(hex_blob): ++ """Decode a Cloudflare-obfuscated email, or None if it does not decode.""" ++ if not hex_blob: ++ return None ++ try: ++ key = int(hex_blob[:2], 16) ++ text = "".join(chr(int(hex_blob[i:i + 2], 16) ^ key) ++ for i in range(2, len(hex_blob), 2)) ++ except ValueError: ++ return None ++ return text if "@" in text else None ++ ++ ++def clean_html(data): ++ text = data.decode("utf-8", "replace") ++ ++ # Drop every Cloudflare script block; keep the site's own scripts. ++ def drop_script(m): ++ block = m.group(0) ++ return "" if ("cdn-cgi" in block or "cloudflareinsights" in block) else block ++ ++ text = SCRIPT_BLOCK.sub(drop_script, text) ++ ++ # Restore an obfuscated element (visible email), then any leftover href. ++ def element(m): ++ email = _cf_decode(m.group(2)) ++ if not email: ++ return m.group(0) ++ if m.group(1).lower() == "a": ++ return f'{email}' ++ return email ++ ++ def href(m): ++ email = _cf_decode(m.group(1)) ++ return "mailto:" + email if email else m.group(0) ++ ++ text = CF_EMAIL_EL.sub(element, text) ++ text = CF_EMAIL_HREF.sub(href, text) ++ return text.encode("utf-8") ++ ++ ++def fetch(url): ++ last = None ++ for attempt in range(RETRIES): ++ try: ++ req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) ++ with urllib.request.urlopen(req, timeout=TIMEOUT) as r: ++ return r.read() ++ except (urllib.error.URLError, urllib.error.HTTPError, OSError) as e: ++ last = e ++ raise RuntimeError(f"fetch failed after {RETRIES} tries: {url}: {last}") ++ ++ ++def head(url): ++ """Return (status, content_length) without downloading the body.""" ++ req = urllib.request.Request(url, method="HEAD", ++ headers={"User-Agent": USER_AGENT}) ++ try: ++ with urllib.request.urlopen(req, timeout=TIMEOUT) as r: ++ length = r.headers.get("Content-Length") ++ return r.status, int(length) if length is not None else None ++ except urllib.error.HTTPError as e: ++ return e.code, None ++ ++ ++def asset_refs(html_bytes, page_url): ++ """Absolute URLs of the local assets referenced by one HTML page.""" ++ parser = AssetRefs() ++ parser.feed(html_bytes.decode("utf-8", "replace")) ++ out = [] ++ for ref in parser.refs: ++ if is_local_ref(ref): ++ out.append(urljoin(page_url, ref)) ++ return out ++ ++ ++def rel_to_base(url, source_base): ++ """Path of url under source_base, or None if it is not under it.""" ++ if not url.startswith(source_base + "/"): ++ return None ++ return url[len(source_base) + 1:] ++ ++ ++def build_plan(source_base, catalog): ++ """The complete set of site-relative paths to mirror. ++ ++ Driven only by the two anchors: the catalog (all per-add-on files) and the ++ HTML pages (their own asset tags). No path is hardcoded except the anchors. ++ """ ++ paths = {INDEX, CATALOG, CATALOG_SCHEMA} ++ sha = {} # site-relative path -> expected sha256 (binaries only) ++ size = {} # site-relative path -> expected size (binaries only) ++ ++ # 1) Content from the catalog. ++ pages = [] ++ for addon in catalog["addons"]: ++ for url in (addon["download"]["url"], addon["iconUrl"], ++ addon["iconDarkUrl"], addon["pageUrl"], ++ addon["sourceTarball"]["url"]): ++ rel = rel_to_base(url, source_base) ++ if rel: ++ paths.add(rel) ++ dl = rel_to_base(addon["download"]["url"], source_base) ++ if dl: ++ sha[dl] = addon["download"]["sha256"] ++ size[dl] = addon["download"]["size"] ++ pg = rel_to_base(addon["pageUrl"], source_base) ++ if pg: ++ pages.append(addon["pageUrl"]) ++ ++ # 2) Shell assets, discovered by crawling the HTML pages' own tags. ++ for page_url in [f"{source_base}/{INDEX}"] + pages: ++ try: ++ html = fetch(page_url) ++ except RuntimeError: ++ continue ++ for ref in asset_refs(html, page_url): ++ rel = rel_to_base(ref, source_base) ++ if rel: ++ paths.add(rel) ++ ++ return paths, sha, size ++ ++ ++def sha256_of(data): ++ h = hashlib.sha256() ++ h.update(data) ++ return h.hexdigest() ++ ++ ++def mirror(source_base, serve_base, out, limit_large=0, verbose=True): ++ source_base = source_base.rstrip("/") ++ serve_base = serve_base.rstrip("/") ++ out = Path(out) ++ out.mkdir(parents=True, exist_ok=True) ++ ++ catalog = json.loads(fetch(f"{source_base}/{CATALOG}")) ++ paths, sha, size = build_plan(source_base, catalog) ++ if verbose: ++ print(f"plan: {len(paths)} files, {len(sha)} verified binaries, " ++ f"{len(catalog['addons'])} add-ons") ++ ++ downloaded = headed = failed = 0 ++ for rel in sorted(paths): ++ url = f"{source_base}/{rel}" ++ dest = out / rel ++ dest.parent.mkdir(parents=True, exist_ok=True) ++ expected = size.get(rel) ++ ++ # In test runs, HEAD the big binaries instead of pulling the body. ++ if limit_large and expected and expected > limit_large: ++ st, length = head(url) ++ ok = st == 200 and (length == expected or length is None) ++ headed += 1 ++ if verbose: ++ mark = "ok" if ok else f"MISMATCH(status={st},len={length})" ++ print(f" HEAD {rel} ({expected} B): {mark}") ++ if not ok: ++ failed += 1 ++ continue ++ ++ try: ++ data = fetch(url) ++ except RuntimeError as e: ++ failed += 1 ++ print(f" FAIL {rel}: {e}", file=sys.stderr) ++ continue ++ ++ if rel in sha: ++ got = sha256_of(data) ++ if got != sha[rel]: ++ failed += 1 ++ print(f" SHA MISMATCH {rel}: got {got[:12]} " ++ f"want {sha[rel][:12]}", file=sys.stderr) ++ continue ++ if size.get(rel) is not None and len(data) != size[rel]: ++ failed += 1 ++ print(f" SIZE MISMATCH {rel}: got {len(data)} " ++ f"want {size[rel]}", file=sys.stderr) ++ continue ++ ++ if rel.endswith(".html"): ++ data = clean_html(data) ++ dest.write_bytes(data) ++ downloaded += 1 ++ if verbose and (downloaded % 10 == 0 or rel in sha): ++ tag = " (verified)" if rel in sha else "" ++ print(f" GET {rel} ({len(data)} B){tag}") ++ ++ # The one transform: point the catalog at the local serve base. ++ catalog_file = out / CATALOG ++ if catalog_file.exists(): ++ text = catalog_file.read_text(encoding="utf-8") ++ rewritten = text.replace(source_base, serve_base) ++ catalog_file.write_text(rewritten, encoding="utf-8") ++ n = text.count(source_base) ++ if verbose: ++ print(f"rewrote catalog base: {source_base} -> {serve_base} " ++ f"({n} occurrences)") ++ ++ print(f"done: {downloaded} downloaded, {headed} head-checked, " ++ f"{failed} failed") ++ return failed == 0 ++ ++ ++def main(argv=None): ++ ap = argparse.ArgumentParser(description="Mirror the add-ons gallery offline.") ++ ap.add_argument("--source-base", default="https://addons.appdevforall.org", ++ help="site to mirror from") ++ ap.add_argument("--serve-base", default="/code-addons", ++ help="local serve path the catalog is rewritten to") ++ ap.add_argument("--out", required=True, help="output mirror directory") ++ ap.add_argument("--limit-large", type=int, default=0, ++ help="test only: HEAD files larger than this many bytes " ++ "instead of downloading them (0 = download all)") ++ ap.add_argument("--quiet", action="store_true") ++ args = ap.parse_args(argv) ++ ok = mirror(args.source_base, args.serve_base, args.out, ++ limit_large=args.limit_large, verbose=not args.quiet) ++ return 0 if ok else 1 ++ ++ ++if __name__ == "__main__": ++ sys.exit(main()) +diff --git a/roles/code_addons/tasks/enable-or-disable.yml b/roles/code_addons/tasks/enable-or-disable.yml +new file mode 100644 +index 0000000..e8eef9f +--- /dev/null ++++ b/roles/code_addons/tasks/enable-or-disable.yml +@@ -0,0 +1,22 @@ ++- name: Enable http://box{{ code_addons_url }} via NGINX, by installing {{ nginx_conf_dir }}/code-addons-nginx.conf ++ template: ++ src: code-addons-nginx.conf.j2 ++ dest: "{{ nginx_conf_dir }}/code-addons-nginx.conf" # /etc/nginx/conf.d ++ mode: "0644" ++ when: code_addons_enabled ++ ++- name: Disable http://box{{ code_addons_url }} via NGINX, by removing {{ nginx_conf_dir }}/code-addons-nginx.conf ++ file: ++ path: "{{ nginx_conf_dir }}/code-addons-nginx.conf" # /etc/nginx/conf.d ++ state: absent ++ when: not code_addons_enabled ++ ++- name: Reload 'nginx' systemd service (not proot) ++ systemd: ++ name: nginx ++ state: reloaded ++ when: not is_proot ++ ++- name: Restart 'nginx' via pdsm (proot) ++ command: pdsm restart nginx ++ when: is_proot +diff --git a/roles/code_addons/tasks/install.yml b/roles/code_addons/tasks/install.yml +new file mode 100644 +index 0000000..3415a61 +--- /dev/null ++++ b/roles/code_addons/tasks/install.yml +@@ -0,0 +1,45 @@ ++- name: Record (initial) disk space used ++ shell: df -B1 --output=used / | tail -1 ++ register: df1 ++ ++ ++# Mirror the published gallery into the serve directory. mirror_addons.py follows ++# references from index.html and v1/catalog.json, downloads every add-on file, ++# verifies each .cgp against the catalog sha256, strips the Cloudflare injections ++# from the HTML, and rewrites the catalog base to code_addons_url. It needs ++# network and pulls the full set of .cgp files, so it is SLOW. A non-zero exit ++# (a failed download or a checksum mismatch) fails the role. ++ ++- name: Create the serve directory {{ code_addons_serve_path }} ++ file: ++ path: "{{ code_addons_serve_path }}" # /library/www/code-addons ++ state: directory ++ ++- name: Mirror the add-ons gallery from {{ code_addons_source_base }} (downloads .cgp, SLOW) ++ command: > ++ python3 {{ role_path }}/files/mirror_addons.py ++ --source-base {{ code_addons_source_base }} ++ --serve-base {{ code_addons_url }} ++ --out {{ code_addons_serve_path }} ++ ++ ++- name: Record (final) disk space used ++ shell: df -B1 --output=used / | tail -1 ++ register: df2 ++ ++- name: Add 'code_addons_disk_usage = {{ df2.stdout | int - df1.stdout | int }}' to {{ iiab_ini_file }} ++ ini_file: ++ path: "{{ iiab_ini_file }}" # /etc/iiab/iiab.ini ++ section: code_addons ++ option: code_addons_disk_usage ++ value: "{{ df2.stdout | int - df1.stdout | int }}" ++ ++- name: "Set 'code_addons_installed: True'" ++ set_fact: ++ code_addons_installed: True ++ ++- name: "Add 'code_addons_installed: True' to {{ iiab_state_file }}" ++ lineinfile: ++ path: "{{ iiab_state_file }}" # /etc/iiab/iiab_state.yml ++ regexp: '^code_addons_installed' ++ line: 'code_addons_installed: True' +diff --git a/roles/code_addons/tasks/main.yml b/roles/code_addons/tasks/main.yml +new file mode 100644 +index 0000000..302ca36 +--- /dev/null ++++ b/roles/code_addons/tasks/main.yml +@@ -0,0 +1,46 @@ ++- name: Assert that "code_addons_install is sameas true" (boolean not string etc) ++ assert: ++ that: code_addons_install is sameas true ++ fail_msg: "PLEASE SET 'code_addons_install: True' e.g. IN: /etc/iiab/local_vars.yml" ++ quiet: yes ++ ++- name: Assert that "code_addons_enabled | type_debug == 'bool'" (boolean not string etc) ++ assert: ++ that: code_addons_enabled | type_debug == 'bool' ++ fail_msg: "PLEASE GIVE VARIABLE 'code_addons_enabled' A PROPER (UNQUOTED) ANSIBLE BOOLEAN VALUE e.g. IN: /etc/iiab/local_vars.yml" ++ quiet: yes ++ ++ ++- block: ++ ++ - name: Install the add-ons gallery if 'code_addons_installed' not defined, e.g. in {{ iiab_state_file }} ++ include_tasks: install.yml ++ when: code_addons_installed is undefined ++ ++ - name: Enable/Disable/Reload NGINX for the add-ons gallery ++ include_tasks: enable-or-disable.yml ++ ++ - name: Add 'code_addons' variable values to {{ iiab_ini_file }} ++ ini_file: ++ path: "{{ iiab_ini_file }}" # /etc/iiab/iiab.ini ++ section: code_addons ++ option: "{{ item.option }}" ++ value: "{{ item.value | string }}" ++ with_items: ++ - option: name ++ value: Code on the Go Add-ons ++ - option: description ++ value: 'Offline gallery of Code on the Go add-ons.' ++ - option: code_addons_install ++ value: "{{ code_addons_install }}" ++ - option: code_addons_enabled ++ value: "{{ code_addons_enabled }}" ++ - option: code_addons_url ++ value: "{{ code_addons_url }}" ++ ++ rescue: ++ ++ - name: 'SEE ERROR ABOVE (skip_role_on_error: {{ skip_role_on_error }})' ++ fail: ++ msg: "" ++ when: not skip_role_on_error +diff --git a/roles/code_addons/templates/code-addons-nginx.conf.j2 b/roles/code_addons/templates/code-addons-nginx.conf.j2 +new file mode 100644 +index 0000000..7face60 +--- /dev/null ++++ b/roles/code_addons/templates/code-addons-nginx.conf.j2 +@@ -0,0 +1,12 @@ ++# Static serve for the offline Code on the Go add-ons gallery at http://box{{ code_addons_url }} ++# Installed by roles/code_addons (tasks/enable-or-disable.yml) into nginx conf.d. ++# The gallery uses relative asset paths, so it serves unchanged under this sub-path. ++ ++location = {{ code_addons_url }} { ++ return 302 {{ code_addons_url }}/; ++} ++ ++location ^~ {{ code_addons_url }}/ { ++ alias {{ code_addons_serve_path }}/; # /library/www/code-addons/ ++ index index.html; ++} +diff --git a/vars/default_vars.yml b/vars/default_vars.yml +index e27500f..15c65be 100644 +--- a/vars/default_vars.yml ++++ b/vars/default_vars.yml +@@ -697,6 +697,10 @@ calibre_web_path: calibre #NEEDS WORK: https://github.com/iiab/iiab/issues/529 + code_install: False + code_enabled: False + ++# Code on the Go add-ons: offline gallery mirrored into /library/www/code-addons (K2GO-99) ++code_addons_install: False ++code_addons_enabled: False ++ + # A full-featured PBX (for rural telephony, etc) based on Asterisk and FreePBX. + # INSTRUCTIONS: https://github.com/iiab/iiab/tree/master/roles/pbx#readme + # If using PBX intensively, investigate nginx_high_php_limits further above. From fcd4d19e939d1443442389728d22d479c78ec048 Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Thu, 1 Oct 2026 12:00:54 -0600 Subject: [PATCH 2/2] K2GO-99 feat(addons): require dash-node 1.3.8 for code_addons code_addons's "Update add-ons" uses the /addons/refresh endpoints added in dash-node 1.3.8, so hard-block its install below that version (DashNodeRequirement), the same gate Forgejo uses for 1.3.7. --- .../k2go/dependency/domain/DashNodeRequirement.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/controller/app/src/main/java/org/appdevforall/k2go/dependency/domain/DashNodeRequirement.java b/controller/app/src/main/java/org/appdevforall/k2go/dependency/domain/DashNodeRequirement.java index 7db6f0a8..abc886a6 100644 --- a/controller/app/src/main/java/org/appdevforall/k2go/dependency/domain/DashNodeRequirement.java +++ b/controller/app/src/main/java/org/appdevforall/k2go/dependency/domain/DashNodeRequirement.java @@ -9,7 +9,9 @@ * * Forgejo needs the dashboard build that ships its repo status/refresh endpoints (1.3.7): * an older dash-node makes "Install repos"/"Update repos" and the seed break silently, so - * installing Forgejo is HARD-blocked below that version. Other modules have no hard minimum + * installing Forgejo is HARD-blocked below that version. Code on the Go add-ons (K2GO-99) + * needs the dashboard build that ships the /addons/refresh endpoints (1.3.8), so the gallery + * can update, so it is HARD-blocked below that version too. Other modules have no hard minimum * (a soft "update the dashboard" suggestion applies to all installs, handled in the gate). * ============================================================================ */ @@ -24,6 +26,7 @@ private DashNodeRequirement() {} @Nullable public static int[] minFor(@Nullable String moduleKey) { if ("forgejo".equals(moduleKey)) return new int[]{1, 3, 7}; + if ("code_addons".equals(moduleKey)) return new int[]{1, 3, 8}; // K2GO-99: needs /addons/refresh return null; }