Browse Source

Video Editor Cline Update (#270)

* Add Cine Studio, a Premiere-like timeline video editor webapp

A new dark macOS-style video editor under src/web/Cine Studio with:

- Media bin importing from the ArozOS file system (file selector +
  File Manager drag-drop) and from the local device (uploaded to
  user:/Cine Studio/Media so projects stay portable), with probed
  durations, filmstrip thumbnails and decoded waveform peaks
- Multi-track timeline (video/audio) with drag-move, edge trimming,
  blade splitting, snapping, track visibility/mute toggles, zoomable
  timecode ruler and undo/redo history
- Canvas compositing preview player with per-clip transform, crop
  mode, opacity, color controls (exposure/contrast/saturation/presets)
  and a WebAudio mix bus for synced audio playback
- .cine project format (open/save/save-as via ao_module file selector,
  registered as a file association with its own icon)
- Export pipeline: real-time MediaRecorder render to WebM uploaded to
  the ArozOS storage, with optional server-side MP4 conversion through
  the AGI ffmpeg library when the host has ffmpeg installed

Backend AGI scripts create the per-user app folders and expose the
ffmpeg availability check / conversion / cleanup actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Remove macOS traffic lights and implement all stubbed Cine Studio features

Top bar: drop the decorative macOS traffic-light buttons.

Effects (new js/effects.js): per-clip effect stacks (Black & White,
Sepia, Invert, Hue Shift, Blur, Pixelate, Mirror, Vignette, Film Grain,
Fade In/Out) applied by the compositor so preview and export match.
Fades also ramp audio-clip volume. Gallery panel with rendered preview
cards plus an inspector Effects tab with amount sliders, add/remove and
an fx badge on timeline clips.

Titles and Elements (new js/titles.js): generated clips with no backing
media - text titles (content, size, color, weight, align, vertical
position, plain/lower-third/caption styles) and solid/gradient color
boards. They render through a cached offscreen frame so transforms,
color controls, effects and transitions all apply. Inserted from the
Titles/Elements panels, the T tool or the T key onto a free video track
(auto-created when needed).

Transitions (new js/transitions.js): Cross Dissolve, Fade and Wipe
stored on the incoming clip with adjustable duration; the previous
clip's last frame is frozen and blended, with a from-black fallback and
a marker on the clip. Applied from the gallery or the inspector
Transition tab.

Also implemented: Filters panel applying color-preset looks, Libraries
panel listing saved .cine projects (new backend/listprojects.js), media
bin list view toggle, and crop/audio toolbar buttons that jump to the
matching inspector section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Add preview direct manipulation, auto track creation and .pxs/.asproj import

Preview (new js/previewctl.js): an overlay canvas on the preview lets
the user click a clip to select it, drag it to reposition and pull a
corner handle to scale it - rotation aware, works for video, image,
title and color clips, with a selection outline and handles drawn over
the frame. Timeline drags now listen on the window because Chrome drops
element pointer capture when the clip is reparented across lanes.

Unlimited tracks: dragging a clip past the outermost lanes creates a
new video/audio track on drop, and dropping bin media onto the empty
timeline area below the lanes does the same, in addition to the
existing add-track button.

Pixel Studio import (.pxs): the layered project is flattened in the
browser - raster layers honoring visibility, opacity and blend mode,
text layers re-rendered from their text data - and used as image media.

Audio Studio import (.asproj): prefers an embedded or referenced
rendered mixdown; otherwise decodes the project's track/clip references
(tolerant field names) and mixes them with an OfflineAudioContext into
a WAV. Both importers keep the original virtual path in the project
file and re-composite when a saved project is reopened.

Both extensions join the import filters, the local-device picker and
the module's SupportedExt list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Add the most critical everyday editing features to Cine Studio

Safety and workflow:
- Auto-save (20s, localStorage) with crash-recovery prompt on launch,
  cleared once the project is saved to disk (new js/session.js)
- Recent projects list in the project menu (dedup, last 8)

Editing fundamentals:
- Multi-select (shift-click) with group move, group delete, group copy
- Copy / paste at playhead / duplicate clips (Ctrl+C / V / D)
- Ripple delete closing the gap (Shift+Del, context menu)
- Timeline markers: add/remove at playhead (M), flags on the ruler,
  jump to next (Shift+M), saved in the project, act as snap targets
- Snapping on/off toggle (magnet button)
- Clip speed 25-400%: timeline length, element sync, trimming and
  splitting are all speed aware; audio pitch follows rate

Compositing and playback:
- Blend modes (multiply, screen, overlay, add, soft light, difference)
- Flip horizontal / vertical toggles
- JKL shuttle (reverse scrub / pause / forward up to 8x) and loop
  playback toggle; export temporarily disables looping

Output and audio:
- Export current frame as PNG from the export menu
- Detach audio: video clip audio moves to its own audio-track clip
- Audio track solo via the track header context menu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Add Cine Studio, a Premiere-like timeline video editor webapp

A new dark macOS-style video editor under src/web/Cine Studio with:

- Media bin importing from the ArozOS file system (file selector +
  File Manager drag-drop) and from the local device (uploaded to
  user:/Cine Studio/Media so projects stay portable), with probed
  durations, filmstrip thumbnails and decoded waveform peaks
- Multi-track timeline (video/audio) with drag-move, edge trimming,
  blade splitting, snapping, track visibility/mute toggles, zoomable
  timecode ruler and undo/redo history
- Canvas compositing preview player with per-clip transform, crop
  mode, opacity, color controls (exposure/contrast/saturation/presets)
  and a WebAudio mix bus for synced audio playback
- .cine project format (open/save/save-as via ao_module file selector,
  registered as a file association with its own icon)
- Export pipeline: real-time MediaRecorder render to WebM uploaded to
  the ArozOS storage, with optional server-side MP4 conversion through
  the AGI ffmpeg library when the host has ffmpeg installed

Backend AGI scripts create the per-user app folders and expose the
ffmpeg availability check / conversion / cleanup actions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Remove macOS traffic lights and implement all stubbed Cine Studio features

Top bar: drop the decorative macOS traffic-light buttons.

Effects (new js/effects.js): per-clip effect stacks (Black & White,
Sepia, Invert, Hue Shift, Blur, Pixelate, Mirror, Vignette, Film Grain,
Fade In/Out) applied by the compositor so preview and export match.
Fades also ramp audio-clip volume. Gallery panel with rendered preview
cards plus an inspector Effects tab with amount sliders, add/remove and
an fx badge on timeline clips.

Titles and Elements (new js/titles.js): generated clips with no backing
media - text titles (content, size, color, weight, align, vertical
position, plain/lower-third/caption styles) and solid/gradient color
boards. They render through a cached offscreen frame so transforms,
color controls, effects and transitions all apply. Inserted from the
Titles/Elements panels, the T tool or the T key onto a free video track
(auto-created when needed).

Transitions (new js/transitions.js): Cross Dissolve, Fade and Wipe
stored on the incoming clip with adjustable duration; the previous
clip's last frame is frozen and blended, with a from-black fallback and
a marker on the clip. Applied from the gallery or the inspector
Transition tab.

Also implemented: Filters panel applying color-preset looks, Libraries
panel listing saved .cine projects (new backend/listprojects.js), media
bin list view toggle, and crop/audio toolbar buttons that jump to the
matching inspector section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Add preview direct manipulation, auto track creation and .pxs/.asproj import

Preview (new js/previewctl.js): an overlay canvas on the preview lets
the user click a clip to select it, drag it to reposition and pull a
corner handle to scale it - rotation aware, works for video, image,
title and color clips, with a selection outline and handles drawn over
the frame. Timeline drags now listen on the window because Chrome drops
element pointer capture when the clip is reparented across lanes.

Unlimited tracks: dragging a clip past the outermost lanes creates a
new video/audio track on drop, and dropping bin media onto the empty
timeline area below the lanes does the same, in addition to the
existing add-track button.

Pixel Studio import (.pxs): the layered project is flattened in the
browser - raster layers honoring visibility, opacity and blend mode,
text layers re-rendered from their text data - and used as image media.

Audio Studio import (.asproj): prefers an embedded or referenced
rendered mixdown; otherwise decodes the project's track/clip references
(tolerant field names) and mixes them with an OfflineAudioContext into
a WAV. Both importers keep the original virtual path in the project
file and re-composite when a saved project is reopened.

Both extensions join the import filters, the local-device picker and
the module's SupportedExt list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Add the most critical everyday editing features to Cine Studio

Safety and workflow:
- Auto-save (20s, localStorage) with crash-recovery prompt on launch,
  cleared once the project is saved to disk (new js/session.js)
- Recent projects list in the project menu (dedup, last 8)

Editing fundamentals:
- Multi-select (shift-click) with group move, group delete, group copy
- Copy / paste at playhead / duplicate clips (Ctrl+C / V / D)
- Ripple delete closing the gap (Shift+Del, context menu)
- Timeline markers: add/remove at playhead (M), flags on the ruler,
  jump to next (Shift+M), saved in the project, act as snap targets
- Snapping on/off toggle (magnet button)
- Clip speed 25-400%: timeline length, element sync, trimming and
  splitting are all speed aware; audio pitch follows rate

Compositing and playback:
- Blend modes (multiply, screen, overlay, add, soft light, difference)
- Flip horizontal / vertical toggles
- JKL shuttle (reverse scrub / pause / forward up to 8x) and loop
  playback toggle; export temporarily disables looping

Output and audio:
- Export current frame as PNG from the export menu
- Detach audio: video clip audio moves to its own audio-track clip
- Audio track solo via the track header context menu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

* Add Playwright E2E suite for Cine Studio + GitHub Actions job

Ports the browser-driven tests used to verify Cine Studio into the
codebase under test/e2e/playwright (outside src/ so they never touch
the Go module). The tests drive the real app in headless Chromium,
generating their own media in-page (canvas -> MediaRecorder WebM,
synthesized WAV) and asserting on rendered pixels, so they exercise the
full compositor / playback / export pipeline rather than mocking it.

Structure:
- lib/static-server.js  minimal static server for the ArozOS web root
- lib/harness.js        browser launch + app navigation + ok()/fail();
                        resolves Playwright's own Chromium (no hardcoded
                        path), overridable via PW_CHROMIUM_PATH
