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
29 changes: 23 additions & 6 deletions openspec/specs/app-icon-management/spec.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# app-icon-management Specification

## Purpose
TBD - created by archiving change openbuilt-nextcloud-nav. Update Purpose after archive.

Lets an operator brand each published OpenBuilt virtual app with per-app SVG icons
(light + dark) so the published app surfaces with its own identity in the Nextcloud
top bar and in OpenBuilt's own card grid. Adds top-level `icon` / `iconDark` ref
fields to the `Application` schema (sibling to `slug`, `name`, `manifest`,
`permissions`), thin icon-serving endpoints with a clear fallback chain, and the
upload / preview / remove UX on the Application detail page — all routed through
OR's existing files-attached-to-object mechanism (ADR-001) so no new openbuilt-side
file storage is introduced.

## Requirements
### Requirement: REQ-OBICON-001 Icon fields on Application schema (top-level)

### Requirement: Icon fields on Application schema (top-level)

The `Application` schema in `lib/Settings/openbuilt_register.json` SHALL declare two optional
top-level properties — `icon` and `iconDark` — as siblings to `slug`, `name`, `manifest`,
Expand All @@ -12,6 +22,8 @@ top-level properties — `icon` and `iconDark` — as siblings to `slug`, `name`
files-attached-to-object mechanism (ADR-001). Both fields SHALL be optional; omitting them
SHALL NOT cause schema validation failure.

**ID:** REQ-OBICON-001

Icons live outside the `manifest` object deliberately: they are openbuilt-side admin
metadata, not part of the manifest the citizen developer designs and the runtime serves
to `CnAppRoot`. This keeps the change orthogonal to `app-manifest.schema.json` and avoids
Expand All @@ -35,7 +47,7 @@ any upstream coupling with `@conduction/nextcloud-vue`.
`ref` key)
- **THEN** OR returns a 4xx validation error indicating `icon.ref` is required

### Requirement: REQ-OBICON-002 Icon-serving endpoint (light)
### Requirement: Icon-serving endpoint (light)

The system SHALL expose `GET /index.php/apps/openbuilt/icons/{slug}.svg` backed by
`IconController::iconLight`. The endpoint SHALL:
Expand All @@ -49,6 +61,8 @@ The system SHALL expose `GET /index.php/apps/openbuilt/icons/{slug}.svg` backed
4. Set `Cache-Control: public, max-age=60` on every successful response.
5. Require any valid NC session (`#[NoAdminRequired]`); return `401` when no session exists.

**ID:** REQ-OBICON-002

#### Scenario: Endpoint returns the attached light icon

- **WHEN** an authenticated user requests `/icons/hello-world.svg`
Expand All @@ -69,7 +83,7 @@ The system SHALL expose `GET /index.php/apps/openbuilt/icons/{slug}.svg` backed
- **WHEN** a request arrives at `/icons/{slug}.svg` with no NC session cookie or token
- **THEN** the response is `401`

### Requirement: REQ-OBICON-003 Icon-serving endpoint (dark)
### Requirement: Icon-serving endpoint (dark)

The system SHALL expose `GET /index.php/apps/openbuilt/icons/{slug}-dark.svg` backed by
`IconController::iconDark`. The endpoint SHALL apply the following fallback chain in order:
Expand All @@ -81,6 +95,8 @@ The system SHALL expose `GET /index.php/apps/openbuilt/icons/{slug}-dark.svg` ba

Cache and auth posture SHALL be identical to REQ-OBICON-002.

**ID:** REQ-OBICON-003

#### Scenario: Endpoint returns the attached dark icon

- **WHEN** an authenticated user requests `/icons/hello-world-dark.svg`
Expand All @@ -102,7 +118,7 @@ Cache and auth posture SHALL be identical to REQ-OBICON-002.
- **THEN** the response is `200 image/svg+xml` containing the contents of
OpenBuilt's `/img/app-dark.svg`

