Skip to content
Open
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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ verified, tried, and decided belongs in the commit message and the PR body.
- **A `config.php` number the double cannot print back exactly is carried through as source text too**, so `9223372036854775807`, `1.0` and `1.0E+30` survive a write. Ordinary values stay real numbers, which is what keeps `maintenance_window_start` and `redis.port` reaching their validators — do not widen this into writing every integer as a float.
- **The File Browser mount's `idmap` (uid 1000 → `www-data` 33) is what makes the integration work at all**, and it needs StartOS 0.4.0-beta.10+. Files other services drop into File Browser's volume under a different uid surface as `nobody` until those services idmap their own mount to 1000 too.
- **Adding an external-storage source is a registry edit in `startos/externalStorage.ts` plus a typed mount.** File Browser is the shared hub most services route through, so a direct source is worth adding only for a service whose files live browsably on its own volume.
- **`richdocuments` drops the Microsoft formats the moment a second office connector is enabled** (`CapabilitiesService::hasOtherOOXMLApps` checks `onlyoffice` and `officeonline`), and the second app does not claim them unless it is configured — so Word, Excel and PowerPoint open in neither and silently download. That is what the `office-connectors` health check exists to catch; don't simplify it away because "only one backend is ever configured", which is true and is not the condition that breaks this.
- **`richdocuments` drops the Microsoft formats the moment a second office connector is enabled** (`CapabilitiesService::hasOtherOOXMLApps` checks `onlyoffice` and `officeonline`), and the second app does not claim them unless it is configured — so Word, Excel and PowerPoint open in neither and silently download. That is what the `office-connectors` health check exists to catch; don't simplify it away because the reconcile disables the outgoing connector on a switch, which does not stop a user enabling one by hand.
- **`richdocuments:activate-config` fetches the discovery document as it runs, so it must not be called before Collabora answers.** A fetch that fails still leaves the app holding a WOPI url with no discovery, and every document then opens to a spinner that never resolves — with nothing in Nextcloud's log naming the cause. The `office-suite` oneshot therefore waits on `sdk.getStatus(effects, { packageId }).waitFor(...)` for the document server's own health check before it touches anything. **Do not turn that into a retry.** A oneshot whose fn rejects is re-invoked by the SDK on a widening backoff forever, so a suite selected against a service that is stopped or was never installed would run `occ` every 30 seconds for the life of the chain. The wait costs nothing, runs no commands, cannot fail, and resolves the instant the service is ready; a bridge address that resolves is not that signal, because the port is bound well before `coolwsd` accepts a connection.
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Six oneshots run alongside them, in order: `chown` hands the data directory to `

**`finish-upgrade` runs after the web daemon is ready, not before it**, which is what makes it safe. In the normal case the upgrade has already happened during init and this is a no-op; when it does have work to do, Apache is up serving the maintenance page while `occ upgrade` runs, exactly as a manual recovery would. It fails open — nothing it does can prevent the service from serving.

Apache also carries a generated `startos-office.conf` and the four proxy modules it needs, written into the container's filesystem on every start. It puts the chosen document server on Nextcloud's own origin — `/browser`, `/cool` and `/hosting` for Collabora, `/ds-vpath` for ONLYOFFICE — which is what lets the editor work on every address Nextcloud is reachable at rather than one. The file is empty when no office suite is selected.
Apache also carries a generated `startos-office.conf`, written into the container's filesystem on every start; the four proxy modules it needs are enabled in the image. It puts the chosen document server on Nextcloud's own origin — `/browser`, `/cool` and `/hosting` for Collabora, `/ds-vpath` for ONLYOFFICE — which is what lets the editor work on every address Nextcloud is reachable at rather than one. The file is empty when no office suite is selected.

For Collabora it also rewrites the WOPI discovery response, stripping the absolute origin out of every `urlsrc` so the editor loads same-origin — Nextcloud otherwise copies Collabora's own absolute address into the editor frame verbatim, pinning it to one address. Tracked upstream as nextcloud/richdocuments#6019; if that lands, the rewrite can go.

Expand Down Expand Up @@ -103,12 +103,12 @@ Three settings depart from what upstream would do:

None are required. Both are optional and exist only while they are selected.

| Dependency | Kind | Health checks | Required |
| ------------- | --------- | ------------- | ----------------------------------------------------------- |
| `filebrowser` | `exists` | — | Only while chosen in the External Storage action |
| `coturn` | `running` | **none** | Only while Talk call relaying is on in the Configure action |
| `collabora-online` | `running` | `cool` | Only while chosen in the Office Suite action |
| `onlyoffice-docs` | `running` | `documentserver` | Only while chosen in the Office Suite action; published to the Community Registry, not the Start9 one |
| Dependency | Kind | Health checks | Required |
| ------------------ | --------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| `filebrowser` | `exists` | — | Only while chosen in the External Storage action |
| `coturn` | `running` | **none** | Only while Talk call relaying is on in the Configure action |
| `collabora-online` | `running` | `cool` | Only while chosen in the Office Suite action |
| `onlyoffice-docs` | `running` | `documentserver` | Only while chosen in the Office Suite action; published to the Community Registry, not the Start9 one |

The External Storage action offers only the sources whose backing service is actually installed, so an uninstalled one never appears in the form.

Expand All @@ -122,10 +122,10 @@ The shared secret is read through a throwaway container that mounts only Coturn'

Two interfaces, both on the same binding and port. WebDAV is the same server under a different path, offered separately so the desktop and mobile sync clients have an address to copy.

| Interface | Id | Type | Port | Path | Description |
| --------- | -- | ---- | ---- | ---- | ----------- |
| Web UI | `ui` | ui | 80 | `/` | The web interface of Nextcloud |
| WebDAV | `webdav` | api | 80 | `/remote.php/dav/` | Addresses for WebDAV syncing |
| Interface | Id | Type | Port | Path | Description |
| --------- | -------- | ---- | ---- | ------------------ | ------------------------------ |
| Web UI | `ui` | ui | 80 | `/` | The web interface of Nextcloud |
| WebDAV | `webdav` | api | 80 | `/remote.php/dav/` | Addresses for WebDAV syncing |

Neither is masked. The addresses published for `ui` are what init writes into `trusted_domains`, so an address Nextcloud does not know about is rejected by Nextcloud itself, not by StartOS.

Expand Down Expand Up @@ -173,18 +173,20 @@ Surfaces another StartOS service's files as a folder in Nextcloud Files, using N

Selects the document server that opens office files — Collabora Online, ONLYOFFICE Docs, or none. Collabora is labelled recommended in the form and is the right answer for most installs; the trade-off is set out in `instructions.md` under **Which one to choose**, and rests on a measured round-trip rather than a marketing claim: both engines preserve text, tables, images, links, footnotes and fields exactly, but LibreOffice rewrites style-inherited formatting as direct formatting on each run, where ONLYOFFICE returns the file byte-identical in structure.

- **When to run it:** after installing one of the two services and its Nextcloud app, and again to switch or to turn editing off.
- **When to run it:** after installing one of the two services, and again to switch or to turn editing off.
- **What it changes:** `officeSuite` in `store.json`. Through it: the package's dependency on that service, the host bridge's IP in `trusted_domains`, the generated Apache proxy that serves the editor from Nextcloud's own origin, and — on the next start, via the `office-suite` oneshot — the connector app's own settings.
- **Cost:** seconds, then a restart.
- **Repeat safety:** idempotent; the form is pre-filled with the current choice.

**Only one connector app may be enabled.** `richdocuments` drops the Microsoft formats out of its default-open capability whenever it finds `onlyoffice` or `officeonline` enabled, and the other app does not pick them up unless it is configured too — so Word, Excel and PowerPoint files open in neither and download instead, with nothing in Nextcloud saying why. The `office-connectors` health check fails while that is the case and names the app to disable.

**Switching first deletes the settings written for the previous backend**, so a connector is never left pointed at a service that has since been uninstalled. That teardown is also what keeps the choice unambiguous: an unconfigured connector registers no file actions of its own, so exactly one handler is live in the Files UI.
**Switching deletes the settings written for the previous backend and disables its connector**, so a connector is never left pointed at a service that has since been uninstalled, and the two are never enabled at once — which is the state that stops Word, Excel and PowerPoint opening in either.

**It installs the connector app, once, on a change of selection.** The `office-suite` oneshot runs `occ app:install` for **Nextcloud Office (Collabora)** or **ONLYOFFICE** when that app is absent. Because it sits behind the signature check it fires only when the selection changes, never on an ordinary start — so an app the user later removes stays removed, and the health check reports it rather than the package silently putting it back.
**It installs and enables the connector app on a change of selection.** The `office-suite` oneshot runs `occ app:install` for **Nextcloud Office (Collabora)** or **ONLYOFFICE** when that app is absent, and `occ app:enable` when it is present but switched off. Because it sits behind the signature check it fires only when the selection changes, never on an ordinary start — so an app the user later removes or disables stays that way, and the health check reports it rather than the package silently putting it back. Neither command overrides Nextcloud's own compatibility check, so an app with no release for the running major version is refused rather than force-enabled.

**It never re-enables a disabled connector.** An app that is present but switched off is either the user's decision or a major Nextcloud upgrade disabling one with no compatible release; re-enabling it is how that protection gets undone, and it is what `Disable Non-default Apps` exists to recover from. That state falls through to the health check instead.
**The reconcile waits for the document server to report healthy before it touches anything.** The `office-suite` oneshot watches the chosen service's status and blocks until its own health check passes — `cool` for Collabora, `documentserver` for ONLYOFFICE. That check fetches the same endpoint this package depends on, so passing it means the work below can succeed rather than merely that something is listening. It matters most for `richdocuments:activate-config`, which refreshes the cached discovery document by fetching it: a fetch that fails partway leaves the app holding a WOPI url with no discovery behind it, and every document then opens to a spinner that never resolves.

The wait is a subscription, not a poll. It runs no commands, cannot fail, and releases the moment the service becomes ready — a few tens of seconds into an ordinary start, or whenever the user installs the service if they selected it first. A bridge address is not a usable readiness signal here: the port is bound, and the address therefore resolves, well before `coolwsd` accepts its first connection.

**The `trusted_domains` entry is load-bearing.** A document server fetches and saves files over the host bridge, and without that entry Nextcloud answers every one of those requests with `Trusted domain error` — the editor opens and then fails to load the document. Nextcloud matches on the host alone, so the bare IP covers whatever port the binding was assigned.

Expand Down Expand Up @@ -245,7 +247,7 @@ A web-interface failure after the grace period is Nextcloud itself: an app that

The transient checks — Recognize Model Download, Memories Indexing, Memories Map Setup, File Scan, Repair — exist only while their task is pending, and report `loading` with a progress message throughout.

**Office Connector** (`office-connectors`) — present only while an office suite is selected. It reads Nextcloud's enabled-app list and fails in two distinct cases, each with its own instruction.
**Office Connector** (`office-connectors`) — present only while an office suite is selected. It reads Nextcloud's enabled-app list and fails in two distinct cases, each with its own instruction. Each poll boots PHP to read that list, so it runs every two minutes while passing and every fifteen seconds while failing. That ceiling is also how long a connector someone has just switched off keeps reading as ready.

**The selected suite's connector is not enabled.** Either it was never installed, or it has been removed or switched off since. The message says *Install* or *Enable* accordingly — telling someone to install what they already have is how a message stops being read — and names the other way out: selecting `None` in the Office Suite action. Without this the failure is silent: the reconcile simply waits, the document server runs, and nothing in Nextcloud opens in it.

Expand Down Expand Up @@ -316,7 +318,7 @@ dependencies:
- filebrowser # optional, exists; only while selected as an external-storage source
- coturn # optional, running, no health checks; only while Talk call relaying is on
- collabora-online # optional, running, health check `cool`; only while selected as the office suite
- onlyoffice-docs # optional, running, health check `documentserver`; only while selected as the office suite
- onlyoffice-docs # optional, running, health check `documentserver`; only while selected as the office suite; Community Registry
interfaces:
ui: { type: ui, port: 80 }
webdav: { type: api, port: 80 } # same binding, path /remote.php/dav/
Expand Down
2 changes: 1 addition & 1 deletion instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Nextcloud on its own shows your documents but cannot edit them. To edit in the b
1. Install **Collabora Online**, unless you have a specific reason for the other one — see below. ONLYOFFICE Docs is in the Community Registry, so you would need that registry added to your server to find it.
2. Run the **Office Suite** action here and pick the one you installed. Nextcloud installs the app it needs for it — **Nextcloud Office (Collabora)** or **ONLYOFFICE** — and points itself at the service.

Keep only one of those apps enabled. With both on, Nextcloud stops opening Word, Excel and PowerPoint files in either, and this service's page will name the one to disable. It will also tell you if the app is ever removed or switched off while a document server is still selected.
Switching between the two later is the same step: pick the other one, and Nextcloud sets up its app and switches the previous one off. Only one of them may be enabled — with both on, Nextcloud stops opening Word, Excel and PowerPoint files in either. If you ever remove or switch off the app yourself, this service's page will say so and name the one to turn back on.

Documents, spreadsheets and presentations then open in the browser from Files, and several people can edit the same file at once. The editor is served from your Nextcloud address, so it works the same on your local network, a public domain, or Tor.

Expand Down
3 changes: 3 additions & 0 deletions nextcloud.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ RUN printf '%s\n' \
'</Directory>' \
> /etc/apache2/conf-available/startos-dav-redirects.conf \
&& a2enconf startos-dav-redirects

# For the office-suite proxy the package writes into conf-enabled at daemon start.
RUN a2enmod proxy proxy_http proxy_wstunnel substitute
8 changes: 5 additions & 3 deletions startos/actions/setOfficeSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ export const inputSpec = InputSpec.of({
office_suite: Value.select({
name: i18n('Office Suite'),
description: i18n(
'Which document server opens documents, spreadsheets and presentations in your browser. Collabora Online is recommended: it needs about a quarter of the memory and opens more formats. ONLYOFFICE Docs, from the Community Registry, is worth its size only if you shuttle a large body of style-heavy documents back and forth with Microsoft Office. Install the service first, then its Nextcloud app — Nextcloud Office for Collabora, ONLYOFFICE for ONLYOFFICE Docs — and enable only that one.',
'Which document server opens documents, spreadsheets and presentations in your browser. Install it from the Marketplace first, then pick it here — Nextcloud installs the app it needs and points itself at the service. Collabora Online is recommended: it needs about a quarter of the memory and opens more formats. ONLYOFFICE Docs, from the Community Registry, is worth its size only if you shuttle a large body of style-heavy documents back and forth with Microsoft Office.',
),
default: 'none',
values: {
none: i18n('None'),
collabora: 'Collabora Online (recommended)',
collabora: i18n('Collabora Online (recommended)'),
onlyoffice: 'ONLYOFFICE Docs',
},
}),
Expand Down Expand Up @@ -43,7 +43,9 @@ export const setOfficeSuite = sdk.Action.withInput(
async ({ effects }) => {
const officeSuite = await storeJson.read((s) => s.officeSuite).once()
return {
office_suite: isOfficeSuite(officeSuite) ? officeSuite : ('none' as const),
office_suite: isOfficeSuite(officeSuite)
? officeSuite
: ('none' as const),
}
},

Expand Down
3 changes: 2 additions & 1 deletion startos/dependencies.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { T } from '@start9labs/start-sdk'
import { externalStorageMeta } from './externalStorage'
import { manifest } from './manifest'
import { storeJson } from './fileModels/store.json'
import { sdk } from './sdk'
import { officeSuiteMeta } from './officeSuite'
Expand All @@ -11,7 +12,7 @@ export const setDependencies = sdk.setupDependencies(async ({ effects }) => {
const talkTurn = await storeJson.read((s) => s.talkTurn).const(effects)
const officeSuite = await storeJson.read((s) => s.officeSuite).const(effects)

const deps: T.CurrentDependenciesResult<any> = {}
const deps: T.CurrentDependenciesResult<typeof manifest> = {}

// Only require a source while it's selected. `exists` (not `running`) — we
// only need the source's volume present on disk to mount and read/write it.
Expand Down
3 changes: 2 additions & 1 deletion startos/fileModels/store.json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ const shape = z.object({
// `externalStoragesConfigured` above.
talkTurnConfigured: z.string().catch('').default(''),
// Which document server serves the office editors, if any. DESIRED state:
// written by the Configure action, read reactively in setupMain/setDependencies.
// written by the Office Suite action, read reactively in
// setupMain/setDependencies.
officeSuite: z.enum(['collabora', 'onlyoffice']).optional().catch(undefined),
// ACTUAL state: an opaque signature of the office settings this package last
// applied, and the record of what to clear on the next change. Same
Expand Down
Loading