Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* ============================================================================
* Name : RunScope.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-434. "What work belongs to this run" as a small monotonic latch set, extracted
* from SetupProgressActivity. Each stage (maps, module batch, Forgejo seed, dashboard
* rebuild) latches true the first time a signal for it is seen and STAYS true for the
* run, so a reopened setup screen still renders the stage and reaches completion. The
* Activity computes each per-tick signal from its live sources (that read is
* Android-coupled and stays there); this holds only the latched state. Pure JVM, no
* Android. Slice 2 of controller/docs/ADR-434-setupprogress-decomposition.md.
* ============================================================================
*/
package org.appdevforall.k2go.setup.domain;

public final class RunScope {
private boolean maps;
private boolean module;
private boolean forgejoSeed;
private boolean rebuild;

/** OR the signal into the latch and return the (possibly newly) latched value. */
public boolean latchMaps(boolean signal) { return maps |= signal; }
public boolean latchModule(boolean signal) { return module |= signal; }
public boolean latchForgejoSeed(boolean signal) { return forgejoSeed |= signal; }
public boolean latchRebuild(boolean signal) { return rebuild |= signal; }

/** Read the current latch without changing it (for a short-circuit before computing a signal). */
public boolean maps() { return maps; }
public boolean module() { return module; }
public boolean forgejoSeed() { return forgejoSeed; }
public boolean rebuild() { return rebuild; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* ============================================================================
* Name : RunSnapshot.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-434. The inputs the run verdict rule needs, captured as a plain value (no
* Android, no services). SetupProgressActivity.render() gathers these from its
* repositories/services each pass; RunVerdict turns them into working/success/failure.
* Built with the nested Builder because there are many independent boolean inputs and
* positional arguments would be easy to transpose. Slice 1 of
* controller/docs/ADR-434-setupprogress-decomposition.md.
* ============================================================================
*/
package org.appdevforall.k2go.setup.domain;

public final class RunSnapshot {
/** No live REST content in this run (a proot-only set). Selects the completion rule. */
public final boolean noRest;
/** A proot stage (maps or a module batch) is part of this run. */
public final boolean prootShown;
/** A non-maps module batch is part of this run. */
public final boolean moduleShown;
/** The REST pipeline has started every stage it was going to start. */
public final boolean drained;
/** The proot queue is terminal and not running. */
public final boolean queueTerminalNotRunning;
/** The post-batch server restart settled: the server came up, or the wait gave up. */
public final boolean moduleServerSettled;
/** The post-batch server did not come up within the wait: a failure, even with no failed item. */
public final boolean batchServerSlow;
/** A Forgejo seed still needs to run (or is running) in this run, and can make progress. */
public final boolean seedPendingRun;
public final StreamState zim;
public final StreamState books;
public final StreamState kolibri;
/** Failed proot runroles that belong to this run. */
public final int prootFailed;
/** The Forgejo seed of this run gave up. */
public final boolean forgejoSeedFailed;

private RunSnapshot(Builder b) {
this.noRest = b.noRest;
this.prootShown = b.prootShown;
this.moduleShown = b.moduleShown;
this.drained = b.drained;
this.queueTerminalNotRunning = b.queueTerminalNotRunning;
this.moduleServerSettled = b.moduleServerSettled;
this.batchServerSlow = b.batchServerSlow;
this.seedPendingRun = b.seedPendingRun;
this.zim = b.zim;
this.books = b.books;
this.kolibri = b.kolibri;
this.prootFailed = b.prootFailed;
this.forgejoSeedFailed = b.forgejoSeedFailed;
}

public static final class Builder {
private boolean noRest, prootShown, moduleShown, drained;
private boolean queueTerminalNotRunning, moduleServerSettled, batchServerSlow, seedPendingRun;
private StreamState zim = new StreamState(false, false, 0);
private StreamState books = new StreamState(false, false, 0);
private StreamState kolibri = new StreamState(false, false, 0);
private int prootFailed;
private boolean forgejoSeedFailed;

public Builder noRest(boolean v) { this.noRest = v; return this; }
public Builder prootShown(boolean v) { this.prootShown = v; return this; }
public Builder moduleShown(boolean v) { this.moduleShown = v; return this; }
public Builder drained(boolean v) { this.drained = v; return this; }
public Builder queueTerminalNotRunning(boolean v) { this.queueTerminalNotRunning = v; return this; }
public Builder moduleServerSettled(boolean v) { this.moduleServerSettled = v; return this; }
public Builder batchServerSlow(boolean v) { this.batchServerSlow = v; return this; }
public Builder seedPendingRun(boolean v) { this.seedPendingRun = v; return this; }
public Builder zim(StreamState v) { this.zim = v; return this; }
public Builder books(StreamState v) { this.books = v; return this; }
public Builder kolibri(StreamState v) { this.kolibri = v; return this; }
public Builder prootFailed(int v) { this.prootFailed = v; return this; }
public Builder forgejoSeedFailed(boolean v) { this.forgejoSeedFailed = v; return this; }
public RunSnapshot build() { return new RunSnapshot(this); }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* ============================================================================
* Name : RunVerdict.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-434. The run verdict rule, extracted from SetupProgressActivity.render() so it
* is one named, unit-tested place instead of inline boolean algebra. Pure: it reads
* only a RunSnapshot. Slice 1 of
* controller/docs/ADR-434-setupprogress-decomposition.md.
*
* Two completion rules by run shape: a proot-only run (no live REST content) finishes
* when its queue is terminal and, for a module batch, the server has settled; a run
* with REST content also waits for every live stream to drain. A pending Forgejo seed
* blocks completion in both. Once complete, the run is a success only with zero
* failures and no slow server restart.
* ============================================================================
*/
package org.appdevforall.k2go.setup.domain;

public final class RunVerdict {

public enum State { WORKING, SUCCESS, FAILURE }

private final boolean allComplete;
private final int failedTotal;
private final State state;

private RunVerdict(boolean allComplete, int failedTotal, State state) {
this.allComplete = allComplete;
this.failedTotal = failedTotal;
this.state = state;
}

public static RunVerdict of(RunSnapshot s) {
boolean allComplete;
if (s.noRest && s.prootShown) {
allComplete = s.queueTerminalNotRunning
&& (!s.moduleShown || s.moduleServerSettled)
&& !s.seedPendingRun;
} else {
allComplete = s.drained
&& s.zim.settledForCompletion()
&& s.books.settledForCompletion()
&& s.kolibri.settledForCompletion()
&& (!s.moduleShown || s.moduleServerSettled)
&& !s.seedPendingRun;
}
int failedTotal = s.zim.failed + s.books.failed + s.kolibri.failed
+ s.prootFailed + (s.forgejoSeedFailed ? 1 : 0);
State state;
if (allComplete && failedTotal == 0 && !s.batchServerSlow) state = State.SUCCESS;
else if (allComplete && (failedTotal > 0 || s.batchServerSlow)) state = State.FAILURE;
else state = State.WORKING;
return new RunVerdict(allComplete, failedTotal, state);
}

public boolean allComplete() { return allComplete; }
public int failedTotal() { return failedTotal; }
public boolean success() { return state == State.SUCCESS; }
public boolean failure() { return state == State.FAILURE; }
public State state() { return state; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* ============================================================================
* Name : SetupUiState.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-434. The setup screen's derived view state as a pure rule: the status dot tone,
* the status message, whether to animate the waiting ellipsis, the bottom-controls mode
* and whether "Run in background" shows. Semantic enums only (no Android resource ids),
* so the mapping to R.color/R.string and to show()/schedule/cancel stays in the Activity
* and this stays unit-testable on a plain JVM. Slice 3 of
* controller/docs/ADR-434-setupprogress-decomposition.md.
* ============================================================================
*/
package org.appdevforall.k2go.setup.domain;

public final class SetupUiState {

/** The status dot tone: a calm run vs a run that is waiting/working. */
public enum StatusTone { NEUTRAL, WAITING }

/** The status line message (mapped to a string resource by the Activity). */
public enum StatusMessage { STARTING, SLOW, ADDING, INSTALLING }

/** The bottom-controls mode (mapped to show()/scheduleRedirect()/cancelRedirect() by the Activity). */
public enum Controls { REDIRECT, FINISH_SUCCESS, FINISH_FAILURE, RUNNING }

public final StatusTone tone;
public final StatusMessage message;
public final boolean animate;
public final Controls controls;
public final boolean runInBackgroundVisible;

private SetupUiState(StatusTone tone, StatusMessage message, boolean animate,
Controls controls, boolean runInBackgroundVisible) {
this.tone = tone;
this.message = message;
this.animate = animate;
this.controls = controls;
this.runInBackgroundVisible = runInBackgroundVisible;
}

public static SetupUiState from(Inputs in) {
// Amber "working" while a module runs, its post-batch restart is pending, or the batch ended
// with a failed module; never while the server is slow (that is a terminal failure).
boolean amberWaiting = !in.batchServerSlow
&& (in.moduleFailed || (in.moduleFlow ? !in.batchServerUp : !in.servicesReady));
StatusTone tone = (amberWaiting || in.batchServerSlow) ? StatusTone.WAITING : StatusTone.NEUTRAL;

StatusMessage message;
if (in.batchServerSlow) message = StatusMessage.SLOW; // could not bring services up in time
else if (in.batchServerSettling) message = StatusMessage.STARTING; // reconciler is (re)starting the server
else if (in.moduleFlow && !in.batchServerUp) message = StatusMessage.INSTALLING; // runroles in flight
else if (in.moduleFailed) message = StatusMessage.INSTALLING; // keep the amber install header on a failed batch
else if (in.moduleFlow) message = StatusMessage.ADDING; // module done + server up
else if (!in.servicesReady) message = in.slowByPolls ? StatusMessage.SLOW : StatusMessage.STARTING;
else message = StatusMessage.ADDING;

Controls controls;
if (in.success && !in.redirectCancelled) controls = Controls.REDIRECT;
else if (in.success) controls = Controls.FINISH_SUCCESS; // success, countdown cancelled by the user
else if (in.failure) controls = Controls.FINISH_FAILURE;
else controls = Controls.RUNNING;

return new SetupUiState(tone, message, amberWaiting, controls, in.runInBackgroundEnabled);
}

/** The inputs the rule reads, gathered by the Activity from its live sources. */
public static final class Inputs {
boolean batchServerSlow, batchServerSettling, batchServerUp, moduleFlow, moduleFailed;
boolean servicesReady, slowByPolls, success, failure, redirectCancelled, runInBackgroundEnabled;

public Inputs batchServerSlow(boolean v) { this.batchServerSlow = v; return this; }
public Inputs batchServerSettling(boolean v) { this.batchServerSettling = v; return this; }
public Inputs batchServerUp(boolean v) { this.batchServerUp = v; return this; }
public Inputs moduleFlow(boolean v) { this.moduleFlow = v; return this; }
public Inputs moduleFailed(boolean v) { this.moduleFailed = v; return this; }
public Inputs servicesReady(boolean v) { this.servicesReady = v; return this; }
public Inputs slowByPolls(boolean v) { this.slowByPolls = v; return this; }
public Inputs success(boolean v) { this.success = v; return this; }
public Inputs failure(boolean v) { this.failure = v; return this; }
public Inputs redirectCancelled(boolean v) { this.redirectCancelled = v; return this; }
public Inputs runInBackgroundEnabled(boolean v) { this.runInBackgroundEnabled = v; return this; }
public SetupUiState build() { return SetupUiState.from(this); }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* ============================================================================
* Name : StreamState.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : K2GO-434. One content stream's state, as the run verdict rule needs it. Pure value,
* no Android, no services. Part of the SetupProgressActivity decomposition
* (controller/docs/ADR-434-setupprogress-decomposition.md), slice 1.
* ============================================================================
*/
package org.appdevforall.k2go.setup.domain;

public final class StreamState {
/** A job of this type belongs to this run. */
public final boolean session;
/** That job has reached its terminal, fully drained. */
public final boolean complete;
/** Items of this type that failed (0 when there is no session). */
public final int failed;

public StreamState(boolean session, boolean complete, int failed) {
this.session = session;
this.complete = complete;
this.failed = failed;
}

/** No longer blocks completion: not in this run, or finished. */
public boolean settledForCompletion() {
return !session || complete;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.appdevforall.k2go.setup.domain;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

/** K2GO-434: the per-run stage latches (slice 2). Pure, so a plain JVM test. */
public class RunScopeTest {

@Test
public void latchIsMonotonic() {
RunScope s = new RunScope();
assertFalse(s.maps());
assertFalse(s.latchMaps(false)); // no signal yet
assertTrue(s.latchMaps(true)); // a signal latches it
assertTrue(s.latchMaps(false)); // stays latched after the signal goes away
assertTrue(s.maps());
}

@Test
public void stagesAreIndependent() {
RunScope s = new RunScope();
s.latchModule(true);
assertTrue(s.module());
assertFalse(s.maps());
assertFalse(s.forgejoSeed());
assertFalse(s.rebuild());
}

@Test
public void eachStageLatchesOnItsOwnSignal() {
RunScope s = new RunScope();
assertTrue(s.latchMaps(true));
assertTrue(s.latchModule(true));
assertTrue(s.latchForgejoSeed(true));
assertTrue(s.latchRebuild(true));
assertTrue(s.maps() && s.module() && s.forgejoSeed() && s.rebuild());
}

@Test
public void readerDoesNotLatch() {
RunScope s = new RunScope();
assertFalse(s.rebuild()); // reading never sets it
assertFalse(s.rebuild());
assertFalse(s.latchRebuild(false));
}
}
Loading
Loading