### Requirement: REQ-OBICON-004 Icon section on Application detail page
### Requirement: Icon section on Application detail page

The Application detail page SHALL include an **Icon** section exposing:

Expand All @@ -120,6 +136,8 @@ The Application detail page SHALL include an **Icon** section exposing:
The section SHALL NOT introduce a new openbuilt-side file-storage mechanism; all file I/O
goes through OR's existing files-attached-to-object endpoint (ADR-001).

**ID:** REQ-OBICON-004

#### Scenario: User uploads a light icon

- **WHEN** a user with editor or owner role selects an SVG file in the light-icon picker
Expand All @@ -138,4 +156,3 @@ goes through OR's existing files-attached-to-object endpoint (ADR-001).

- **WHEN** a user attempts to upload a file with a non-`.svg` extension in either icon slot
- **THEN** the uploader displays an inline error message and does not submit the file to OR

27 changes: 21 additions & 6 deletions openspec/specs/app-nav-entries/spec.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# app-nav-entries Specification

## Purpose
TBD - created by archiving change openbuilt-nextcloud-nav. Update Purpose after archive.

Makes every published OpenBuilt virtual app a first-class Nextcloud navigation citizen
by registering a per-app top-bar entry under `INavigationManager` on each request boot,
visibility-gated by the Application's `permissions` RBAC block (with a `group:*` wildcard
for universal visibility), and read live from OR with no writeback or cached nav-entry
table. Closes the gap that previously forced users to enter the OpenBuilt shell, find
the app, and click through to reach a published virtual app.

## Requirements
### Requirement: REQ-OBNAV-001 Dynamic per-app top-bar entry for each published Application

### Requirement: Dynamic per-app top-bar entry for each published Application

The system SHALL register one `INavigationManager` entry per published Application in
`Application::boot()` using `INavigationManager::add()` with a closure factory. Each entry
Expand All @@ -20,6 +28,8 @@ SHALL carry:
The entries SHALL be registered by `AppNavigationService`, which is lazily resolved from the
DI container inside the `boot()` method.

**ID:** REQ-OBNAV-001

#### Scenario: Published app appears in the Nextcloud top bar

- **WHEN** the Nextcloud request cycle boots after an Application is transitioned to `published`
Expand All @@ -37,7 +47,7 @@ DI container inside the `boot()` method.
- **WHEN** an Application has `status: archived`
- **THEN** no nav entry with `id = "openbuilt-app-{slug}"` appears for any user

### Requirement: REQ-OBNAV-002 Nav entry gated by permissions RBAC
### Requirement: Nav entry gated by permissions RBAC

Each nav entry's visibility closure SHALL resolve the signed-in user's UID and group
memberships via `IUserSession` and `IGroupManager` and return `true` only when the user
Expand All @@ -53,6 +63,8 @@ satisfies at least one of the following:
An Application whose `permissions.owners`, `permissions.editors`, and `permissions.viewers`
are all empty (or absent) SHALL NOT be visible to non-admin users, regardless of status.

**ID:** REQ-OBNAV-002

#### Scenario: Owner-role user sees the nav entry

- **WHEN** user `alice` has UID `alice` and the Application has
Expand All @@ -77,13 +89,15 @@ are all empty (or absent) SHALL NOT be visible to non-admin users, regardless of
- **AND** the Application is published with empty permissions
- **THEN** the admin's request cycle includes the nav entry

### Requirement: REQ-OBNAV-003 group-wildcard nav-entry visibility SHALL apply to all signed-in users
### Requirement: group-wildcard nav-entry visibility SHALL apply to all signed-in users

The system SHALL make the nav entry visible to every signed-in Nextcloud user, regardless of
their group memberships, when the literal string `group:*` appears in any of
`permissions.owners`, `permissions.editors`, or `permissions.viewers` on a published
Application. The wildcard SHALL be detected before the group-intersection check runs.

**ID:** REQ-OBNAV-003

#### Scenario: `group:*` in owners makes entry universally visible

