Skip to content
Closed
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
8 changes: 8 additions & 0 deletions run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail

# Build-once launcher (contrast with ./go.sh, which runs Bloom under `dotnet watch`).
# Builds BloomExe once (Debug) and launches the built Bloom.exe directly — no C#
# file watcher, so the output tree is not held locked and rebuilds are unobstructed.
# The front-end still hot-reloads via Vite. See src/BloomBrowserUI/scripts/run.mjs.
exec node ./src/BloomBrowserUI/scripts/run.mjs "$@"
10 changes: 10 additions & 0 deletions scripts/watchBloomExe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ const dotnetArgs = [
"--automation",
];

// `pnpm go` is the HUMAN launcher: --attended keeps --automation's ready-handshake and
// single-instance bypass but turns its unattended-UI policies back off (dialog auto-close,
// problem-report suppression). Without it, any warning in the Team Collection startup sync
// auto-closed the sync dialog and silently killed the launch (dogfood bug #16, 13 Jul 2026).
// Set BLOOM_GO_UNATTENDED=1 for a fully unattended launch (agent-driven CDP runs that must
// never block on a dialog); the E2E harness has its own launcher and is unaffected either way.
if (process.env.BLOOM_GO_UNATTENDED !== "1") {
dotnetArgs.push("--attended");
}

const startupLabel = getHelpfulStartupLabel(options.repoRoot);

if (startupLabel) {
Expand Down
6 changes: 6 additions & 0 deletions src/BloomExe/ApplicationContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public ApplicationContainer()
typeof(NewCollectionWizardApi),
typeof(CollectionChooserApi),
typeof(I18NApi),
typeof(SharingApi),
}.Contains(t)
);

Expand All @@ -95,6 +96,11 @@ public ApplicationContainer()
_container.Resolve<NewCollectionWizardApi>().RegisterWithApiHandler(server.ApiHandler);
_container.Resolve<CollectionChooserApi>().RegisterWithApiHandler(server.ApiHandler);
_container.Resolve<I18NApi>().RegisterWithApiHandler(server.ApiHandler);
// Cloud Team Collections (task 06): SharingApi is application-level, not project-level
// (unlike TeamCollectionApi), because collections/mine, collections/pullDown, and
// sharing/loginState/login/logout must all work from the collection chooser BEFORE any
// project is loaded -- see SharingApi's own file comment.
_container.Resolve<SharingApi>().RegisterWithApiHandler(server.ApiHandler);
server.ApiHandler.RecordApplicationLevelHandlers();
}

Expand Down
39 changes: 39 additions & 0 deletions src/BloomExe/Collection/CollectionSettingsDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Bloom.Properties;
using Bloom.SubscriptionAndFeatures;
using Bloom.TeamCollection;
using Bloom.TeamCollection.Cloud;
using Bloom.Utils;
using Bloom.web.controllers;
using Bloom.WebLibraryIntegration;
Expand Down Expand Up @@ -53,6 +54,11 @@ public string PendingDefaultBookshelf
internal bool PendingAllowAppBuilder;
internal bool AllowTeamCollectionOptionEnabled = false;

// The "Cloud Team Collections (experimental)" checkbox: gates the S3+Supabase-backed
// Team Collection UI, wired end-to-end the same way as PendingAllowTeamCollection above.
internal bool PendingAllowCloudTeamCollection;
internal bool AllowCloudTeamCollectionOptionEnabled = false;

// "Internal" so CollectionSettingsApi can update these.
internal readonly string[] PendingFontSelections = new[] { "", "", "" };
internal string PendingNumberingStyle { get; set; }
Expand Down Expand Up @@ -118,12 +124,23 @@ XMatterPackFinder xmatterPackFinder
PendingAllowTeamCollection = ExperimentalFeatures.IsFeatureEnabled(
ExperimentalFeatures.kTeamCollections
);
PendingAllowCloudTeamCollection = ExperimentalFeatures.IsFeatureEnabled(
ExperimentalFeatures.kCloudTeamCollections
);
PendingAllowAppBuilder = ExperimentalFeatures.IsFeatureEnabled(
ExperimentalFeatures.kAppBuilder
);

