Skip to content

Commit 8d10ae0

Browse files
Merge pull request #593 from appdevforall/feat/K2GO-422-forgejo-repos-post-install
K2GO-422 feat(forgejo): post-install "Install repos" action + status endpoint
2 parents 3f8bf4e + b627b80 commit 8d10ae0

43 files changed

Lines changed: 345 additions & 12 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎controller/app/src/main/java/org/appdevforall/k2go/forgejo/data/ForgejoSeedClient.java‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,24 @@ public interface Listener {
6666
*/
6767
@NonNull
6868
public Result drive(boolean includeRepos, @Nullable Listener l) {
69+
return drive(includeRepos, false, l);
70+
}
71+
72+
/**
73+
* As {@link #drive(boolean, Listener)}, but {@code force} ignores a leftover "done" status and
74+
* (re)starts a fresh seed. The status file is a single, shared marker, so a prior op's "done"
75+
* would otherwise short-circuit an INTENTIONAL re-seed (K2GO-422 post-install repos) in a few ms
76+
* without running anything. The box POST rewrites the status to "running" synchronously, so the
77+
* poll then follows the fresh run. Non-forced keeps the re-attach optimization (an app that died
78+
* mid-seed reads the finished box status instead of re-running the whole seed).
79+
*/
80+
@NonNull
81+
public Result drive(boolean includeRepos, boolean force, @Nullable Listener l) {
6982
String state = readState(l);
70-
if ("done".equals(state)) return Result.DONE;
83+
if (!force && "done".equals(state)) return Result.DONE;
7184
if (!"running".equals(state)) {
72-
// idle / error / missing -> (re)start it. A 409 "already running" is fine: fall through to poll.
85+
// idle / error / missing (or forced past a stale done) -> (re)start it. A 409 "already
86+
// running" is fine: fall through to poll.
7387
if (!postStart(includeRepos)) return Result.ERROR;
7488
}
7589
final long deadline = System.currentTimeMillis() + MAX_WAIT_MS;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* ============================================================================
3+
* Name : ForgejoStatusClient.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-422. App-side client of the dash-node Forgejo status endpoint
7+
* (static/dashboard GET /k2go-api/forgejo/status, dash-node 1.3.6).
8+
* Read-only: it tells the module detail whether to offer "Install
9+
* repos", show the repos as present, or block (an admin exists that
10+
* K2Go cannot authenticate). The box does the work over its own API,
11+
* so this is a plain HTTP GET the app runs on an IO thread.
12+
* ============================================================================
13+
*/
14+
package org.appdevforall.k2go.forgejo.data;
15+
16+
import androidx.annotation.Nullable;
17+
18+
import org.appdevforall.k2go.config.BoxEndpoints;
19+
import org.json.JSONArray;
20+
import org.json.JSONObject;
21+
22+
import java.io.ByteArrayOutputStream;
23+
import java.io.InputStream;
24+
import java.net.HttpURLConnection;
25+
import java.net.URL;
26+
import java.nio.charset.StandardCharsets;
27+
28+
/** Fetches the box Forgejo status. {@link #fetch()} BLOCKS (one HTTP GET), so callers run it off the
29+
* main thread. Returns {@code null} when the box did not answer (unreachable / not ready). */
30+
public final class ForgejoStatusClient {
31+
32+
/** Immutable snapshot of the box Forgejo, as the status endpoint reports it. */
33+
public static final class Status {
34+
public final boolean reachable;
35+
public final boolean adminExists;
36+
public final boolean adminAuthenticable;
37+
public final boolean manageable;
38+
public final int repoCount;
39+
40+
Status(boolean reachable, boolean adminExists, boolean adminAuthenticable,
41+
boolean manageable, int repoCount) {
42+
this.reachable = reachable;
43+
this.adminExists = adminExists;
44+
this.adminAuthenticable = adminAuthenticable;
45+
this.manageable = manageable;
46+
this.repoCount = repoCount;
47+
}
48+
49+
/** K2Go can administer the forge and it has no example repos yet -> offer "Install repos". */
50+
public boolean canInstallRepos() { return manageable && repoCount == 0; }
51+
52+
/** The example repos are present -> nothing to install (refresh/update is K2GO-422 section A). */
53+
public boolean hasRepos() { return repoCount > 0; }
54+
55+
/** An admin exists that K2Go cannot authenticate -> do not offer any repo action. */
56+
public boolean blocked() { return !manageable; }
57+
}
58+
59+
private static final String STATUS_URL = BoxEndpoints.API + "/forgejo/status";
60+
61+
/** GET the status; null on any read/parse error (treat as "cannot tell", offer nothing). */
62+
@Nullable
63+
public Status fetch() {
64+
try {
65+
HttpURLConnection c = (HttpURLConnection) new URL(STATUS_URL).openConnection();
66+
c.setUseCaches(false);
67+
c.setConnectTimeout(4000);
68+
// The box may do a couple of short HTTP calls to the forge before answering; keep the read
69+
// timeout comfortably above their combined worst case so a warming forge does not make the
70+
// status read time out (which would silently hide the button).
71+
c.setReadTimeout(30000);
72+
c.setRequestMethod("GET");
73+
c.setRequestProperty("Accept", "application/json");
74+
int code = c.getResponseCode();
75+
boolean ok = code >= 200 && code < 300;
76+
String text = readAll(ok ? c.getInputStream() : c.getErrorStream());
77+
c.disconnect();
78+
if (!ok) return null;
79+
JSONObject j = new JSONObject(text.isEmpty() ? "{}" : text);
80+
JSONArray repos = j.optJSONArray("repos");
81+
int repoCount = repos != null ? repos.length() : 0;
82+
return new Status(
83+
j.optBoolean("reachable", false),
84+
j.optBoolean("adminExists", false),
85+
j.optBoolean("adminAuthenticable", false),
86+
j.optBoolean("manageable", false),
87+
repoCount);
88+
} catch (Exception e) {
89+
return null;
90+
}
91+
}
92+
93+
private static String readAll(@Nullable InputStream is) throws Exception {
94+
if (is == null) return "";
95+
ByteArrayOutputStream buf = new ByteArrayOutputStream();
96+
byte[] chunk = new byte[4096];
97+
int n;
98+
while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n);
99+
is.close();
100+
return buf.toString(StandardCharsets.UTF_8.name());
101+
}
102+
}