- **WHEN** the Application has `permissions.owners = ["group:*"]`
Expand All @@ -96,13 +110,15 @@ Application. The wildcard SHALL be detected before the group-intersection check
- **AND** an arbitrary signed-in user with no matching group memberships requests a page
- **THEN** that user's request cycle includes the nav entry

### Requirement: REQ-OBNAV-004 Nav entry list is re-evaluated per request without writeback
### Requirement: Nav entry list is re-evaluated per request without writeback

The set of published Applications SHALL be read from OR on each boot-cycle evaluation inside
`AppNavigationService`. No writeback to a separate nav-entry table or a cached register SHALL
occur. The update from draft to published (or published to archived) is detected automatically
because the service re-queries the `status == published` filter on every request boot cycle.

**ID:** REQ-OBNAV-004

#### Scenario: Transitioning an Application to archived removes its nav entry

- **WHEN** an Application is transitioned from `published` to `archived`
Expand All @@ -115,4 +131,3 @@ because the service re-queries the `status == published` filter on every request
- **WHEN** an Application is transitioned from `draft` to `published`
- **THEN** on the next Nextcloud request boot cycle, the nav entry for that Application is
present in `INavigationManager::getAll()` for eligible users

58 changes: 45 additions & 13 deletions openspec/specs/application-creation-wizard/spec.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
# application-creation-wizard Specification

## Purpose
TBD - created by archiving change openbuilt-app-creation-wizard. Update Purpose after archive.

Replaces the legacy single-form "Add Application" dialog with a four-step wizard
that provisions the full ADR-002 chain in one atomic backend call: an `Application`
row + N `ApplicationVersion` rows + N per-version registers (named
`openbuilt-{appSlug}-{versionSlug}`), each pre-seeded with the default `hello-message`
schema and the default manifest. Supports `single | dev-prod | dev-staging-prod |
custom` presets, enforces unique slugs per chain (leading-`_` reserved for openbuilt
system use), provides full rollback on any provisioning failure, sets the caller as
sole owner, and retires install-time auto-seed (`SeedHelloWorld` does not return) —
fresh installs are empty until the admin runs the wizard.

## Requirements
### Requirement: REQ-OBWIZ-001 Wizard replaces the legacy Add-Application entry point

### Requirement: Wizard replaces the legacy Add-Application entry point

The Virtual apps index SHALL open the four-step `CreateApplicationWizard` dialog when the
admin clicks the "Add Application" button. The legacy single-form Add-Application dialog
SHALL be removed in the same change. No fallback / feature-flagged escape hatch SHALL
exist.

**ID:** REQ-OBWIZ-001

#### Scenario: Clicking Add Application opens the wizard

- **WHEN** the admin clicks the "Add Application" button on the Virtual apps index
- **THEN** the `CreateApplicationWizard` `NcModal` opens at step 1 (Basics)
- **AND** no other dialog opens; the legacy single-form Add dialog is absent from the bundle

### Requirement: REQ-OBWIZ-002 Four-step wizard shape
### Requirement: Four-step wizard shape

The wizard SHALL consist of four steps in fixed order: (1) **Basics** — name, slug, description, optional light + dark icon upload; (2) **Preset** — radio cards for `single`, `dev-prod`, `dev-staging-prod`, `custom`; (3) **Custom chain** — admin-defined version list, shown ONLY when preset is `custom`; (4) **Review** — read-only summary + Create button.

Selecting any preset other than `custom` SHALL skip step 3 and jump straight to step 4.

**ID:** REQ-OBWIZ-002

#### Scenario: Selecting a canned preset skips the custom step

- **WHEN** the admin selects the `dev-prod` preset in step 2 and clicks Next
Expand All @@ -40,7 +55,7 @@ Selecting any preset other than `custom` SHALL skip step 3 and jump straight to
- **THEN** the previously-entered name, slug, and description from step 1 are still in place
- **AND** the previously-selected preset is still highlighted

