From e0cc6255ab068ccaa16d00604d3af112930fbbf6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 8 Sep 2026 11:51:51 -0300 Subject: [PATCH 01/17] feat: add admin settings interface --- .github/workflows/node-test.yml | 25 ++-- .github/workflows/playwright.yml | 115 +++++++++++++++++ README.md | 18 +-- appinfo/info.xml | 8 ++ lib/Command/RetentionGet.php | 35 ++++++ lib/Command/RetentionSet.php | 50 ++++++++ lib/Settings/AdminSection.php | 36 ++++++ lib/Settings/AdminSettings.php | 35 ++++++ package.json | 32 ++++- playwright.config.ts | 25 ++++ src/api/settings.ts | 37 ++++++ src/env.d.ts | 6 + src/settings.ts | 10 ++ src/tests/AdminSettings.spec.ts | 66 ++++++++++ src/views/AdminSettings.vue | 116 ++++++++++++++++++ templates/settings/admin.php | 8 ++ .../integration/features/occ_settings.feature | 21 ++++ tests/playwright/admin-settings.spec.ts | 28 +++++ tsconfig.json | 14 ++- vite.config.mjs | 17 +++ vitest.config.ts | 20 +++ 21 files changed, 693 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 lib/Command/RetentionGet.php create mode 100644 lib/Command/RetentionSet.php create mode 100644 lib/Settings/AdminSection.php create mode 100644 lib/Settings/AdminSettings.php create mode 100644 playwright.config.ts create mode 100644 src/api/settings.ts create mode 100644 src/env.d.ts create mode 100644 src/settings.ts create mode 100644 src/tests/AdminSettings.spec.ts create mode 100644 src/views/AdminSettings.vue create mode 100644 templates/settings/admin.php create mode 100644 tests/integration/features/occ_settings.feature create mode 100644 tests/playwright/admin-settings.spec.ts create mode 100644 vite.config.mjs create mode 100644 vitest.config.ts diff --git a/.github/workflows/node-test.yml b/.github/workflows/node-test.yml index b7ad0fd..d5cdc3e 100644 --- a/.github/workflows/node-test.yml +++ b/.github/workflows/node-test.yml @@ -1,5 +1,5 @@ # Based on the Nextcloud organization workflow template. -# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2022-2026 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: MIT name: Node tests @@ -21,21 +21,24 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: Read package engines - uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 id: versions - with: - fallbackNode: '^24' - fallbackNpm: '^11.3' + - name: Set up node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: ${{ steps.versions.outputs.nodeVersion }} + node-version: ${{ steps.versions.outputs.node-version }} + - name: Set up npm - run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' + - name: Install dependencies run: npm install --no-package-lock - - name: Run Node checks - run: | - npm run lint - npm run ts:check + + - name: Run unit tests + run: npm run test:coverage + + - name: Build frontend + run: npm run build diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..a5e80a6 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,115 @@ +# Based on the Nextcloud organization workflow template. +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Playwright tests + +on: pull_request + +permissions: + contents: read + +concurrency: + group: playwright-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + playwright: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + APP_NAME: usage_statistics_server + PLAYWRIGHT_BASE_URL: http://127.0.0.1:8080 + + steps: + - name: Checkout server + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + submodules: true + repository: nextcloud/server + ref: stable36 + + - name: Checkout app + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + path: apps/${{ env.APP_NAME }} + + - name: Read package engines + working-directory: apps/${{ env.APP_NAME }} + uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 + id: versions + + - name: Set up node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.node-version }} + + - name: Set up npm + run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' + + - name: Set up PHP + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 + with: + php-version: '8.4' + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, sockets, sqlite, pdo_sqlite, xmlreader, xmlwriter, zip, zlib + coverage: none + ini-file: development + ini-values: disable_functions= + + - name: Install app dependencies + working-directory: apps/${{ env.APP_NAME }} + run: | + composer remove nextcloud/ocp --dev --no-scripts + composer install --no-dev --no-scripts + npm install --no-package-lock + npm run build + + - name: Install Playwright browser + working-directory: apps/${{ env.APP_NAME }} + run: npm run playwright:install-ci + + - name: Set up Nextcloud + run: | + mkdir data + ./occ maintenance:install \ + --database=sqlite \ + --database-name=nextcloud \ + --admin-user admin \ + --admin-pass admin + ./occ app:enable --force $APP_NAME + ./occ config:system:set auth.bruteforce.protection.enabled --value false --type boolean + ./occ config:system:set ratelimit.protection.enabled --value false --type boolean + + - name: Start Nextcloud + run: | + php -S 127.0.0.1:8080 -t . > /tmp/nextcloud-server.log 2>&1 & + echo $! > /tmp/nextcloud-server.pid + for attempt in {1..30}; do + if curl --fail --silent http://127.0.0.1:8080/status.php > /dev/null; then + exit 0 + fi + sleep 1 + done + cat /tmp/nextcloud-server.log + exit 1 + + - name: Run Playwright tests + working-directory: apps/${{ env.APP_NAME }} + run: npm run playwright + + - name: Show server logs + if: failure() + run: | + cat /tmp/nextcloud-server.log || true + cat data/nextcloud.log || true + + - name: Upload Playwright report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-report + path: apps/${{ env.APP_NAME }}/playwright-report/ + if-no-files-found: ignore + retention-days: 7 diff --git a/README.md b/README.md index 7a21611..38f6c38 100644 --- a/README.md +++ b/README.md @@ -37,21 +37,15 @@ The ingestion protocol is application-agnostic. Metric values are typed and sche The server does not treat submitted data as an audited census. Reports come from software running on infrastructure controlled by the sender, so aggregated results should be described as statistics reported by participating installations. +Server administrators can configure data retention from the Nextcloud administration settings or with the `usage-statistics-server:retention:*` OCC commands. + ## Documentation Start here if you want to integrate another Nextcloud app: -- [Protocol v1](docs/protocol-v1.md) — report format, validation rules, and ingestion behavior. -- [Application schemas](docs/application-schemas.md) — how applications define metrics and evolve schemas. -- [Administrative API](docs/admin-api.md) — endpoints for querying aggregated statistics. -- [Retention](docs/retention.md) — historical data retention and cleanup behavior. +- [Protocol v1](docs/protocol-v1.md): report format, validation rules, and ingestion behavior. +- [Application schemas](docs/application-schemas.md): how applications define metrics and evolve schemas. +- [Administrative API](docs/admin-api.md): endpoints for querying aggregated statistics. +- [Retention](docs/retention.md): historical data retention and cleanup behavior. The OpenAPI specifications are generated from the Nextcloud controller contracts and are kept in the repository for API consumers and generated types. - -## Current support - -The app currently targets Nextcloud 35 and 36 and is tested with SQLite, MySQL, MariaDB, and PostgreSQL. - -## License - -Usage Statistics Server is licensed under the GNU Affero General Public License v3 or later. See [COPYING](COPYING). diff --git a/appinfo/info.xml b/appinfo/info.xml index 790b908..eba7563 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -23,6 +23,14 @@ A generic Nextcloud app for receiving, storing, aggregating, and exposing privac + + OCA\UsageStatisticsServer\Command\RetentionGet + OCA\UsageStatisticsServer\Command\RetentionSet + + + OCA\UsageStatisticsServer\Settings\AdminSettings + OCA\UsageStatisticsServer\Settings\AdminSection + OCA\UsageStatisticsServer\BackgroundJob\CleanupOldData diff --git a/lib/Command/RetentionGet.php b/lib/Command/RetentionGet.php new file mode 100644 index 0000000..6852962 --- /dev/null +++ b/lib/Command/RetentionGet.php @@ -0,0 +1,35 @@ +setName('usage-statistics-server:retention:get') + ->setDescription('Show the report retention period in days'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln(sprintf('Retention period: %d days', $this->settings->getRetentionDays())); + return self::SUCCESS; + } +} diff --git a/lib/Command/RetentionSet.php b/lib/Command/RetentionSet.php new file mode 100644 index 0000000..fb8cbd8 --- /dev/null +++ b/lib/Command/RetentionSet.php @@ -0,0 +1,50 @@ +setName('usage-statistics-server:retention:set') + ->setDescription('Set the report retention period in days') + ->addArgument('days', InputArgument::REQUIRED, 'Retention period in days'); + } + + #[Override] + protected function execute(InputInterface $input, OutputInterface $output): int { + $days = filter_var($input->getArgument('days'), FILTER_VALIDATE_INT); + if ($days === false) { + $output->writeln('Retention period must be an integer.'); + return self::INVALID; + } + + try { + $days = $this->settings->setRetentionDays($days); + } catch (\InvalidArgumentException $e) { + $output->writeln(sprintf('%s', $e->getMessage())); + return self::INVALID; + } + + $output->writeln(sprintf('Retention period set to %d days', $days)); + return self::SUCCESS; + } +} diff --git a/lib/Settings/AdminSection.php b/lib/Settings/AdminSection.php new file mode 100644 index 0000000..5a1ef44 --- /dev/null +++ b/lib/Settings/AdminSection.php @@ -0,0 +1,36 @@ +l10n->t('Usage Statistics'); + } + + #[Override] + public function getPriority(): int { + return 80; + } +} diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php new file mode 100644 index 0000000..af02613 --- /dev/null +++ b/lib/Settings/AdminSettings.php @@ -0,0 +1,35 @@ + { + ocs: { + data: T + } +} + +const settingsUrl = generateOcsUrl('/apps/usage_statistics_server/api/v1/admin/settings') + +export async function getServerSettings(): Promise { + const response = await axios.get>(settingsUrl, { + headers: { 'OCS-APIRequest': 'true' }, + }) + return response.data.ocs.data +} + +export async function updateServerSettings(retentionDays: number): Promise { + const response = await axios.put>( + settingsUrl, + { retentionDays }, + { headers: { 'OCS-APIRequest': 'true' } }, + ) + return response.data.ocs.data.retentionDays +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..0f508c3 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,6 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/// diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..7c1ca92 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,10 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createApp } from 'vue' + +import AdminSettings from './views/AdminSettings.vue' + +createApp(AdminSettings).mount('#usage-statistics-server-admin-settings') diff --git a/src/tests/AdminSettings.spec.ts b/src/tests/AdminSettings.spec.ts new file mode 100644 index 0000000..f1b8f81 --- /dev/null +++ b/src/tests/AdminSettings.spec.ts @@ -0,0 +1,66 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import AdminSettings from '../views/AdminSettings.vue' +import { getServerSettings, updateServerSettings } from '../api/settings' + +vi.mock('../api/settings', () => ({ + getServerSettings: vi.fn(), + updateServerSettings: vi.fn(), +})) + +const stubs = { + NcSettingsSection: { template: '
' }, + NcNoteCard: { template: '
' }, + NcTextField: { + props: ['modelValue', 'label', 'type', 'min', 'max', 'disabled', 'helpText'], + emits: ['update:modelValue'], + template: '', + }, + NcButton: { + props: ['disabled'], + emits: ['click'], + template: '', + }, +} + +describe('AdminSettings', () => { + beforeEach(() => { + vi.mocked(getServerSettings).mockReset() + vi.mocked(updateServerSettings).mockReset() + vi.mocked(getServerSettings).mockResolvedValue({ + retentionDays: 1095, + minimumRetentionDays: 45, + maximumRetentionDays: 3650, + }) + }) + + it('loads and saves the retention period', async () => { + vi.mocked(updateServerSettings).mockResolvedValue(120) + const wrapper = mount(AdminSettings, { global: { stubs } }) + await flushPromises() + + const input = wrapper.get('input') + expect(input.element.value).toBe('1095') + + await input.setValue('120') + await wrapper.get('button').trigger('click') + await flushPromises() + + expect(updateServerSettings).toHaveBeenCalledWith(120) + expect(wrapper.text()).toContain('Settings saved.') + }) + + it('does not allow values outside the configured range', async () => { + const wrapper = mount(AdminSettings, { global: { stubs } }) + await flushPromises() + + await wrapper.get('input').setValue('44') + expect(wrapper.get('button').attributes('disabled')).toBeDefined() + }) +}) diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue new file mode 100644 index 0000000..eb570da --- /dev/null +++ b/src/views/AdminSettings.vue @@ -0,0 +1,116 @@ + + + + + + + diff --git a/templates/settings/admin.php b/templates/settings/admin.php new file mode 100644 index 0000000..85bb936 --- /dev/null +++ b/templates/settings/admin.php @@ -0,0 +1,8 @@ + +
diff --git a/tests/integration/features/occ_settings.feature b/tests/integration/features/occ_settings.feature new file mode 100644 index 0000000..9fd8b69 --- /dev/null +++ b/tests/integration/features/occ_settings.feature @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +Feature: configure Usage Statistics Server with OCC + + Scenario: read and update the retention period + Given run the command "usage-statistics-server:retention:get" + Then the output of the last command should contain the following text: + "Retention period: 1095 days" + When run the command "usage-statistics-server:retention:set 120" + Then the output of the last command should contain the following text: + "Retention period set to 120 days" + When run the command "usage-statistics-server:retention:get" + Then the output of the last command should contain the following text: + "Retention period: 120 days" + When run the command "usage-statistics-server:retention:set 44" with result code 2 + Then the output of the last command should contain the following text: + "retentionDays must be between 45 and 3650." + When run the command "usage-statistics-server:retention:set 1095" + Then the output of the last command should contain the following text: + "Retention period set to 1095 days" diff --git a/tests/playwright/admin-settings.spec.ts b/tests/playwright/admin-settings.spec.ts new file mode 100644 index 0000000..e806c56 --- /dev/null +++ b/tests/playwright/admin-settings.spec.ts @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '@playwright/test' + +async function login(page: import('@playwright/test').Page): Promise { + await page.goto('/index.php/login') + await page.locator('#user').fill('admin') + await page.locator('#password').fill('admin') + await page.locator('#submit-form').click() + await page.waitForURL(/\/index\.php\/apps\/|\/index\.php\/settings\/user/) +} + +test('administrator can change the retention period', async ({ page }) => { + await login(page) + await page.goto('/index.php/settings/admin/usage-statistics-server') + + const input = page.getByLabel('Retention period (days)') + await expect(input).toHaveValue('1095') + await input.fill('120') + await page.getByRole('button', { name: 'Save' }).click() + await expect(page.getByText('Settings saved.')).toBeVisible() + + await page.reload() + await expect(input).toHaveValue('120') +}) diff --git a/tsconfig.json b/tsconfig.json index 736f992..5feafaa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,19 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "ESNext", + "moduleResolution": "Bundler", "strict": true, "noEmit": true, - "skipLibCheck": true + "skipLibCheck": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client", "vitest/globals"] }, "include": [ - "src/types/**/*.ts" + "src/**/*.ts", + "src/**/*.vue", + "tests/playwright/**/*.ts", + "playwright.config.ts", + "vitest.config.ts" ] } diff --git a/vite.config.mjs b/vite.config.mjs new file mode 100644 index 0000000..c1dd35e --- /dev/null +++ b/vite.config.mjs @@ -0,0 +1,17 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createAppConfig } from '@nextcloud/vite-config' + +export default createAppConfig({ + settings: 'src/settings.ts', +}, { + extractLicenseInformation: { + includeSourceMaps: true, + }, + emptyOutputDirectory: { + additionalDirectories: ['css'], + }, +}) diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..f07532a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,20 @@ +/** + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'happy-dom', + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**/*.{ts,vue}'], + exclude: ['src/settings.ts', 'src/types/**', 'src/env.d.ts'], + }, + }, +}) From 7b9031ff885372f5681c79723aeb495460902ff5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos Date: Tue, 8 Sep 2026 11:53:38 -0300 Subject: [PATCH 02/17] fix: align admin frontend with Nextcloud conventions --- .github/workflows/lint-stylelint.yml | 46 ++++++++++++++++++++++++++++ .github/workflows/playwright.yml | 10 ++---- package.json | 4 +++ src/views/AdminSettings.vue | 19 ++++++------ stylelint.config.js | 8 +++++ 5 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/lint-stylelint.yml create mode 100644 stylelint.config.js diff --git a/.github/workflows/lint-stylelint.yml b/.github/workflows/lint-stylelint.yml new file mode 100644 index 0000000..a3d4973 --- /dev/null +++ b/.github/workflows/lint-stylelint.yml @@ -0,0 +1,46 @@ +# This workflow is based on the Nextcloud organization template repository. +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT + +name: Lint stylelint + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-stylelint-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + name: stylelint + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 + id: versions + with: + fallbackNode: '^24' + fallbackNpm: '^11.3' + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Install dependencies + run: npm install --no-package-lock + + - name: Lint + run: npm run stylelint diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a5e80a6..e1ea386 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -36,18 +36,13 @@ jobs: persist-credentials: false path: apps/${{ env.APP_NAME }} - - name: Read package engines - working-directory: apps/${{ env.APP_NAME }} - uses: nextcloud-libraries/parse-package-engines-action@122ae05d4257008180a514e1ddeb0c1b9d094bdd # v0.1.0 - id: versions - - name: Set up node uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: ${{ steps.versions.outputs.node-version }} + node-version: '24' - name: Set up npm - run: npm i -g 'npm@${{ steps.versions.outputs.package-manager-version }}' + run: npm i -g 'npm@^11.3' - name: Set up PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2 @@ -85,7 +80,6 @@ jobs: - name: Start Nextcloud run: | php -S 127.0.0.1:8080 -t . > /tmp/nextcloud-server.log 2>&1 & - echo $! > /tmp/nextcloud-server.pid for attempt in {1..30}; do if curl --fail --silent http://127.0.0.1:8080/status.php > /dev/null; then exit 0 diff --git a/package.json b/package.json index 57b4fdd..7c23083 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "vite build", "dev": "vite build --mode development", "lint": "eslint .", + "stylelint": "stylelint src/**/*.vue", + "stylelint:fix": "stylelint src/**/*.vue --fix", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest", @@ -34,6 +36,7 @@ "devDependencies": { "@nextcloud/browserslist-config": "^3.1.2", "@nextcloud/eslint-config": "^9.0.1", + "@nextcloud/stylelint-config": "^3.2.1", "@nextcloud/vite-config": "^3.0.0-beta.1", "@playwright/test": "^1.62.1", "@vitejs/plugin-vue": "^6.0.8", @@ -41,6 +44,7 @@ "@vue/test-utils": "^2.5.0", "happy-dom": "^20.14.0", "openapi-typescript": "^7.13.0", + "stylelint": "^16.25.0", "typescript": "^6.0.3", "vite": "^8.2.2", "vitest": "^4.1.4", diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue index eb570da..6f8d530 100644 --- a/src/views/AdminSettings.vue +++ b/src/views/AdminSettings.vue @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later