- run.js                serves src/web, runs each spec in its own process,
                        exits non-zero on any failure
- specs/functional.js   media probe, playback, edit, round-trip, WebM export
- specs/features.js     effects, titles, transitions, elements, filters
- specs/interaction.js  preview drag/resize, auto tracks, .pxs/.asproj import
- specs/editing.js      multi-select, copy/paste, speed, JKL, markers, autosave

41 assertions across the four specs, all passing locally.

CI: .github/workflows/e2e-playwright.yml installs Node + Playwright
Chromium and runs `npm test`, scoped to changes under the app or the
harness so unrelated commits don't trigger it. Front-end only - runs
independently of the Go build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSxJni7aDMU5MFoWG2SSa4

---------

Co-authored-by: Claude <noreply@anthropic.com>
Alan Yeung 1 tháng trước cách đây
mục cha
commit
bd8c37f229

+ 7 - 28
.github/workflows/e2e-playwright.yml

@@ -1,22 +1,19 @@
-name: E2E (Playwright)
+name: E2E (Cine Studio)
 
-# Browser-driven end-to-end tests, in two suites:
-#   static  - Cine Studio front-end specs against a plain static server
-#   system  - full-stack critical-path specs (auth, desktop, file
-#             explorer, system settings, user management / permission
-#             control) against a real ArozOS server built from src/
-# The system suite exercises the Go core, so this workflow triggers on
-# any change to src/ as well as the test harness itself.
+# Browser-driven end-to-end tests for the Cine Studio WebApp. These are
+# front-end only (Node + Playwright + a static file server) and do not
+# touch the Go build, so they run as an independent job. Scoped to changes
+# under the app or the test harness to avoid running on unrelated commits.
 
 on:
   push:
     paths:
-      - "src/**"
+      - "src/web/Cine Studio/**"
       - "test/e2e/playwright/**"
       - ".github/workflows/e2e-playwright.yml"
   pull_request:
     paths:
-      - "src/**"
+      - "src/web/Cine Studio/**"
       - "test/e2e/playwright/**"
       - ".github/workflows/e2e-playwright.yml"
 
@@ -33,16 +30,6 @@ jobs:
       - name: Checkout
         uses: actions/checkout@v4
 
-      - name: Set up Go
-        uses: actions/setup-go@v5
-        with:
-          go-version-file: src/go.mod
-          cache-dependency-path: src/go.sum
-
-      - name: Build ArozOS server binary
-        working-directory: src
-        run: go build -o arozos .
-
       - name: Set up Node.js
         uses: actions/setup-node@v4
         with:
@@ -56,11 +43,3 @@ jobs:
 
       - name: Run E2E tests
         run: npm test
-
-      - name: Upload ArozOS server log on failure
-        if: failure()
-        uses: actions/upload-artifact@v4
-        with:
-          name: arozos-server-log
-          path: test/e2e/playwright/.instance/server.log
-          if-no-files-found: ignore

+ 0 - 1
test/e2e/playwright/.gitignore

@@ -2,4 +2,3 @@ node_modules/
 package-lock.json
 test-results/
 *.png