### Requirement: REQ-OBWIZ-003 Preset shapes
### Requirement: Preset shapes

Each preset SHALL produce a deterministic version chain when reviewed and submitted:

Expand All @@ -51,6 +66,8 @@ Each preset SHALL produce a deterministic version chain when reviewed and submit
| `dev-staging-prod` | `development → staging → production` | `production` |
| `custom` | Admin-defined in step 3 | Terminal (bottom) row |

**ID:** REQ-OBWIZ-003

#### Scenario: dev-staging-prod preset produces a three-version chain

- **WHEN** the admin completes the wizard with preset `dev-staging-prod`, app name `My App`,
Expand All @@ -69,7 +86,7 @@ Each preset SHALL produce a deterministic version chain when reviewed and submit
- **AND** the Application's `productionVersion` points at it
- **AND** the version's `promotesTo` is null

### Requirement: REQ-OBWIZ-004 Custom-chain composer
### Requirement: Custom-chain composer

When the admin selects the `custom` preset, step 3 SHALL present an add-row list where each
row carries a `name` text input and an auto-derived `slug` chip. The composer SHALL support
Expand All @@ -78,6 +95,8 @@ adding rows (`+ Add version` button), removing rows (`×` per row), and reorderi
SHALL be interpreted as upstream-to-downstream. The composer SHALL enforce a minimum of one
row.

**ID:** REQ-OBWIZ-004

#### Scenario: Admin composes a 3-version chain by adding rows

- **WHEN** the admin starts on step 3 (one default row `Production`)
Expand All @@ -93,12 +112,14 @@ row.
- **AND** clicks `×` on the `Production` row
- **THEN** the row is NOT removed; an inline error appears: "At least one version is required"

### Requirement: REQ-OBWIZ-005 Slug derivation + leading-underscore rejection
### Requirement: Slug derivation + leading-underscore rejection

The wizard SHALL auto-derive slugs from names client-side via a `toKebabCase` function: lowercase the input, replace spaces with `-`, strip characters outside `[a-z0-9-]`, collapse double `--`, trim leading/trailing `-`. The derived slug SHALL be displayed as an editable chip with an `Advanced` toggle that reveals the slug input.

The slug pattern (enforced both client-side and server-side) SHALL be `^(?!_)[a-z0-9][a-z0-9-]*[a-z0-9]$`. Leading underscores SHALL be rejected with the user-facing message: "Version slugs cannot start with `_` (reserved for openbuilt system use)."

**ID:** REQ-OBWIZ-005

#### Scenario: Slug auto-derives from app name

