Skip to content

Commit fadfa50

Browse files
Merge pull request #600 from appdevforall/refactor/K2GO-434-setupprogress-decomposition
K2GO-434 refactor(setup): decomposition ADR + first three pure-domain slices
2 parents 7754a20 + 1c979ac commit fadfa50

10 files changed

Lines changed: 785 additions & 83 deletions

File tree

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

Lines changed: 88 additions & 83 deletions
Large diffs are not rendered by default.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* ============================================================================
3+
* Name : RunScope.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-434. "What work belongs to this run" as a small monotonic latch set, extracted
7+
* from SetupProgressActivity. Each stage (maps, module batch, Forgejo seed, dashboard
8+
* rebuild) latches true the first time a signal for it is seen and STAYS true for the
9+
* run, so a reopened setup screen still renders the stage and reaches completion. The
10+
* Activity computes each per-tick signal from its live sources (that read is
11+
* Android-coupled and stays there); this holds only the latched state. Pure JVM, no
12+
* Android. Slice 2 of controller/docs/ADR-434-setupprogress-decomposition.md.
13+
* ============================================================================
14+
*/
15+
package org.appdevforall.k2go.setup.domain;
16+
17+
public final class RunScope {
18+
private boolean maps;
19+
private boolean module;
20+
private boolean forgejoSeed;
21+
private boolean rebuild;
22+
23+
/** OR the signal into the latch and return the (possibly newly) latched value. */
24+
public boolean latchMaps(boolean signal) { return maps |= signal; }
25+
public boolean latchModule(boolean signal) { return module |= signal; }
26+
public boolean latchForgejoSeed(boolean signal) { return forgejoSeed |= signal; }
27+
public boolean latchRebuild(boolean signal) { return rebuild |= signal; }
28+
29+
/** Read the current latch without changing it (for a short-circuit before computing a signal). */
30+
public boolean maps() { return maps; }
31+
public boolean module() { return module; }
32+
public boolean forgejoSeed() { return forgejoSeed; }
33+
public boolean rebuild() { return rebuild; }
34+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* ============================================================================
3+
* Name : RunSnapshot.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-434. The inputs the run verdict rule needs, captured as a plain value (no
7+
* Android, no services). SetupProgressActivity.render() gathers these from its
8+
* repositories/services each pass; RunVerdict turns them into working/success/failure.
9+
* Built with the nested Builder because there are many independent boolean inputs and
10+
* positional arguments would be easy to transpose. Slice 1 of
11+
* controller/docs/ADR-434-setupprogress-decomposition.md.
12+
* ============================================================================
13+
*/
14+
package org.appdevforall.k2go.setup.domain;
15+
16+
public final class RunSnapshot {
17+
/** No live REST content in this run (a proot-only set). Selects the completion rule. */
18+
public final boolean noRest;
19+
/** A proot stage (maps or a module batch) is part of this run. */
20+
public final boolean prootShown;
21+
/** A non-maps module batch is part of this run. */
22+
public final boolean moduleShown;
23+
/** The REST pipeline has started every stage it was going to start. */
24+
public final boolean drained;
25+
/** The proot queue is terminal and not running. */
26+
public final boolean queueTerminalNotRunning;
27+
/** The post-batch server restart settled: the server came up, or the wait gave up. */
28+
public final boolean moduleServerSettled;
29+
/** The post-batch server did not come up within the wait: a failure, even with no failed item. */
30+
public final boolean batchServerSlow;
31+
/** A Forgejo seed still needs to run (or is running) in this run, and can make progress. */
32+
public final boolean seedPendingRun;
33+
public final StreamState zim;
34+
public final StreamState books;
35+
public final StreamState kolibri;
36+
/** Failed proot runroles that belong to this run. */
37+
public final int prootFailed;
38+
/** The Forgejo seed of this run gave up. */
39+
public final boolean forgejoSeedFailed;
40+
41+
private RunSnapshot(Builder b) {
42+
this.noRest = b.noRest;
43+
this.prootShown = b.prootShown;
44+
this.moduleShown = b.moduleShown;
45+
this.drained = b.drained;
46+
this.queueTerminalNotRunning = b.queueTerminalNotRunning;
47+
this.moduleServerSettled = b.moduleServerSettled;
48+
this.batchServerSlow = b.batchServerSlow;
49+
this.seedPendingRun = b.seedPendingRun;
50+
this.zim = b.zim;
51+
this.books = b.books;
52+
this.kolibri = b.kolibri;
53+
this.prootFailed = b.prootFailed;
54+
this.forgejoSeedFailed = b.forgejoSeedFailed;
55+
}
56+
57+
public static final class Builder {
58+
private boolean noRest, prootShown, moduleShown, drained;
59+
private boolean queueTerminalNotRunning, moduleServerSettled, batchServerSlow, seedPendingRun;
60+
private StreamState zim = new StreamState(false, false, 0);
61+
private StreamState books = new StreamState(false, false, 0);
62+
private StreamState kolibri = new StreamState(false, false, 0);
63+
private int prootFailed;
64+
private boolean forgejoSeedFailed;
65+
66+
public Builder noRest(boolean v) { this.noRest = v; return this; }
67+
public Builder prootShown(boolean v) { this.prootShown = v; return this; }
68+
public Builder moduleShown(boolean v) { this.moduleShown = v; return this; }
69+
public Builder drained(boolean v) { this.drained = v; return this; }
70+
public Builder queueTerminalNotRunning(boolean v) { this.queueTerminalNotRunning = v; return this; }
71+
public Builder moduleServerSettled(boolean v) { this.moduleServerSettled = v; return this; }
72+
public Builder batchServerSlow(boolean v) { this.batchServerSlow = v; return this; }
73+
public Builder seedPendingRun(boolean v) { this.seedPendingRun = v; return this; }
74+
public Builder zim(StreamState v) { this.zim = v; return this; }
75+
public Builder books(StreamState v) { this.books = v; return this; }
76+
public Builder kolibri(StreamState v) { this.kolibri = v; return this; }
77+
public Builder prootFailed(int v) { this.prootFailed = v; return this; }
78+
public Builder forgejoSeedFailed(boolean v) { this.forgejoSeedFailed = v; return this; }
79+
public RunSnapshot build() { return new RunSnapshot(this); }
80+
}
81+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* ============================================================================
3+
* Name : RunVerdict.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-434. The run verdict rule, extracted from SetupProgressActivity.render() so it
7+
* is one named, unit-tested place instead of inline boolean algebra. Pure: it reads
8+
* only a RunSnapshot. Slice 1 of
9+
* controller/docs/ADR-434-setupprogress-decomposition.md.
10+
*
11+
* Two completion rules by run shape: a proot-only run (no live REST content) finishes
12+
* when its queue is terminal and, for a module batch, the server has settled; a run
13+
* with REST content also waits for every live stream to drain. A pending Forgejo seed
14+
* blocks completion in both. Once complete, the run is a success only with zero
15+
* failures and no slow server restart.
16+
* ============================================================================
17+
*/
18+
package org.appdevforall.k2go.setup.domain;
19+
20+
public final class RunVerdict {
21+
22+
public enum State { WORKING, SUCCESS, FAILURE }
23+
24+
private final boolean allComplete;
25+
private final int failedTotal;
26+
private final State state;
27+
28+
private RunVerdict(boolean allComplete, int failedTotal, State state) {
29+
this.allComplete = allComplete;
30+
this.failedTotal = failedTotal;
31+
this.state = state;
32+
}
33+
34+
public static RunVerdict of(RunSnapshot s) {
35+
boolean allComplete;
36+
if (s.noRest && s.prootShown) {
37+
allComplete = s.queueTerminalNotRunning
38+
&& (!s.moduleShown || s.moduleServerSettled)
39+
&& !s.seedPendingRun;
40+
} else {
41+
allComplete = s.drained
42+
&& s.zim.settledForCompletion()
43+
&& s.books.settledForCompletion()
44+
&& s.kolibri.settledForCompletion()
45+
&& (!s.moduleShown || s.moduleServerSettled)
46+
&& !s.seedPendingRun;
47+
}
48+
int failedTotal = s.zim.failed + s.books.failed + s.kolibri.failed
49+
+ s.prootFailed + (s.forgejoSeedFailed ? 1 : 0);
50+
State state;
51+
if (allComplete && failedTotal == 0 && !s.batchServerSlow) state = State.SUCCESS;
52+
else if (allComplete && (failedTotal > 0 || s.batchServerSlow)) state = State.FAILURE;
53+
else state = State.WORKING;
54+
return new RunVerdict(allComplete, failedTotal, state);
55+
}
56+
57+
public boolean allComplete() { return allComplete; }
58+
public int failedTotal() { return failedTotal; }
59+
public boolean success() { return state == State.SUCCESS; }
60+
public boolean failure() { return state == State.FAILURE; }
61+
public State state() { return state; }
62+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* ============================================================================
3+
* Name : SetupUiState.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-434. The setup screen's derived view state as a pure rule: the status dot tone,
7+
* the status message, whether to animate the waiting ellipsis, the bottom-controls mode
8+
* and whether "Run in background" shows. Semantic enums only (no Android resource ids),
9+
* so the mapping to R.color/R.string and to show()/schedule/cancel stays in the Activity
10+
* and this stays unit-testable on a plain JVM. Slice 3 of
11+
* controller/docs/ADR-434-setupprogress-decomposition.md.
12+
* ============================================================================
13+
*/
14+
package org.appdevforall.k2go.setup.domain;
15+
16+
public final class SetupUiState {
17+
18+
/** The status dot tone: a calm run vs a run that is waiting/working. */
19+
public enum StatusTone { NEUTRAL, WAITING }
20+
21+
/** The status line message (mapped to a string resource by the Activity). */
22+
public enum StatusMessage { STARTING, SLOW, ADDING, INSTALLING }
23+
24+
/** The bottom-controls mode (mapped to show()/scheduleRedirect()/cancelRedirect() by the Activity). */
25+
public enum Controls { REDIRECT, FINISH_SUCCESS, FINISH_FAILURE, RUNNING }
26+
27+
public final StatusTone tone;
28+
public final StatusMessage message;
29+
public final boolean animate;
30+
public final Controls controls;
31+
public final boolean runInBackgroundVisible;
32+
33+
private SetupUiState(StatusTone tone, StatusMessage message, boolean animate,
34+
Controls controls, boolean runInBackgroundVisible) {
35+
this.tone = tone;
36+
this.message = message;
37+
this.animate = animate;
38+
this.controls = controls;
39+
this.runInBackgroundVisible = runInBackgroundVisible;
40+
}
41+
42+
public static SetupUiState from(Inputs in) {
43+
// Amber "working" while a module runs, its post-batch restart is pending, or the batch ended
44+
// with a failed module; never while the server is slow (that is a terminal failure).
45+
boolean amberWaiting = !in.batchServerSlow
46+
&& (in.moduleFailed || (in.moduleFlow ? !in.batchServerUp : !in.servicesReady));
47+
StatusTone tone = (amberWaiting || in.batchServerSlow) ? StatusTone.WAITING : StatusTone.NEUTRAL;
48+
49+
StatusMessage message;
50+
if (in.batchServerSlow) message = StatusMessage.SLOW; // could not bring services up in time
51+
else if (in.batchServerSettling) message = StatusMessage.STARTING; // reconciler is (re)starting the server
52+
else if (in.moduleFlow && !in.batchServerUp) message = StatusMessage.INSTALLING; // runroles in flight
53+
else if (in.moduleFailed) message = StatusMessage.INSTALLING; // keep the amber install header on a failed batch
54+
else if (in.moduleFlow) message = StatusMessage.ADDING; // module done + server up
55+
else if (!in.servicesReady) message = in.slowByPolls ? StatusMessage.SLOW : StatusMessage.STARTING;
56+
else message = StatusMessage.ADDING;
57+
58+
Controls controls;
59+
if (in.success && !in.redirectCancelled) controls = Controls.REDIRECT;
60+
else if (in.success) controls = Controls.FINISH_SUCCESS; // success, countdown cancelled by the user
61+
else if (in.failure) controls = Controls.FINISH_FAILURE;
62+
else controls = Controls.RUNNING;
63+
64+
return new SetupUiState(tone, message, amberWaiting, controls, in.runInBackgroundEnabled);
65+
}
66+
67+
/** The inputs the rule reads, gathered by the Activity from its live sources. */
68+
public static final class Inputs {
69+
boolean batchServerSlow, batchServerSettling, batchServerUp, moduleFlow, moduleFailed;
70+
boolean servicesReady, slowByPolls, success, failure, redirectCancelled, runInBackgroundEnabled;
71+
72+
public Inputs batchServerSlow(boolean v) { this.batchServerSlow = v; return this; }
73+
public Inputs batchServerSettling(boolean v) { this.batchServerSettling = v; return this; }
74+
public Inputs batchServerUp(boolean v) { this.batchServerUp = v; return this; }
75+
public Inputs moduleFlow(boolean v) { this.moduleFlow = v; return this; }
76+
public Inputs moduleFailed(boolean v) { this.moduleFailed = v; return this; }
77+
public Inputs servicesReady(boolean v) { this.servicesReady = v; return this; }
78+
public Inputs slowByPolls(boolean v) { this.slowByPolls = v; return this; }
79+
public Inputs success(boolean v) { this.success = v; return this; }
80+
public Inputs failure(boolean v) { this.failure = v; return this; }
81+
public Inputs redirectCancelled(boolean v) { this.redirectCancelled = v; return this; }
82+
public Inputs runInBackgroundEnabled(boolean v) { this.runInBackgroundEnabled = v; return this; }
83+
public SetupUiState build() { return SetupUiState.from(this); }
84+
}
85+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* ============================================================================
3+
* Name : StreamState.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-434. One content stream's state, as the run verdict rule needs it. Pure value,
7+
* no Android, no services. Part of the SetupProgressActivity decomposition
8+
* (controller/docs/ADR-434-setupprogress-decomposition.md), slice 1.
9+
* ============================================================================
10+
*/
11+
package org.appdevforall.k2go.setup.domain;
12+
13+
public final class StreamState {
14+
/** A job of this type belongs to this run. */
15+
public final boolean session;
16+
/** That job has reached its terminal, fully drained. */
17+
public final boolean complete;
18+
/** Items of this type that failed (0 when there is no session). */
19+
public final int failed;
20+
21+
public StreamState(boolean session, boolean complete, int failed) {
22+
this.session = session;
23+
this.complete = complete;
24+
this.failed = failed;
25+
}
26+
27+
/** No longer blocks completion: not in this run, or finished. */
28+
public boolean settledForCompletion() {
29+
return !session || complete;
30+
}
31+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.appdevforall.k2go.setup.domain;
2+
3+
import static org.junit.Assert.assertFalse;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import org.junit.Test;
7+
8+
/** K2GO-434: the per-run stage latches (slice 2). Pure, so a plain JVM test. */
9+
public class RunScopeTest {
10+
11+
@Test
12+
public void latchIsMonotonic() {
13+
RunScope s = new RunScope();
14+
assertFalse(s.maps());
15+
assertFalse(s.latchMaps(false)); // no signal yet
16+
assertTrue(s.latchMaps(true)); // a signal latches it
17+
assertTrue(s.latchMaps(false)); // stays latched after the signal goes away
18+
assertTrue(s.maps());
19+
}
20+
21+
@Test
22+
public void stagesAreIndependent() {
23+
RunScope s = new RunScope();
24+
s.latchModule(true);
25+
assertTrue(s.module());
26+
assertFalse(s.maps());
27+
assertFalse(s.forgejoSeed());
28+
assertFalse(s.rebuild());
29+
}
30+
31+
@Test
32+
public void eachStageLatchesOnItsOwnSignal() {
33+
RunScope s = new RunScope();
34+
assertTrue(s.latchMaps(true));
35+
assertTrue(s.latchModule(true));
36+
assertTrue(s.latchForgejoSeed(true));
37+
assertTrue(s.latchRebuild(true));
38+
assertTrue(s.maps() && s.module() && s.forgejoSeed() && s.rebuild());
39+
}
40+
41+
@Test
42+
public void readerDoesNotLatch() {
43+
RunScope s = new RunScope();
44+
assertFalse(s.rebuild()); // reading never sets it
45+
assertFalse(s.rebuild());
46+
assertFalse(s.latchRebuild(false));
47+
}
48+
}

0 commit comments

Comments
 (0)