‎controller/app/src/main/java/org/appdevforall/k2go/forgejo/presentation/ForgejoSeedService.java‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,19 @@ public final class ForgejoSeedService extends Service {
5555
private static final int NOTIFICATION_ID = 8;
5656

5757
public static final String ACTION_START = "org.iiab.controller.FORGEJO_SEED_START";
58+
/** K2GO-422: force a fresh seed past a leftover "done" status (an intentional post-install re-seed). */
59+
private static final String EXTRA_FORCE = "force";
5860

5961
private final Handler main = new Handler(Looper.getMainLooper());
6062

6163
/** Start (or re-attach to) the seed. Idempotent: a running session is left alone. */
62-
public static void start(Context ctx) {
64+
public static void start(Context ctx) { start(ctx, false); }
65+
66+
/** As {@link #start(Context)}, but {@code force} re-runs even if the box status is a stale "done"
67+
* (K2GO-422: the post-install "Install repos" is an intentional re-seed). */
68+
public static void start(Context ctx, boolean force) {
6369
ContextCompat.startForegroundService(ctx,
64-
new Intent(ctx, ForgejoSeedService.class).setAction(ACTION_START));
70+
new Intent(ctx, ForgejoSeedService.class).setAction(ACTION_START).putExtra(EXTRA_FORCE, force));
6571
}
6672

6773
/** Clear the finished session so a later install starts clean. */
@@ -96,16 +102,17 @@ public int onStartCommand(Intent intent, int flags, int startId) {
96102
return START_NOT_STICKY;
97103
}
98104
boolean includeRepos = ForgejoInstallPrefs.includeRepos(this);
105+
boolean force = intent != null && intent.getBooleanExtra(EXTRA_FORCE, false);
99106
repo.startSession(includeRepos);
100-
drive(includeRepos);
107+
drive(includeRepos, force);
101108
return START_NOT_STICKY;
102109
}
103110