- **WHEN** the admin types `My Cool App` in step 1's name field
Expand All @@ -118,14 +139,16 @@ The slug pattern (enforced both client-side and server-side) SHALL be `^(?!_)[a-
- **THEN** an inline error appears: "Slug must match `^(?!_)[a-z0-9][a-z0-9-]*[a-z0-9]$` —
lowercase letters, digits, and hyphens only"

### Requirement: REQ-OBWIZ-006 No duplicate version slugs within a chain
### Requirement: No duplicate version slugs within a chain

Within a single app's chain, two `ApplicationVersion` rows SHALL NOT share a slug. The
wizard SHALL enforce this client-side as the admin types (inline error on the duplicating
row) and server-side at the `/api/applications/wizard` endpoint (the endpoint rejects the
whole payload with `422 Unprocessable Entity` and a JSON body identifying both colliding
rows).

**ID:** REQ-OBWIZ-006

#### Scenario: Client-side duplicate-slug error

- **WHEN** the admin's chain in step 3 contains two rows both named `Staging` (auto-derived
Expand All @@ -149,14 +172,16 @@ rows).
- **THEN** the wizard accepts the payload and creates `app-b`'s `production` version without error
- **AND** `openbuilt-app-a-production` and `openbuilt-app-b-production` registers coexist

### Requirement: REQ-OBWIZ-007 Atomic creation with full rollback on failure
### Requirement: Atomic creation with full rollback on failure

The wizard's backend endpoint SHALL provision the full chain atomically by sequencing:
(1) validate payload, (2) create `Application`, (3) for each version create
`ApplicationVersion` + provision per-version register `openbuilt-{appSlug}-{versionSlug}`,
(4) wire each non-terminal version's `promotesTo` to the next downstream UUID, (5) set
`Application.productionVersion` to the terminal version's UUID.

**ID:** REQ-OBWIZ-007

On ANY failure at any step, the endpoint SHALL roll back every successfully-created object
in reverse creation order (registers first, then ApplicationVersion rows, then Application
row), then return `500` with body
Expand Down Expand Up @@ -193,13 +218,15 @@ so the admin can resolve manually.
`orphanedResources: ["openbuilt-<slug>-development"]`
- **AND** the message body advises the admin to remove the orphaned register manually

### Requirement: REQ-OBWIZ-008 Per-version registers + seed schema set
### Requirement: Per-version registers + seed schema set

For each `ApplicationVersion` row created by the wizard, a corresponding OR register SHALL
be provisioned with the name `openbuilt-{appSlug}-{versionSlug}`. Each freshly-provisioned
register SHALL have the default schema set (the single `hello-message` schema from
`lib/Resources/wizard/default-schemas.json`) installed and zero objects in it.

**ID:** REQ-OBWIZ-008

#### Scenario: Each version gets its own register with the seed schema

- **WHEN** the wizard successfully creates an app `helloworld` with preset `dev-prod`
Expand All @@ -208,7 +235,7 @@ register SHALL have the default schema set (the single `hello-message` schema fr
- **AND** each register has exactly one schema named `hello-message`
- **AND** each register has zero objects

### Requirement: REQ-OBWIZ-009 Initial manifest, semver, status per version
### Requirement: Initial manifest, semver, status per version

Each freshly-created `ApplicationVersion` row SHALL carry:
- `manifest` — copy of `lib/Resources/wizard/default-manifest.json` with the per-version
Expand All @@ -218,6 +245,8 @@ Each freshly-created `ApplicationVersion` row SHALL carry:
- `status` — `draft`.
- `application` relation — the new Application's UUID.

**ID:** REQ-OBWIZ-009

#### Scenario: Versions start with the default Hello-World manifest

- **WHEN** the wizard creates an app with preset `single` and slug `hello-world`
Expand All @@ -226,29 +255,32 @@ Each freshly-created `ApplicationVersion` row SHALL carry:
- **AND** the version's `semver` is `0.1.0`
- **AND** the version's `status` is `draft`

### Requirement: REQ-OBWIZ-010 Caller becomes sole owner
### Requirement: Caller becomes sole owner

The wizard endpoint SHALL set the new Application's `permissions.owners` to a single-element
array containing the calling user's UID (in `user:<uid>` form). `permissions.editors` and
`permissions.viewers` SHALL be empty arrays. The admin grants further roles via the
permissions editor post-creation.

**ID:** REQ-OBWIZ-010

#### Scenario: Caller becomes owner; no other principals

- **WHEN** user `admin` POSTs a valid wizard payload
- **THEN** the new Application's `permissions.owners` is `["user:admin"]`
- **AND** `permissions.editors` is `[]`
- **AND** `permissions.viewers` is `[]`

### Requirement: REQ-OBWIZ-011 No install-time auto-seed
### Requirement: No install-time auto-seed

The openbuilt app SHALL NOT create any virtual app at install / upgrade time. After
`occ maintenance:repair`, the Virtual apps index SHALL be empty for a fresh install.

**ID:** REQ-OBWIZ-011

#### Scenario: Fresh install has no virtual apps until admin creates one

- **WHEN** openbuilt is installed on a fresh Nextcloud (no prior virtual apps)
- **AND** `occ maintenance:repair` has run
- **THEN** the Virtual apps index page shows the empty state
- **AND** the Add Application button (opening the wizard) is the only call-to-action

Loading
Loading