if (
!ExperimentalFeatures.IsFeatureEnabled(ExperimentalFeatures.kTeamCollections)
// A user adopting ONLY the cloud flavor (leaving the folder-based
// "Team Collections" flag off) must still see this tab -- otherwise the
// "Share this collection on the Bloom sharing server" button
// (TeamCollectionSettingsPanel.tsx) would be unreachable. Found while writing
// the task 10 adoption-path user docs.
&& !ExperimentalFeatures.IsFeatureEnabled(
ExperimentalFeatures.kCloudTeamCollections
)
&& tcManager.CurrentCollectionEvenIfDisconnected == null
)
{
Expand All @@ -139,6 +156,12 @@ XMatterPackFinder xmatterPackFinder
AllowTeamCollectionOptionEnabled = !(
PendingAllowTeamCollection && tcManager.CurrentCollectionEvenIfDisconnected != null
);
// Same idea, but specifically for the cloud-backed flavor: don't allow disabling the
// cloud experimental flag while the currently-open collection is itself a cloud TC.
AllowCloudTeamCollectionOptionEnabled = !(
PendingAllowCloudTeamCollection
&& tcManager.CurrentCollectionEvenIfDisconnected is CloudTeamCollection
);

if (AutoUpdateSupportedOnThisPlatform)
PendingAutomaticallyUpdate = Settings.Default.AutoUpdate;
Expand Down Expand Up @@ -409,6 +432,7 @@ private void _okButton_Click(object sender, EventArgs e)
PendingAutomaticallyUpdate && AutoUpdateSupportedOnThisPlatform;
UpdateExperimentalBookSources();
UpdateTeamCollectionAllowed();
UpdateCloudTeamCollectionAllowed();
UpdateAppBuilderAllowed();

_collectionSettings.Country = _countryText.Text.Trim();
Expand Down Expand Up @@ -824,6 +848,21 @@ private void UpdateTeamCollectionAllowed()
ChangeThatRequiresRestart();
}

private void UpdateCloudTeamCollectionAllowed()
{
var wasCloudTeamCollectionsEnabled = ExperimentalFeatures.IsFeatureEnabled(
ExperimentalFeatures.kCloudTeamCollections
);

ExperimentalFeatures.SetValue(
ExperimentalFeatures.kCloudTeamCollections,
PendingAllowCloudTeamCollection
);

if (wasCloudTeamCollectionsEnabled != PendingAllowCloudTeamCollection)
ChangeThatRequiresRestart();
}

private void UpdateAppBuilderAllowed()
{
// NB: This change does not require a restart.
Expand Down
16 changes: 16 additions & 0 deletions src/BloomExe/ErrorReporter/HtmlErrorReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,22 @@ Action<string, Exception> onExtraButtonClicked
// Before we do anything that might be "risky", put the problem in the log.
ProblemReportApi.LogProblem(exception, messageText, severity);

if (Program.UnattendedAutomation)
{
// In UNATTENDED automation (E2E harnesses, CI -- --automation without
// --attended) there is no human to dismiss this modal, so showing it would hang
// the automated run forever (see the matching gates in
// ProblemReportApi.ShowProblemReactDialogWithFallbacks and
// BrowserProgressDialog). The problem is already in the log (above), which is
// how the driving test learns about it. Attended automation (`pnpm go`) shows
// the dialog normally.
Logger.WriteEvent(
"HtmlErrorReporter: notify dialog suppressed in automation mode (see the "
+ "problem logged just before this)."
);
return;
}

// ENHANCE: Allow the caller to pass in the control, which would be at the front of this.
//System.Windows.Forms.Control control = Form.ActiveForm ?? FatalExceptionHandler.ControlOnUIThread;
var control = GetControlToUse();
Expand Down
7 changes: 7 additions & 0 deletions src/BloomExe/ExperimentalFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ public static class ExperimentalFeatures
public const string kTeamCollections = "team-collections";
public const string kAppBuilder = "app-builder";

/// <summary>
/// Token for the cloud-backed Team Collections experimental feature. Wired to the
/// "Cloud Team Collections (experimental)" checkbox in Settings -> Advanced (see
/// CollectionSettingsDialog.PendingAllowCloudTeamCollection / CollectionSettingsApi).
/// </summary>
public const string kCloudTeamCollections = "cloud-team-collections";

public static string TokensOfEnabledFeatures =>
Settings.Default.EnabledExperimentalFeatures;

Expand Down
7 changes: 7 additions & 0 deletions src/BloomExe/History/HistoryEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,15 @@ public enum BookHistoryEventType
SyncProblem,
Deleted,
Moved, // Moved from one collection to another

// NB: add them here, too: teamCollection\CollectionHistoryTable.tsx
// and also add them to EventTypeEnumerationIsStable() in History\HistoryEventTests.cs

// Cloud Team Collection incident events start at 100 so that ordinary events added
// above (always at the end, before 100) can never collide with them. These numeric
// values are shared with the server's tc.events.type check constraint
// (supabase/migrations) — keep the two in sync.
WorkPreservedLocally = 100, // local work saved to Lost & Found as a .bloomSource
}