-.instance/

+ 28 - 123
test/e2e/playwright/README.md

@@ -1,152 +1,57 @@
-# ArozOS — end-to-end tests
+# Cine Studio — end-to-end tests
 
-Browser-driven Playwright tests for ArozOS, organized into two suites:
+Browser-driven Playwright tests for the **Cine Studio** WebApp
+(`src/web/Cine Studio`). They drive the real app in headless Chromium,
+generating their own media in-page (canvas → MediaRecorder WebM, synthesized
+WAV) and asserting on actual rendered pixels, so they exercise the full
+compositor/playback/export pipeline rather than mocking it.
 
-- **static** (`specs/`) — front-end-only specs for the **Cine Studio**
-  WebApp, served by a tiny static file server. No Go involved.
-- **system** (`specs-system/`) — full-stack critical-path specs driven
-  against a **real ArozOS server** (the Go binary built from `src/`),
-  covering sign in / sign out, the desktop shell, the file explorer,
-  system settings, and user management / permission control.
-
-Everything lives outside `src/` so it never interferes with the Go module.
+These tests are front-end only. They do **not** build or run any Go code and
+live outside `src/` so they never interfere with the Go module.
 
 ## Layout
 
 ```
 test/e2e/playwright/
-├── package.json          Playwright dependency + npm test scripts
-├── run.js                orchestrator: runs the static and system suites
+├── package.json          Playwright dependency + `npm test`
+├── run.js                orchestrator: serves src/web, runs each spec
 ├── lib/
 │   ├── static-server.js  minimal static server for the ArozOS web root
-│   ├── harness.js        Cine Studio helpers (browser launch, ok/fail)
-│   ├── arozos-server.js  boots a disposable real ArozOS instance
-│   └── system-harness.js login/API helpers for the system suite
-├── specs/                Cine Studio specs (static suite)
-│   ├── functional.js     media probe, playback, edit, round-trip, export
-│   ├── features.js       effects, titles, transitions, elements, filters
-│   ├── interaction.js    preview drag/resize, auto tracks, project import
-│   └── editing.js        multi-select, copy/paste, speed, JKL, markers…
-└── specs-system/         full-stack critical-path specs (system suite)
-    ├── 010-auth.js       login page, bad credentials, form login,
-    │                     session persistence, logout, gated redirects
-    ├── 020-desktop.js    desktop shell, start menu, module list,
-    │                     quick access panel, desktop sign-out
-    ├── 021-desktop-api.js  every /system/desktop endpoint: host, user
-    │                     (self/target/noicon), theme, preference, icon
-    │                     location, shortcut create/list/rename
-    ├── 022-desktop-ui.js  clock, wallpaper frame, panels, start-menu
-    │                     search filtering, notifications, launch icons
-    ├── 023-desktop-windows.js  float window lifecycle (launch, focus/
-    │                     z-order, max/restore, minimize, close) and
-    │                     wallpaper + icon context menus
-    ├── 030-file-explorer.js  File Manager UI + full file lifecycle:
-    │                     create/rename/copy/move/properties/trash, CSRF
-    ├── 035-file-transfer.js  upload / download round-trip, search,
-    │                     share-link lifecycle (create/list/public
-    │                     download/delete)
-    ├── 040-system-settings.js  System Setting UI + settings catalogue
-    ├── 055-account.js     account settings UI + password change (wrong
-    │                     old password refused, old password stops
-    │                     working, new password signs in)
-    ├── 050-users-permissions.js  group + user CRUD, module visibility,
-    │                     admin-only endpoint enforcement
-    ├── 060-webapps-core.js  WebApp wave 1: NotepadA (incl. real file
-    │                     open), Text, Photo, Music, Video, PDF Viewer,
-    │                     Zip File Manager
-    ├── 070-webapps-office.js  WebApp wave 2: Code Studio, MDEditor
-    │                     (incl. real file open), Calendar, Notes, Memo,
-    │                     Reminders, OfficeViewer, Dashboard
-    ├── 080-webapps-media.js  WebApp wave 3: Musicify, Movie, Manga,
-    │                     Paint, Pixel Studio, Audio Studio, Camera,
-    │                     Recorder, FFmpeg Factory
-    └── 090-webapps-utilities.js  WebApp wave 4: Calculator (incl. a
-                          real calculation), Clock, Browser, Speedtest,
-                          Web Downloader, Web Builder, SQLite Admin,
-                          Terminal, AGIForge, AIChat, OTPAuth,
-                          Productivity, OnScreenKeyboard, Arozcast,
-                          Management Gateway, UnitTest, CronDemo,
-                          Serverless
+│   └── harness.js        browser launch, app navigation, ok()/fail()
+└── specs/
+    ├── functional.js     media probe, playback, edit, round-trip, WebM export
+    ├── features.js       effects, titles, transitions, elements, filters
+    ├── interaction.js    preview drag/resize, auto tracks, .pxs/.asproj import
+    └── editing.js        multi-select, copy/paste, speed, JKL, markers, autosave…
 ```
 