104111
/** Total drive attempts before the seed is marked failed (best-effort give-up). */
105112
private static final int MAX_ATTEMPTS = ForgejoInstallPrefs.MAX_ATTEMPTS;
106113
private static final long RETRY_DELAY_MS = 4000L;
107114

108-
private void drive(final boolean includeRepos) {
115+
private void drive(final boolean includeRepos, final boolean force) {
109116
final Context app = getApplicationContext();
110117
AppExecutors.get().io().execute(() -> {
111118
ForgejoSeedRepository repo = ForgejoSeedRepository.get();
@@ -127,7 +134,7 @@ private void drive(final boolean includeRepos) {
127134
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
128135
Log.i(TAG, "forgejo seed: driving (includeRepos=" + includeRepos + ", attempt "
129136
+ attempt + "/" + MAX_ATTEMPTS + ")");
130-
r = new ForgejoSeedClient().drive(includeRepos, repo::appendLog);
137+
r = new ForgejoSeedClient().drive(includeRepos, force, repo::appendLog);
131138
if (r == ForgejoSeedClient.Result.DONE) break;
132139
if (attempt < MAX_ATTEMPTS) {
133140
repo.appendLog("seed attempt " + attempt + " failed; retrying");

‎controller/app/src/main/java/org/appdevforall/k2go/redesign/ModuleDetailFragment.java‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,39 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
153153
}
154154
if (isInstalled) {
155155
addStatus(statusRow, K2GoStatusBadge.create(requireContext(), getString(R.string.k2go_mod_phase_done), R.color.k2go_leaf));
156-
return; // nothing to offer: a module cannot be uninstalled or reinstalled here
156+
// K2GO-422: an installed Forgejo can still be missing its example repos (installed
157+
// with the opt-in off, or a seed that never completed). Offer "Install repos" when
158+
// the box status says the forge is manageable and has no repos yet. Read the status
159+
// off the main thread; a null read (box not ready) or repos already present offers
160+
// nothing, and an admin we cannot authenticate shows a blocked note.
161+
if (isForgejo) {
162+
org.appdevforall.k2go.util.AppExecutors.get().io().execute(() -> {
163+
final org.appdevforall.k2go.forgejo.data.ForgejoStatusClient.Status st =
164+
new org.appdevforall.k2go.forgejo.data.ForgejoStatusClient().fetch();
165+
root.post(() -> {
166+
if (!isAdded() || st == null) return;
167+
if (st.canInstallRepos()) {
168+
installNowBtn.setText(R.string.k2go_forgejo_install_repos);
169+
installNowBtn.setOnClickListener(v -> {
170+
// Get More style: bank the seed, start the foreground service, and
171+
// hand off to the tasks index (SetupProgressActivity), which tracks
172+
// the running seed to completion (EXTRA_FORGEJO_SEED).
173+
org.appdevforall.k2go.forgejo.data.ForgejoInstallPrefs.bankSeed(requireContext(), true);
174+
// force: this is an intentional re-seed; do not let a prior seed's
175+
// leftover "done" status short-circuit it (K2GO-422).
176+
org.appdevforall.k2go.forgejo.presentation.ForgejoSeedService.start(requireContext(), true);
177+
startActivity(new android.content.Intent(requireContext(), SetupProgressActivity.class)
178+
.putExtra(SetupProgressActivity.EXTRA_FORGEJO_SEED, true));
179+
});
180+
installNowBtn.setVisibility(View.VISIBLE);
181+
} else if (st.blocked()) {
182+
addStatus(statusRow, K2GoStatusBadge.create(requireContext(),
183+
getString(R.string.k2go_forgejo_repos_blocked), R.color.k2go_amber_text));
184+
}
185+
});
186+
});
187+
}
188+
return; // a module cannot be uninstalled or reinstalled here (repos action aside)
157189
}
158190
// ADFA-4898: this module's runrole failed in the last finished batch — the SAME per-module
159191
// didFail(key) that colours the hub's "Couldn't install" pill, so only the module that

‎controller/app/src/main/java/org/appdevforall/k2go/redesign/SetupProgressActivity.java‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ public class SetupProgressActivity extends AppCompatActivity implements org.appd
6767
/** ADFA-5011: this screen is driving a dash-node REST-core rebuild (not an install/content drain).
6868
* Latched so the screen stays on the animation and blocks leaving until the rebuild is SUCCESS/FAILED. */
6969
public static final String EXTRA_REBUILD = "rebuild";
70+
/** K2GO-422: this run is a post-install Forgejo repo seed (the "Install repos" button), so the
71+
* index tracks and waits on the seed even though there is no module install this run. */
72+
public static final String EXTRA_FORGEJO_SEED = "forgejoSeed";
7073

7174
private static final long READY_POLL_MS = 2000L;
7275
private static final long REDIRECT_MS = 3000L;
@@ -116,6 +119,7 @@ public class SetupProgressActivity extends AppCompatActivity implements org.appd
116119
private boolean moduleStartFailed = false;
117120
private boolean moduleSeen = false; // latched once a non-maps proot batch is seen
118121
private boolean forgejoSeedSeen = false; // K2GO-423: latched once a Forgejo seed belongs to this session
122+
private boolean postInstallSeed = false; // K2GO-422: this run was launched to seed repos post-install
119123
private int readyPolls = 0; // ADFA-4874: failed readiness polls so far (slow-start message)
120124
// ADFA-4842: a real module batch stops the server (pdsm stop) for its runroles. When the queue is
121125
// DONE, the index restarts the server and WAITS here — showing "Starting services…" — until the REST
@@ -136,6 +140,8 @@ public class SetupProgressActivity extends AppCompatActivity implements org.appd
136140
protected void onCreate(@Nullable Bundle s) {
137141
super.onCreate(s);
138142
setContentView(R.layout.activity_k2go_setup_progress);
143+
// K2GO-422: launched by the "Install repos" button, so this run tracks the post-install seed.
144+
postInstallSeed = getIntent() != null && getIntent().getBooleanExtra(EXTRA_FORGEJO_SEED, false);
139145

140146
dot = findViewById(R.id.k2go_sp_dot);
141147
statusText = findViewById(R.id.k2go_sp_status);
@@ -709,7 +715,8 @@ private void render() {
709715
// once it is imminent (module server up) or already has a session, so it does not sit as
710716
// "Queued" through the whole runrole while the module row already tells that story.
711717
if (forgejoSeedInSession()
712-
&& (serverObservedUp() || org.appdevforall.k2go.forgejo.presentation.ForgejoSeedRepository.get().hasSession())) {
718+
&& (serverObservedUp() || postInstallSeed
719+
|| org.appdevforall.k2go.forgejo.presentation.ForgejoSeedRepository.get().hasSession())) {
713720
sections.addView(forgejoSeedRow());
714721
}
715722

@@ -760,8 +767,14 @@ private void render() {
760767
// - not when the forgejo runrole FAILED -- the seed never starts then (it would clear the
761768
// marker), so waiting on it would hang; that run is already a failure (Finish + Retry);
762769
// - not when the server is slow/failed -- also already a failure.
763-
boolean seedPendingRun = forgejoSeedActive() && !batchServerSlow && moduleShown
764-
&& !mq.didFail("forgejo");
770+
// K2GO-422: also wait in a post-install seed run (the "Install repos" button, no module this
771+
// run). The stranded case (a banked seed leaking into an unrelated Get More flow) has neither
772+
// moduleShown nor the launch extra, so it still does not block -- no hang reintroduced.
773+
boolean seedPendingRun = forgejoSeedActive() && !batchServerSlow
774+
&& (moduleShown || postInstallSeed)
775+
// The forgejo runrole failing releases the gate ONLY in a module-install flow; a
776+
// post-install seed run (postInstallSeed) must not read a stale/unrelated queue verdict.
777+
&& !(moduleShown && mq.didFail("forgejo"));
765778
if (noRest && prootShown) {
766779
// proot-only: complete when the queue is terminal — plus, for a module batch, once the server
767780
// is back (up) or the restart has failed (a dead home that wakes up seconds later is exactly

‎controller/app/src/main/res/values-ar/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,4 +1359,6 @@
13591359
<string name="k2go_forgejo_seed_repos_fmt">تمت إضافة %1$d مستودعات</string>
13601360
<string name="k2go_forgejo_seed_leave_title">يمكنك ترك هذا قيد التشغيل</string>
13611361
<string name="k2go_forgejo_seed_leave_body">يستمر الإعداد في الخلفية. عُد في أي وقت للتحقق من التقدم.</string>
1362+
<string name="k2go_forgejo_install_repos">تثبيت مستودعات نموذجية</string>
1363+
<string name="k2go_forgejo_repos_blocked">يحتوي مضيف الشيفرة هذا بالفعل على مسؤول لا يستطيع K2Go إدارته.</string>
13621364
</resources>

‎controller/app/src/main/res/values-az/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,4 +1379,6 @@
13791379
<string name="k2go_forgejo_seed_repos_fmt">%1$d anbar əlavə edildi</string>
13801380
<string name="k2go_forgejo_seed_leave_title">Bunu işləməyə buraxa bilərsiniz</string>
13811381
<string name="k2go_forgejo_seed_leave_body">Quraşdırma arxa planda davam edir. Gedişatı yoxlamaq üçün istənilən vaxt geri qayıdın.</string>
1382+
<string name="k2go_forgejo_install_repos">Nümunə anbarlarını quraşdır</string>
1383+
<string name="k2go_forgejo_repos_blocked">Bu kod hostunda artıq K2Go-nun idarə edə bilmədiyi bir admin var.</string>
13821384
</resources>

‎controller/app/src/main/res/values-bg/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,4 +1366,6 @@
13661366
<string name="k2go_forgejo_seed_repos_fmt">Добавени хранилища: %1$d</string>
13671367
<string name="k2go_forgejo_seed_leave_title">Можете да оставите това да работи</string>
13681368
<string name="k2go_forgejo_seed_leave_body">Настройката продължава на заден план. Върнете се по всяко време, за да проверите напредъка.</string>
1369+
<string name="k2go_forgejo_install_repos">Инсталиране на примерни хранилища</string>
1370+
<string name="k2go_forgejo_repos_blocked">Този хост за код вече има администратор, който K2Go не може да управлява.</string>
13691371
</resources>

‎controller/app/src/main/res/values-bn/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,4 +1372,6 @@
13721372
<string name="k2go_forgejo_seed_repos_fmt">%1$d রিপোজিটরি যোগ করা হয়েছে</string>
13731373
<string name="k2go_forgejo_seed_leave_title">আপনি এটি চলতে রেখে দিতে পারেন</string>
13741374
<string name="k2go_forgejo_seed_leave_body">সেটআপ পটভূমিতে চলতে থাকে। অগ্রগতি দেখতে যেকোনো সময় ফিরে আসুন।</string>
1375+
<string name="k2go_forgejo_install_repos">উদাহরণ রিপোজিটরি ইনস্টল করুন</string>
1376+
<string name="k2go_forgejo_repos_blocked">এই কোড হোস্টে ইতিমধ্যে একজন অ্যাডমিন আছে যা K2Go পরিচালনা করতে পারে না।</string>
13751377
</resources>

‎controller/app/src/main/res/values-cs/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,4 +1366,6 @@
13661366
<string name="k2go_forgejo_seed_repos_fmt">Přidáno repozitářů: %1$d</string>
13671367
<string name="k2go_forgejo_seed_leave_title">Můžete to nechat běžet</string>
13681368
<string name="k2go_forgejo_seed_leave_body">Nastavení pokračuje na pozadí. Kdykoli se vraťte a zkontrolujte průběh.</string>
1369+
<string name="k2go_forgejo_install_repos">Nainstalovat ukázkové repozitáře</string>
1370+
<string name="k2go_forgejo_repos_blocked">Tento hostitel kódu už má správce, kterého K2Go nemůže spravovat.</string>
13691371
</resources>

0 commit comments

Comments
 (0)