diff --git a/.env.example b/.env.example
index 03b6ae69..9626c091 100644
--- a/.env.example
+++ b/.env.example
@@ -11,6 +11,9 @@ STACKABLE_COCKPIT_OIDC_DISCOVERY_URL=https://your-idp.example.com/realms/your-re
STACKABLE_COCKPIT_OIDC_CLIENT_ID=stackable-cockpit
STACKABLE_COCKPIT_OIDC_CLIENT_SECRET=your-client-secret
+# Storage encryption key (64 hex characters) used to encrypt sensitive data in storage.
+STORAGE_ENCRYPTION_KEY=F000000000000000000000000000000000000000000000000000000000000000
+
# OIDC claim used as the Trino user for impersonation (default: preferred_username)
# STACKABLE_COCKPIT_TRINO_USER_CLAIM=preferred_username
@@ -40,4 +43,14 @@ STACKABLE_COCKPIT_OIDC_CLIENT_SECRET=your-client-secret
# Feature flags
# STACKABLE_COCKPIT_COMPLETION_ENABLED=false # Disable SQL editor code completion (default: true)
# STACKABLE_COCKPIT_STORAGE_BROWSER_ENABLED=true # Enable S3/HDFS file browser (default: false)
+# STACKABLE_COCKPIT_ARCHIVE_PREVIEW_MAX_MB=100 # Max decompressed size for archive preview in megabytes (default: 100)
+# STACKABLE_COCKPIT_PARQUET_PREVIEW_DISALLOWED_COMPRESSION_TYPES=gzip-no_offset # Comma-separated list of compression types to disallow from parquet data preview (default: "gzip-no_offset")
+# PUBLIC_STACKABLE_COCKPIT_STORAGE_AUTO_CONNECT_TIMEOUT_MS=15000 # Auto-connect to last used storage after this many milliseconds (default: 15000)
+# PUBLIC_STACKABLE_COCKPIT_STORAGE_RESTORE_TABS=true # File browser: continue where you left off — restore open tabs from the last session (default: false)
+# PUBLIC_STACKABLE_COCKPIT_MAX_EDITABLE_FILE_SIZE=1048576 # Max file size (in bytes) of editable text files in the storage browser (default: 5 MiB)
# PUBLIC_STACKABLE_COCKPIT_UPLOAD_CONCURRENCY=3 # Maximum number of concurrent file uploads (default: 3)
+# PUBLIC_STACKABLE_COCKPIT_INFINITE_SCROLL_ENABLED=true # Enable infinite scroll in the storage browser (default: true)
+# PUBLIC_STACKABLE_COCKPIT_STORAGE_CUT_COPY_ENABLED=true # Enables cut/copy/paste functionality in the storage browser (default: false)
+# PUBLIC_STACKABLE_COCKPIT_STORAGE_PASTE_ENABLED=true # Enables paste functionality in the storage browser (default: false)
+# PUBLIC_STACKABLE_COCKPIT_STORAGE_RENAME_ENABLED=true # Enables rename functionality in the storage browser (default: false)
+# PUBLIC_STACKABLE_COCKPIT_STORAGE_MOVE_ENABLED=true # Enables move functionality in the storage browser (default: false)
diff --git a/.env.test b/.env.test
index c1b50cfe..92f69b70 100644
--- a/.env.test
+++ b/.env.test
@@ -11,10 +11,22 @@ STACKABLE_COCKPIT_IMAGE_PREVIEW_BYTES=5242880
STACKABLE_COCKPIT_PDF_PREVIEW_BYTES=26214400
STACKABLE_COCKPIT_FILE_PREVIEW_ROWS=250
STACKABLE_COCKPIT_FILE_PREVIEW_COLUMNS=50
+STACKABLE_COCKPIT_PARQUET_PREVIEW_DISALLOWED_COMPRESSION_TYPES=gzip-no_offset
PUBLIC_STACKABLE_COCKPIT_STORAGE_AUTO_CONNECT=true
+PUBLIC_STACKABLE_COCKPIT_STORAGE_RESTORE_TABS=true
PUBLIC_STACKABLE_COCKPIT_PAGE_SIZES=25,50,100
PUBLIC_STACKABLE_COCKPIT_DEFAULT_PAGE_SIZE=25
PUBLIC_STACKABLE_COCKPIT_MAX_RECENT_FILES=15
PUBLIC_STACKABLE_COCKPIT_UPLOAD_CONCURRENCY=3
+PUBLIC_STACKABLE_COCKPIT_INFINITE_SCROLL_ENABLED=true
+PUBLIC_STACKABLE_COCKPIT_STORAGE_CUT_COPY_ENABLED=true
+PUBLIC_STACKABLE_COCKPIT_STORAGE_PASTE_ENABLED=true
+PUBLIC_STACKABLE_COCKPIT_STORAGE_RENAME_ENABLED=true
+PUBLIC_STACKABLE_COCKPIT_STORAGE_MOVE_ENABLED=true
GARAGE_ADMIN_URL=http://localhost:30902
GARAGE_ADMIN_TOKEN=stackable-cockpit-e2e-admin-token
+BETTER_AUTH_URL=http://localhost:4173
+BETTER_AUTH_SECRET=stackable-cockpit-e2e-better-auth-secret
+DATABASE_PASSWORD=e2e-build-placeholder-password
+STORAGE_ENCRYPTION_KEY=a781775ee543107bfa97191691bb23c982dd9d9550542df27194ebc09d617dfd
+PREVIEW_PORT=4173
diff --git a/.github/workflows/pr_checks.yaml b/.github/workflows/pr_checks.yaml
index f1b7216b..399cd25d 100644
--- a/.github/workflows/pr_checks.yaml
+++ b/.github/workflows/pr_checks.yaml
@@ -59,7 +59,7 @@ jobs:
run: python -m pip install pre-commit
- name: Run pre-commit
- run: pre-commit run --all-files --show-diff-on-failure --color=always
+ run: pre-commit run --all-files --hook-stage manual --show-diff-on-failure --color=always
unit-tests:
name: Unit Tests
@@ -85,10 +85,9 @@ jobs:
- name: Run unit tests
run: npm run test:unit
- e2e-with-garage:
- name: E2E Tests with Garage S3
+ e2e-tests:
+ name: E2E Tests with Garage S3 and PostgreSQL
runs-on: ubuntu-latest
-
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -107,35 +106,13 @@ jobs:
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox
- - name: Start Garage S3 and initialize bucket
- run: |
- # GitHub Actions services now support entrypoint + command, but we still
- # start Garage manually here because service containers are created before
- # actions/checkout runs. The upstream Garage image contains only the
- # /garage binary (no /bin/sh), so the service cannot wait for the checked-
- # out e2e/garage.toml to appear before it starts.
- docker run -d --name garage \
- -p 3900:3900 \
- -p 3902:3902 \
- -e GARAGE_CONFIG_FILE=/workspace/dev/garage/garage.toml \
- -v "$PWD:/workspace" \
- --entrypoint /garage \
- oci.stackable.tech/stackable/dxflrs/garage:v2.3.0 \
- server --single-node
-
- # Wait for the S3 API to become available
- for _ in $(seq 1 30); do
- if curl -s http://localhost:3900 >/dev/null 2>&1; then
- break
- fi
- sleep 1
- done
-
- S3_SECRET_ACCESS_KEY="e2e-test-secret-key-for-ci" \
- GARAGE_ADMIN_TOKEN='stackable-cockpit-e2e-admin-token' \
- S3_ENDPOINT='http://localhost:3900' \
- S3_CONFIG_PATH="$PWD/s3-config.json" \
- ./e2e/init-garage-s3.sh
-
- - name: Run E2E tests with Garage
- run: npm run test:e2e:garage
+ - name: Run E2E tests with Garage and PostgreSQL
+ run: npm run test:e2e
+
+ - name: Upload Playwright traces
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: playwright-traces
+ path: e2e/test-results/
+ retention-days: 7
diff --git a/.gitignore b/.gitignore
index 871677f2..aaee646b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ src/lib/editor/generated/
.wrangler
.svelte-kit
/build
+/reports/
# OS
.DS_Store
@@ -40,6 +41,7 @@ coverage/*
# Playwright
e2e/test-results
e2e/.auth
+.playwright/
# Paraglide
src/lib/paraglide
project.inlang/cache/
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000..df6ae337
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+24.21.0
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 239c27d2..1f71bd65 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -43,27 +43,45 @@ repos:
- repo: local
hooks:
+ # ── Fix variant (runs locally on commit — auto-fixes where possible) ──
+ - id: npm-lint-fix
+ name: npm lint with --fix (prettier + eslint)
+ language: system
+ entry: npm run lint:fix
+ stages: [pre-commit, pre-merge-commit]
+ pass_filenames: false
+ files: \.(svelte|ts|js|css|html|json|md)$
+
+ # ── Check-only variant (runs in CI — no auto-fix) ──
- id: npm-lint
- name: npm lint (prettier + eslint)
+ name: npm lint check only (prettier + eslint)
language: system
entry: npm run lint
- stages: [pre-commit, pre-merge-commit]
+ stages: [manual]
pass_filenames: false
files: \.(svelte|ts|js|css|html|json|md)$
+ # ── Shared hooks (same behaviour locally and in CI) ──
- id: npm-check
name: npm check (svelte-check)
language: system
entry: npm run check
- stages: [pre-commit, pre-merge-commit]
+ stages: [pre-commit, pre-merge-commit, manual]
pass_filenames: false
files: \.(svelte|ts|js)$
+ - id: npm-arch-test
+ name: npm arch test (fitness functions)
+ language: system
+ entry: npm run test:arch
+ stages: [pre-commit, pre-merge-commit, manual]
+ pass_filenames: false
+
- id: helm-lint
name: helm lint
language: system
entry: helm lint deploy/helm/cockpit
- stages: [pre-commit, pre-merge-commit]
+ stages: [pre-commit, pre-merge-commit, manual]
pass_filenames: false
files: ^deploy/helm/
@@ -76,6 +94,6 @@ repos:
--set auth.oidc.discoveryUrl=https://stub
--set auth.oidc.clientId=stub
--set auth.oidc.clientSecret.secretKeyRef.name=stub > /dev/null'
- stages: [pre-commit, pre-merge-commit]
+ stages: [pre-commit, pre-merge-commit, manual]
pass_filenames: false
files: ^deploy/helm/
diff --git a/.prettierignore b/.prettierignore
index 6dbc7f7c..f2eaf48d 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -33,3 +33,4 @@ coverage/
# Helm templates
deploy/helm/
dev/garage/
+dev/postgresql/
diff --git a/AGENTS.md b/AGENTS.md
index 68489856..7803a2fd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -188,10 +188,10 @@ The application uses **Paraglide-JS v2** for type-safe, compiler-based internati
### Node.js Version
-The required Node.js version is pinned in `.node-version`. Use `nvm` to install and activate it before running any `npm` commands — `npm` will hard-fail with engine errors otherwise (`.npmrc` sets `engine-strict=true`).
+The required Node.js version is pinned in `.node-version` for CI and `.nvmrc` for NVM. Keep both files synchronised. Use `nvm` to install and activate it before running any `npm` commands — `npm` will hard-fail with engine errors otherwise (`.npmrc` sets `engine-strict=true`).
```bash
-nvm install # installs the version from .node-version
+nvm install # installs the version from .nvmrc
nvm use # activates it in the current shell
```
@@ -224,6 +224,36 @@ pre-commit run --all-files # Run all checks: lint (prettier + eslint), type ch
# markdownlint, yamllint, shellcheck, actionlint, hadolint, helm lint
```
+### Architecture Fitness Functions
+
+```bash
+npm run test:arch # Run all architecture fitness function tests (~5 s)
+npm run test:arch:report # Same, plus generate HTML dependency/metrics reports in /reports/
+```
+
+Architecture tests live in `src/architecture/*.spec.ts` and use [ArchUnitTS](https://github.com/LukasNiessen/ArchUnitTS) together with plain Node.js `fs` checks. They run in CI and must remain **green at all times**.
+
+**Run `npm run test:arch` whenever you:**
+
+- Add a new file to `src/lib/server/` (verify it doesn't break client-boundary rules)
+- Add a new Svelte component (PascalCase naming and UI-pattern checks)
+- Add or remove message keys in `messages/en.json` (both locale files must stay in sync)
+- Refactor the Trino sub-layer (circular-dependency rules)
+
+**What the fitness functions enforce:**
+
+| Category | What is checked |
+| ------------------------ | ------------------------------------------------------------------------------------------------------------- |
+| Server / Client Boundary | `src/lib/client`, `stores`, `storage`, `editor`, `types` must not import `src/lib/server/**` |
+| No Circular Dependencies | `src/lib/**` (excluding Trino) and `src/routes/**` must be cycle-free |
+| Naming Conventions | Stores → `*.svelte.ts`; lib components → PascalCase `.svelte`; server files → no `.svelte.ts` extension |
+| Code Size Limits | `.ts` files < 2 400 LOC; `.svelte` files < 1 100 LOC; test files < 1 000 LOC |
+| UI Pattern Enforcement | No native date inputs; `` must have `alt`; no clickable `