/// <summary>
Expand Down
41 changes: 39 additions & 2 deletions src/BloomExe/MiscUI/BrowserProgressDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public static void DoWorkWithProgressDialog(
// depending on the nature of the problem, we might want to do more or less than this.
// But at least this lets the dialog reach one of the states where it can be closed,
// and gives the user some idea things are not right.
SIL.Reporting.Logger.WriteError(
"BrowserProgressDialog: the work behind a progress dialog failed",
ex
);
socketServer.SendEvent(socketContext, "finished");
waitForUserToCloseDialogOrReportProblems = true;
progress.MessageWithoutLocalizing(
Expand All @@ -92,7 +96,25 @@ public static void DoWorkWithProgressDialog(

// stop the spinner
socketServer.SendEvent(socketContext, "finished");
if (waitForUserToCloseDialogOrReportProblems)
if (waitForUserToCloseDialogOrReportProblems && Program.UnattendedAutomation)
{
// In UNATTENDED automation (E2E harnesses, CI -- --automation without
// --attended) there is no human to click the Close/Report buttons, so the
// wait-for-user state below would hang the run forever (diagnosed twice
// from dotnet-stack dumps of hung instances -- see
// Design/CloudTeamCollections/tasks/09-e2e.md finding 9). Log that
// problems were reported and close, so the failure surfaces to the
// driving test as an error it can read instead of a hang. An ATTENDED
// automation launch (a developer's `pnpm go`) keeps the buttons: bug #16
// was this auto-close silently killing a human's startup over a warning.
SIL.Reporting.Logger.WriteEvent(
"BrowserProgressDialog: problems were reported during progress-dialog "
+ "work; auto-closing because Bloom is in automation mode (see "
+ "preceding log entries for the actual error)."
);
dlg.Invoke((Action)(() => dlg.Close()));
}
else if (waitForUserToCloseDialogOrReportProblems)
{
// Now the user is allowed to close the dialog or report problems.
// (ProgressDialog in JS-land is watching for this message, which causes it to turn
Expand Down Expand Up @@ -223,6 +245,10 @@ public static async Task DoWorkWithProgressDialogAsync(
// depending on the nature of the problem, we might want to do more or less than this.
// But at least this lets the dialog reach one of the states where it can be closed,
// and gives the user some idea things are not right.
SIL.Reporting.Logger.WriteError(
"BrowserProgressDialog: the work behind a progress dialog failed",
ex
);
socketServer.SendEvent(socketContext, "finished");
waitForUserToCloseDialogOrReportProblems = true;
_progress.MessageWithoutLocalizing(
Expand All @@ -233,7 +259,18 @@ public static async Task DoWorkWithProgressDialogAsync(

// stop the spinner
socketServer.SendEvent(socketContext, "finished");
if (waitForUserToCloseDialogOrReportProblems)
if (waitForUserToCloseDialogOrReportProblems && Program.UnattendedAutomation)
{
// See the matching branch in DoWorkWithProgressDialog above: no human exists
// to click the buttons in UNATTENDED automation, so close instead of hanging.
SIL.Reporting.Logger.WriteEvent(
"BrowserProgressDialog: problems were reported during progress-dialog "
+ "work; auto-closing because Bloom is in automation mode (see "
+ "preceding log entries for the actual error)."
);
socketServer.SendBundle(socketContext, "close-progress", new DynamicJson());
}
else if (waitForUserToCloseDialogOrReportProblems)
{
// Now the user is allowed to close the dialog or report problems.
// (ProgressDialog in JS-land is watching for this message, which causes it to turn
Expand Down
16 changes: 16 additions & 0 deletions src/BloomExe/NonFatalProblem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ public static void Report(
return;
}

if (Program.UnattendedAutomation)
{
// In UNATTENDED automation (--automation without --attended) there is no
// human to dismiss a modal MessageBox, so a
// modal report silently hangs the whole instance -- possibly before any
// window or server exists (found via a live stack dump: an E2E-relaunched
// instance sat blocked in MessageBox.Show inside TeamCollectionManager's
// constructor for the full ready-timeout with EMPTY stdout, 10 Jul 2026).
// Report to stdout (the harness's per-instance log, the same channel
// BLOOM_AUTOMATION_READY uses) and keep going.
Console.WriteLine(
$"BLOOM_AUTOMATION_NONFATAL_PROBLEM {fullDetailedMessage}\n{exception}"
);
return;
}

//just convert from PassiveIf to ModalIf so that we don't have to duplicate code
var passive = (ModalIf)ModalIf.Parse(typeof(ModalIf), passiveThreshold.ToString());
var formForSynchronizing = Shell.GetShellOrOtherOpenForm();
Expand Down
Loading
Loading