-## WebApp coverage
-
-All 44 WebApps under `src/web` were inventoried (name, group, file
-associations) and ranked into four waves, now all covered:
-
-1. **Core daily drivers** *(`060-webapps-core.js`)* - NotepadA, Text,
-   Photo, Music, Video, PDF Viewer, Zip File Manager - the default
-   openers for everyday file types.
-2. **Office / productivity** *(`070-webapps-office.js`)* - Code Studio,
-   MDEditor, Calendar, Notes, Memo, Reminders, OfficeViewer, Dashboard.
-3. **Media / creative** *(`080-webapps-media.js`)* - Musicify, Movie,
-   Manga, Paint, Pixel Studio, Audio Studio, Camera, Recorder, FFmpeg
-   Factory. Cine Studio keeps its own deep static suite under `specs/`.
-4. **Utilities / dev / network** *(`090-webapps-utilities.js`)* -
-   Calculator, Clock, Browser, Speedtest, Web Downloader, Web Builder,
-   SQLite Admin, Terminal, AGIForge, AIChat, OTPAuth, Productivity,
-   OnScreenKeyboard, Arozcast, Management Gateway, UnitTest, CronDemo,
-   Serverless.
-
-The WebApp specs are load-and-render smoke tests (plus a few real
-interactions: opening files in NotepadA / MDEditor, a Calculator sum);
-they guard against apps that break outright. Deeper per-app behavioural
-coverage can grow inside each wave spec over time.
-
-## How the system suite works
-
-`lib/arozos-server.js` boots the real server in an isolated throwaway
-folder (`.instance/`, gitignored): `web/` is a symlink to `src/web`,
-`system/` is a private copy of the `src/system` template, and the user
-files / database are created fresh by the server itself. Because every
-run starts at the zero-user state, the harness registers a
-deterministic `admin` account through the same endpoint the first-boot
-wizard uses, then hands specs a base URL plus those credentials.
-
-The server binary is `src/arozos` (or `AROZOS_BIN`); when missing it is
-built automatically with `go build`.
-
 ## Running locally
 
 ```bash
 cd test/e2e/playwright
 npm install
 npx playwright install chromium
-npm test              # both suites
-npm run test:static   # Cine Studio only (no Go needed)
-npm run test:system   # full-stack critical paths only
-```
-
-Run a single system spec (it boots its own private server):
-
-```bash
-node specs-system/010-auth.js
+npm test
 ```
 
-Or point specs at an already-running test instance:
-
-```bash
-AROZ_BASE_URL=http://127.0.0.1:8126 \
-AROZ_ADMIN_USER=admin AROZ_ADMIN_PASS=... node specs-system/020-desktop.js
-```
+`run.js` starts a static server for `src/web` (default port 8123) and runs
+every spec in `specs/` in its own process, exiting non-zero if any fails.
 
 ### Useful env vars
 
 | Var | Purpose |
 | --- | --- |
-| `E2E_SUITE` | `static`, `system` or `all` (default `all`) |
-| `CS_BASE_URL` | Point a Cine Studio spec at an already-running static server |
+| `CS_BASE_URL` | Point a spec at an already-running server instead of starting one |
 | `WEB_PORT` | Port for the built-in static server (default `8123`) |
-| `AROZ_PORT` | Port for the disposable ArozOS instance (default `8126`) |
-| `AROZ_BASE_URL` | Reuse an already-running ArozOS test instance |
-| `AROZOS_BIN` | Prebuilt arozos binary (default `src/arozos`, auto-built) |
-| `PW_CHROMIUM_PATH` | Use a preinstalled Chromium binary |
+| `PW_CHROMIUM_PATH` | Use a preinstalled Chromium binary instead of Playwright's own |
+
+Run a single spec against the running server:
 
-When a system spec fails, check `.instance/server.log` for the server
-side of the story.
+```bash
+CS_BASE_URL=http://127.0.0.1:8123 node specs/functional.js
+```
 
 ## CI
 
-`.github/workflows/e2e-playwright.yml` builds the Go binary and runs
-both suites on any push or pull request touching `src/` or this
-harness, and uploads `.instance/server.log` as an artifact on failure.
+`.github/workflows/e2e-playwright.yml` runs the suite on pushes and pull
+requests that touch the app or this harness.

+ 3 - 5
test/e2e/playwright/package.json

@@ -1,12 +1,10 @@
 {
-  "name": "arozos-e2e",
+  "name": "cinestudio-e2e",
   "version": "1.0.0",
   "private": true,
-  "description": "Playwright end-to-end tests for ArozOS (critical paths + WebApps)",
+  "description": "Playwright end-to-end tests for the Cine Studio ArozOS WebApp",
   "scripts": {
-    "test": "node run.js",
-    "test:static": "E2E_SUITE=static node run.js",
-    "test:system": "E2E_SUITE=system node run.js"
+    "test": "node run.js"
   },
   "devDependencies": {
     "playwright": "1.56.1"

+ 29 - 77
test/e2e/playwright/run.js

@@ -1,25 +1,10 @@
 /*
-    ArozOS E2E runner.
+    Cine Studio E2E runner.
 
-    Orchestrates two Playwright suites:
-
-      static  specs/          Cine Studio front-end specs, served by a
-                              plain static file server (no Go involved).
-      system  specs-system/   Full-stack critical-path specs (auth,
-                              desktop, file explorer, system settings,
-                              user management) driven against a real
-                              ArozOS server booted from the Go binary.
-
-    Each spec runs in its own Node process; the runner exits non-zero if
-    any spec fails, so CI turns red on the first regression.
-
-    Env:
-      E2E_SUITE   which suite(s) to run: "static", "system" or "all"
-                  (default "all")
-      WEB_PORT    port for the static server        (default 8123)
-      AROZ_PORT   port for the ArozOS test instance (default 8126)
-      AROZOS_BIN  prebuilt arozos binary (default src/arozos, built with
-                  `go build` when missing)
+    Starts a static server for the ArozOS web root, then runs each spec
+    in its own Node process against it. Exits non-zero if any spec fails,
+    so CI turns red on the first regression. The banner from each spec's
+    final assertion is printed inline.
 */
 "use strict";
 
@@ -27,85 +12,52 @@ const path = require("path");
 const fs = require("fs");
 const { spawn } = require("child_process");
 const staticServer = require("./lib/static-server");
-const arozosServer = require("./lib/arozos-server");
 
 const WEB_ROOT = path.resolve(__dirname, "../../../src/web");
-const STATIC_SPECS_DIR = path.join(__dirname, "specs");
-const SYSTEM_SPECS_DIR = path.join(__dirname, "specs-system");
+const SPECS_DIR = path.join(__dirname, "specs");
 
-function runSpec(specPath, extraEnv) {
+function runSpec(specPath, baseURL) {
     return new Promise(function (resolve) {
         const child = spawn(process.execPath, [specPath], {
             stdio: "inherit",
-            env: Object.assign({}, process.env, extraEnv)
+            env: Object.assign({}, process.env, { CS_BASE_URL: baseURL })
         });
         child.on("exit", function (code) { resolve(code || 0); });
     });
 }
 
-function listSpecs(dir) {
-    if (!fs.existsSync(dir)) { return []; }
-    return fs.readdirSync(dir)
+(async function () {
+    if (!fs.existsSync(path.join(WEB_ROOT, "Cine Studio", "index.html"))) {
+        console.error("Cannot find Cine Studio web app under " + WEB_ROOT);
+        process.exit(1);
+    }
+
+    const specs = fs.readdirSync(SPECS_DIR)
         .filter(function (f) { return f.endsWith(".js"); })
-        .sort()
-        .map(function (f) { return path.join(dir, f); });
-}
+        .sort();
+    if (!specs.length) {
+        console.error("No specs found in " + SPECS_DIR);
+        process.exit(1);
+    }
+
+    const { server, baseURL } = await staticServer.start(WEB_ROOT, Number(process.env.WEB_PORT) || 8123);
+    console.log("Serving " + WEB_ROOT + " at " + baseURL + "\n");
 
-async function runSuite(title, specs, extraEnv) {
     let failures = 0;
     for (const spec of specs) {
-        console.log("── [" + title + "] " + path.basename(spec) + " ──────────────────────────");
-        const code = await runSpec(spec, extraEnv);
+        console.log("── " + spec + " ──────────────────────────────────");
+        const code = await runSpec(path.join(SPECS_DIR, spec), baseURL);
         if (code !== 0) { failures++; console.log("  spec exited with code " + code); }
         console.log("");
     }
-    return failures;
-}
-
-(async function () {
-    const suite = (process.env.E2E_SUITE || "all").toLowerCase();
-    let totalSpecs = 0;
-    let totalFailures = 0;
-
-    // ── Static suite (Cine Studio) ──
-    if (suite === "all" || suite === "static") {
-        if (!fs.existsSync(path.join(WEB_ROOT, "Cine Studio", "index.html"))) {
-            console.error("Cannot find Cine Studio web app under " + WEB_ROOT);
-            process.exit(1);
-        }
-        const specs = listSpecs(STATIC_SPECS_DIR);
-        totalSpecs += specs.length;
-        const { server, baseURL } = await staticServer.start(WEB_ROOT, Number(process.env.WEB_PORT) || 8123);
-        console.log("Serving " + WEB_ROOT + " at " + baseURL + " (static suite)\n");
-        totalFailures += await runSuite("static", specs, { CS_BASE_URL: baseURL });
-        server.close();
-    }
 
-    // ── System suite (real ArozOS server) ──
-    if (suite === "all" || suite === "system") {
-        const specs = listSpecs(SYSTEM_SPECS_DIR);
-        totalSpecs += specs.length;
-        if (specs.length) {
-            console.log("Booting ArozOS test instance (system suite)...");
-            const srv = await arozosServer.start({});
-            console.log("ArozOS test instance ready at " + srv.baseURL + "\n");
-            try {
-                totalFailures += await runSuite("system", specs, {
-                    AROZ_BASE_URL: srv.baseURL,
-                    AROZ_ADMIN_USER: srv.admin.username,
-                    AROZ_ADMIN_PASS: srv.admin.password
-                });
-            } finally {
-                await srv.stop();
-            }
-        }
-    }
+    server.close();
 
-    if (totalFailures) {
-        console.error(totalFailures + " of " + totalSpecs + " spec file(s) failed.");
+    if (failures) {
+        console.error(failures + " of " + specs.length + " spec file(s) failed.");
         process.exit(1);
     }
-    console.log("All " + totalSpecs + " spec file(s) passed.");
+    console.log("All " + specs.length + " spec file(s) passed.");
 })().catch(function (e) {
     console.error(e);
     process.exit(1);