Full Code of etkecc/synapse-admin for AI

main cf66c8e3a49e cached
345 files
1.9 MB
517.4k tokens
226 symbols
1 requests
Download .txt
Showing preview only (2,218K chars total). Download the full file or copy to clipboard to get everything.
Repository: etkecc/synapse-admin
Branch: main
Commit: cf66c8e3a49e
Files: 345
Total size: 1.9 MB

Directory structure:
gitextract_pt5yit7t/

├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── SECURITY.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── reuse.yml
│       └── workflow.yml
├── .gitignore
├── .prettierignore
├── .watchmanconfig
├── LICENSE
├── LICENSES/
│   ├── Apache-2.0.txt
│   ├── BSD-2-Clause.txt
│   ├── CC0-1.0.txt
│   ├── MIT.txt
│   └── OFL-1.1.txt
├── README.md
├── REUSE.toml
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile.build
│   ├── Dockerfile.subpath-admin
│   ├── docker-compose-dev.yml
│   └── docker-compose.yml
├── docs/
│   ├── README.md
│   ├── apis.md
│   ├── availability.md
│   ├── components.md
│   ├── config.md
│   ├── configurable-columns.md
│   ├── cors-credentials.md
│   ├── csv-import.md
│   ├── custom-menu.md
│   ├── event-reports.md
│   ├── external-auth-provider.md
│   ├── federation.md
│   ├── media.md
│   ├── prefill-login-form.md
│   ├── registration-tokens.md
│   ├── restrict-hs.md
│   ├── reverse-proxy.md
│   ├── room-management.md
│   ├── screenshots/
│   │   ├── README.md
│   │   └── prepare.js
│   ├── server-statistics.md
│   ├── system-users.md
│   ├── testdata/
│   │   ├── element/
│   │   │   ├── config.json
│   │   │   └── nginx.conf
│   │   ├── mas/
│   │   │   └── config.yaml
│   │   ├── nginx/
│   │   │   └── nginx.conf
│   │   ├── postgres.initdb/
│   │   │   └── mas.sql
│   │   └── synapse/
│   │       ├── homeserver.yaml
│   │       ├── synapse.log.config
│   │       └── synapse.signing.key
│   ├── update-api-docs.ts
│   ├── user-badges.md
│   ├── user-management.md
│   ├── user-search.md
│   └── well-known-discovery.md
├── eslint.config.js
├── justfile
├── package.json
├── public/
│   ├── config.json
│   ├── data/
│   │   └── example.csv
│   └── robots.txt
├── src/
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── Context.tsx
│   ├── TEST_COVERAGE_TODO.md
│   ├── assets/
│   │   ├── fonts.css
│   │   └── theme.ts
│   ├── components/
│   │   ├── MatrixWordmark.tsx
│   │   ├── README.md
│   │   ├── etke.cc/
│   │   │   ├── BillingPage.tsx
│   │   │   ├── BillingStatusBadge.tsx
│   │   │   ├── ComponentsPage.tsx
│   │   │   ├── CurrentlyRunningCommand.tsx
│   │   │   ├── EtkeAttribution.test.tsx
│   │   │   ├── EtkeAttribution.tsx
│   │   │   ├── InstanceConfig.tsx
│   │   │   ├── README.md
│   │   │   ├── RichTextEditor.tsx
│   │   │   ├── ServerActionsPage.tsx
│   │   │   ├── ServerCommandsPanel.tsx
│   │   │   ├── ServerNotificationsBadge.test.tsx
│   │   │   ├── ServerNotificationsBadge.tsx
│   │   │   ├── ServerNotificationsPage.tsx
│   │   │   ├── ServerNotificationsUnavailable.test.tsx
│   │   │   ├── ServerNotificationsUnavailable.tsx
│   │   │   ├── ServerStatusBadge.test.tsx
│   │   │   ├── ServerStatusBadge.tsx
│   │   │   ├── ServerStatusPage.test.tsx
│   │   │   ├── ServerStatusPage.tsx
│   │   │   ├── SupportAttachments.tsx
│   │   │   ├── SupportPage.tsx
│   │   │   ├── SupportRequestPage.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── useServerCommands.ts
│   │   │   │   └── useUnits.ts
│   │   │   └── schedules/
│   │   │       ├── components/
│   │   │       │   ├── recurring/
│   │   │       │   │   ├── RecurringCommandEdit.tsx
│   │   │       │   │   ├── RecurringCommandsList.tsx
│   │   │       │   │   └── RecurringDeleteButton.tsx
│   │   │       │   └── scheduled/
│   │   │       │       ├── ScheduledCommandEdit.tsx
│   │   │       │       ├── ScheduledCommandShow.tsx
│   │   │       │       ├── ScheduledCommandsList.tsx
│   │   │       │       └── ScheduledDeleteButton.tsx
│   │   │       └── hooks/
│   │   │           ├── useRecurringCommands.tsx
│   │   │           └── useScheduledCommands.tsx
│   │   ├── hooks/
│   │   │   ├── useDocTitle.test.tsx
│   │   │   └── useDocTitle.tsx
│   │   ├── layout/
│   │   │   ├── AdminLayout.test.tsx
│   │   │   ├── AdminLayout.tsx
│   │   │   ├── Datagrid.test.tsx
│   │   │   ├── Datagrid.tsx
│   │   │   ├── EmptyState.test.tsx
│   │   │   ├── EmptyState.tsx
│   │   │   ├── Footer.test.tsx
│   │   │   ├── Footer.tsx
│   │   │   ├── List.tsx
│   │   │   ├── LoginFormBox.tsx
│   │   │   └── index.ts
│   │   ├── media/
│   │   │   ├── DeleteMediaButton.tsx
│   │   │   ├── ProtectMediaButton.tsx
│   │   │   ├── PurgeRemoteMediaButton.tsx
│   │   │   ├── QuarantineMediaButton.tsx
│   │   │   ├── ViewMedia.tsx
│   │   │   └── index.ts
│   │   ├── rooms/
│   │   │   ├── EventLookupDialog.tsx
│   │   │   ├── RoomHierarchy.test.ts
│   │   │   ├── RoomHierarchy.tsx
│   │   │   └── RoomMessages.tsx
│   │   ├── user-import/
│   │   │   ├── ConflictModeCard.tsx
│   │   │   ├── ErrorsCard.tsx
│   │   │   ├── ResultsCard.tsx
│   │   │   ├── StartImportCard.tsx
│   │   │   ├── StatsCard.tsx
│   │   │   ├── UploadCard.tsx
│   │   │   ├── UserImport.tsx
│   │   │   ├── types.ts
│   │   │   ├── useImportFile.test.ts
│   │   │   └── useImportFile.tsx
│   │   └── users/
│   │       ├── AdminClientConfigItems.tsx
│   │       ├── DeviceDisplayNameInput.tsx
│   │       ├── ExperimentalFeatures.tsx
│   │       ├── ServerNotices.tsx
│   │       ├── UserAccountData.tsx
│   │       ├── UserCounts.tsx
│   │       ├── UserRateLimits.tsx
│   │       ├── buttons/
│   │       │   ├── AllowCrossSigningButton.tsx
│   │       │   ├── BlockRoomButton.tsx
│   │       │   ├── DeleteAllMediaButton.tsx
│   │       │   ├── DeleteRoomButton.tsx
│   │       │   ├── DeleteUserButton.tsx
│   │       │   ├── DeviceCreateButton.tsx
│   │       │   ├── DeviceRemoveButton.tsx
│   │       │   ├── FindUserButton.tsx
│   │       │   ├── LoginAsUserButton.tsx
│   │       │   ├── PurgeHistoryButton.tsx
│   │       │   ├── QuarantineAllMediaButton.tsx
│   │       │   ├── RenewAccountValidityButton.tsx
│   │       │   └── ResetPasswordButton.tsx
│   │       └── fields/
│   │           ├── AvatarField.test.tsx
│   │           ├── AvatarField.tsx
│   │           └── EditableAvatarField.tsx
│   ├── entrypoints/
│   │   ├── auth-callback.html
│   │   └── index.html
│   ├── i18n/
│   │   ├── README.md
│   │   ├── de/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── en/
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── fa/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── fr/
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── i18n-keys.test.ts
│   │   ├── index.test.ts
│   │   ├── index.ts
│   │   ├── it/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── ja/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── pt/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── ru/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── types.d.ts
│   │   ├── uk/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   └── zh/
│   │       ├── base.ts
│   │       ├── common.ts
│   │       ├── index.ts
│   │       ├── mas.ts
│   │       ├── misc_resources.ts
│   │       ├── reports.ts
│   │       ├── rooms.ts
│   │       └── users.ts
│   ├── index.tsx
│   ├── pages/
│   │   ├── DonatePage.test.tsx
│   │   ├── DonatePage.tsx
│   │   ├── LoginPage.test.tsx
│   │   ├── LoginPage.tsx
│   │   ├── MASPolicyDataPage.test.tsx
│   │   ├── MASPolicyDataPage.tsx
│   │   ├── auth-callback-error.test.tsx
│   │   ├── auth-callback-error.tsx
│   │   ├── auth-callback.test.tsx
│   │   └── auth-callback.tsx
│   ├── providers/
│   │   ├── README.md
│   │   ├── auth/
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   ├── data/
│   │   │   ├── etke.test.ts
│   │   │   ├── etke.ts
│   │   │   ├── index.test.ts
│   │   │   ├── index.ts
│   │   │   ├── lifecycle.ts
│   │   │   ├── mas-actions.ts
│   │   │   ├── mas-utils.test.ts
│   │   │   ├── mas-utils.ts
│   │   │   ├── mas.ts
│   │   │   ├── scan.ts
│   │   │   ├── synapse-actions.ts
│   │   │   └── synapse.ts
│   │   ├── http.ts
│   │   ├── matrix.test.ts
│   │   ├── matrix.ts
│   │   ├── serverVersion.ts
│   │   └── types/
│   │       ├── common.ts
│   │       ├── destinations.ts
│   │       ├── etke.ts
│   │       ├── index.ts
│   │       ├── mas.ts
│   │       ├── reports.ts
│   │       ├── rooms.ts
│   │       └── users.ts
│   ├── resourceMap.ts
│   ├── resources/
│   │   ├── README.md
│   │   ├── destinations/
│   │   │   ├── List.tsx
│   │   │   └── index.ts
│   │   ├── mas/
│   │   │   ├── CompatSessions.tsx
│   │   │   ├── OAuth2Sessions.tsx
│   │   │   ├── PersonalSessions.tsx
│   │   │   ├── UpstreamOAuthLinks.tsx
│   │   │   ├── UpstreamOAuthProviders.tsx
│   │   │   ├── UserEmails.tsx
│   │   │   ├── UserSessions.tsx
│   │   │   ├── index.ts
│   │   │   └── shared.tsx
│   │   ├── registration-tokens/
│   │   │   ├── Create.tsx
│   │   │   ├── Edit.tsx
│   │   │   ├── List.tsx
│   │   │   └── index.ts
│   │   ├── reports/
│   │   │   ├── List.tsx
│   │   │   ├── Show.tsx
│   │   │   └── index.ts
│   │   ├── room-directory/
│   │   │   └── index.tsx
│   │   ├── rooms/
│   │   │   ├── List.tsx
│   │   │   ├── Show.tsx
│   │   │   └── index.ts
│   │   ├── scheduled-tasks/
│   │   │   ├── List.tsx
│   │   │   └── index.ts
│   │   ├── statistics/
│   │   │   ├── DatabaseRooms.tsx
│   │   │   ├── UserMedia.tsx
│   │   │   └── index.ts
│   │   └── users/
│   │       ├── Create.tsx
│   │       ├── Edit.tsx
│   │       ├── List.tsx
│   │       └── index.ts
│   ├── utils/
│   │   ├── config.test.ts
│   │   ├── config.ts
│   │   ├── date.test.ts
│   │   ├── date.ts
│   │   ├── error.test.ts
│   │   ├── error.ts
│   │   ├── fetchMedia.test.ts
│   │   ├── fetchMedia.ts
│   │   ├── formatBytes.test.ts
│   │   ├── formatBytes.ts
│   │   ├── icons.ts
│   │   ├── logger.test.ts
│   │   ├── logger.ts
│   │   ├── mxid.test.ts
│   │   ├── mxid.ts
│   │   ├── password.test.ts
│   │   ├── password.ts
│   │   ├── safety.test.ts
│   │   ├── safety.ts
│   │   ├── version.test.ts
│   │   └── version.ts
│   └── vitest.setup.ts
├── tsconfig.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
/docs/testdata


================================================
FILE: .editorconfig
================================================
# EditorConfig https://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true


================================================
FILE: .gitattributes
================================================
yarn*.cjs binary


================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contribution Guidelines

Table of Contents:

<!-- vim-markdown-toc GFM -->

* [Did you find a bug?](#did-you-find-a-bug)
  * [Is it a Security Vulnerability?](#is-it-a-security-vulnerability)
  * [Is it already a known issue?](#is-it-already-a-known-issue)
  * [Reporting a Bug](#reporting-a-bug)
  * [Is there a patch for the bug?](#is-there-a-patch-for-the-bug)
* [Do you want to add a new feature?](#do-you-want-to-add-a-new-feature)
  * [Is it just an idea?](#is-it-just-an-idea)
  * [Is there a patch for the feature?](#is-there-a-patch-for-the-feature)
* [Do you have questions about the Ketesa project or need guidance?](#do-you-have-questions-about-the-ketesa-project-or-need-guidance)

<!-- vim-markdown-toc -->

## Did you find a bug?

### Is it a Security Vulnerability?

Please follow the [Security Policy](https://github.com/etkecc/ketesa/blob/main/.github/SECURITY.md) for reporting
security vulnerabilities.

### Is it already a known issue?

Please ensure the bug was not already reported by searching [the Issues section](https://github.com/etkecc/ketesa/issues).

### Reporting a Bug

If you think you have found a bug in Ketesa, it is not a security vulnerability, and it is not already reported,
please open [a new issue](https://github.com/etkecc/ketesa/issues/new) with:
    * A proper title and clear description of the problem.
    * As much relevant information as possible:
        * The version of Ketesa you are using.
        * The version of Synapse you are using.
        * Any relevant browser console logs, failed requests details, and error messages.

### Is there a patch for the bug?

If you already have a patch for the bug, please open a pull request with the patch,
and mention the issue number in the pull request description.

## Do you want to add a new feature?

### Is it just an idea?

Please open [a new issue](https://github.com/etkecc/ketesa/issues/new) with:
    * A proper title and clear description of the requested feature.
    * Any relevant information about the feature:
        * Why do you think this feature is needed?
        * How do you think it should work? (provide Ketesa API endpoint)
        * Any relevant screenshots or mockups.

### Is there a patch for the feature?

If you already have a patch for the feature, please open a pull request with the patch,
and mention the issue number in the pull request description.

## Do you have questions about the Ketesa project or need guidance?

Please use the official community Matrix room: [#ketesa:etke.cc](https://matrix.to/#/#ketesa:etke.cc)


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report a Ketesa bug
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Browser console logs**
If applicable, add the browser console's log

**Instance configuration:**
 - Ketesa version: [e.g. v0.10.3-etke39]
 - Synapse version [v1.127.1]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for Ketesa
title: ''
labels: enhancement
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Provide related Synapse Admin API endpoints**
If applicable, provide links to the Synapse Admin API's endpoints that could be used to implement that feature

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/SECURITY.md
================================================
# Security Policy

## Supported Versions

Only [the last published version](https://github.com/etkecc/ketesa/releases/latest) of the project is supported.
This means that only the latest version will receive security updates.
If you are using an older version, you are strongly encouraged to upgrade to the latest version.

## Reporting a Vulnerability

Please contact us using the [#ketesa:etke.cc](https://matrix.to/#/#ketesa:etke.cc) Matrix room.
Ketesa is a static JS UI for Matrix servers,
so it is unlikely that there are (or will be) any impactful security vulnerabilities in the project itself.
However, we do not rule out the possibility of such cases, so we will be happy to receive any reports!


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 30

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 30

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 30


================================================
FILE: .github/workflows/reuse.yml
================================================
---
name: REUSE Compliance Check

on: [push, pull_request]

permissions:
  contents: read

jobs:
  reuse-compliance-check:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: REUSE Compliance Check
        uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0


================================================
FILE: .github/workflows/workflow.yml
================================================
name: CI
on:
  push:
    branches: [ "main" ]
    tags: [ "v*" ]
env:
  bunny_version: v0.1.0
  base_path: ./
  NODE_OPTIONS: --max_old_space_size=4096
permissions:
  checks: write
  contents: write
  packages: write
  pull-requests: read
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          cache: yarn
      - name: Install dependencies
        run: yarn install --immutable --network-timeout=300000 --pure-lockfile
      - name: Build
        run: |
          yarn build --base=${{ env.base_path }}
          mv dist dist-root
          yarn build --base=/admin/
          mv dist dist-subpath-admin
        env:
          NODE_ENV: production
      - uses: actions/upload-artifact@v7
        with:
          path: dist-root/
          name: dist-root
          if-no-files-found: error
          retention-days: 1
          compression-level: 0
          overwrite: true
          include-hidden-files: true
      - uses: actions/upload-artifact@v7
        with:
          path: dist-subpath-admin/
          name: dist-subpath-admin
          if-no-files-found: error
          retention-days: 1
          compression-level: 0
          overwrite: true
          include-hidden-files: true

  docker:
    name: Docker
    needs: build
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v6
      - uses: actions/download-artifact@v8
        with:
          name: dist-root
          path: dist/
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
      - name: Login to ghcr.io
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Login to hub.docker.com
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          username: etkecc
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          images: |
            ${{ github.repository }}
            etkecc/synapse-admin
            ghcr.io/${{ github.repository }}
            registry.etke.cc/${{ github.repository }}
          tags: |
            type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
            type=semver,pattern={{raw}}
            type=match,pattern=^v([0-9]+)\..*$,group=1,prefix=v,enable=${{ startsWith(github.ref, 'refs/tags/') }}
      - name: Build and push
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          platforms: linux/amd64,linux/arm64
          context: .
          push: true
          file: docker/Dockerfile
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
            VCS_REF=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
            BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}

  docker-subpath-admin:
    name: Docker (subpath /admin)
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/download-artifact@v8
        with:
          name: dist-subpath-admin
          path: dist/
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
      - name: Login to ghcr.io
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Login to hub.docker.com
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
        with:
          username: etkecc
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
        with:
          images: |
            ${{ github.repository }}
            etkecc/synapse-admin
            ghcr.io/${{ github.repository }}
          tags: |
            type=raw,value=latest-subpath-admin,enable=${{ github.ref_name == 'main' }}
            type=semver,pattern={{raw}}-subpath-admin
      - name: Build and push
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          platforms: linux/amd64,linux/arm64
          context: .
          push: true
          file: docker/Dockerfile.subpath-admin
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
            VCS_REF=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
            BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}

  cdn:
    name: CDN
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/download-artifact@v8
        with:
          name: dist-root
          path: dist/
      - name: Upload
        run: |
          wget -O bunny-upload.tar.gz https://github.com/etkecc/bunny-upload/releases/download/${{ env.bunny_version }}/bunny-upload_Linux_x86_64.tar.gz
          tar -xzf bunny-upload.tar.gz
          echo "${{ secrets.BUNNY_CONFIG }}" > bunny-config.yaml
          sed -i "s|<head>|<head>${{ secrets.CDN_HEAD }}|g" dist/index.html
          sed -i "s|<head>|<head>${{ secrets.CDN_HEAD }}|g" dist/auth-callback/index.html
          rm dist/robots.txt
          ./bunny-upload -c bunny-config.yaml

  github-release:
    name: Github Release
    needs: build
    if: ${{ startsWith(github.ref, 'refs/tags/') }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/download-artifact@v8
        with:
          name: dist-root
          path: dist-root/
      - uses: actions/download-artifact@v8
        with:
          name: dist-subpath-admin
          path: dist-subpath-admin/
      - name: Prepare release
        run: |
          mv dist-root ketesa
          tar chvzf ketesa.tar.gz ketesa
          mv dist-subpath-admin ketesa-subpath-admin
          tar chvzf ketesa-subpath-admin.tar.gz ketesa-subpath-admin
      - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
        with:
          files: |
            ketesa.tar.gz
            ketesa-subpath-admin.tar.gz
          generate_release_notes: true
          make_latest: "true"
          draft: false
          prerelease: false


================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=node,yarn,react,visualstudiocode

## local state
docs/apis/*
docs/testdata/postgres.data
docs/testdata/synapse.data

### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn
.pnp.*

### Node Patch ###
# Serverless Webpack directories
.webpack/

# Optional stylelint cache

# SvelteKit build / generate output
.svelte-kit

### react ###
.DS_*
**/*.backup.*
**/*.back.*

node_modules

*.sublime*

psd
thumb
sketch

### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/

# Built Visual Studio Code Extensions
*.vsix

### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide

### yarn ###
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored

.yarn/*

# End of https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode


================================================
FILE: .prettierignore
================================================
.vscode
.yarn
**/*.md
**/*.woff2


================================================
FILE: .watchmanconfig
================================================
{
  "ignore_dirs": ["docs/testdata"]
}


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS


================================================
FILE: LICENSES/Apache-2.0.txt
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.

"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.

"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).

"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.

"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and

     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and

     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and

     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.

     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


================================================
FILE: LICENSES/BSD-2-Clause.txt
================================================
Copyright (c) <<var;name=copyright;original= <year> <owner>;match=.+>> All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY <<var;name=copyrightHolderAsIs;original=THE COPYRIGHT HOLDERS AND CONTRIBUTORS;match=.+>> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <<var;name=copyrightHolderLiability;original=THE COPYRIGHT HOLDER OR CONTRIBUTORS;match=.+>> BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

================================================
FILE: LICENSES/CC0-1.0.txt
================================================
Creative Commons Legal Code

CC0 1.0 Universal

    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
    HEREUNDER.

Statement of Purpose

The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").

Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.

For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.

1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:

  i. the right to reproduce, adapt, distribute, perform, display,
     communicate, and translate a Work;
 ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
     likeness depicted in a Work;
 iv. rights protecting against unfair competition in regards to a Work,
     subject to the limitations in paragraph 4(a), below;
  v. rights protecting the extraction, dissemination, use and reuse of data
     in a Work;
 vi. database rights (such as those arising under Directive 96/9/EC of the
     European Parliament and of the Council of 11 March 1996 on the legal
     protection of databases, and under any national implementation
     thereof, including any amended or successor version of such
     directive); and
vii. other similar, equivalent or corresponding rights throughout the
     world based on applicable law or treaty, and any national
     implementations thereof.

2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.

3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.

4. Limitations and Disclaimers.

 a. No trademark or patent rights held by Affirmer are waived, abandoned,
    surrendered, licensed or otherwise affected by this document.
 b. Affirmer offers the Work as-is and makes no representations or
    warranties of any kind concerning the Work, express, implied,
    statutory or otherwise, including without limitation warranties of
    title, merchantability, fitness for a particular purpose, non
    infringement, or the absence of latent or other defects, accuracy, or
    the present or absence of errors, whether or not discoverable, all to
    the greatest extent permissible under applicable law.
 c. Affirmer disclaims responsibility for clearing rights of other persons
    that may apply to the Work or any use thereof, including without
    limitation any person's Copyright and Related Rights in the Work.
    Further, Affirmer disclaims responsibility for obtaining any necessary
    consents, permissions or other rights required for any use of the
    Work.
 d. Affirmer understands and acknowledges that Creative Commons is not a
    party to this document and has no duty or obligation with respect to
    this CC0 or use of the Work.


================================================
FILE: LICENSES/MIT.txt
================================================
MIT License

Copyright (c) <year> <copyright holders>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

================================================
FILE: LICENSES/OFL-1.1.txt
================================================
SIL OPEN FONT LICENSE

Version 1.1 - 26 February 2007

PREAMBLE

The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.

The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.

DEFINITIONS

"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the copyright statement(s).

"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.

"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS

Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.

5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.

TERMINATION

This license becomes null and void if any of the above conditions are not met.

DISCLAIMER

THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
  <img alt="Ketesa Logo" src="./public/images/logo.webp" height="140" />
  <h3 align="center">
    Ketesa<br>
    <a href="https://matrix.to/#/#ketesa:etke.cc">
      <img alt="Community room" src="https://img.shields.io/badge/room-community_room-green?logo=matrix&label=%23ketesa%3Aetke.cc">
    </a><br>
    <a href="./LICENSE">
      <img alt="License" src="https://img.shields.io/github/license/etkecc/ketesa">
    </a>
    <a href="https://api.reuse.software/info/github.com/etkecc/ketesa">
      <img alt="REUSE compliance" src="https://api.reuse.software/badge/github.com/etkecc/ketesa">
    </a>
  </h3>
  <p align="center"><strong>The evolution of Synapse Admin. Manage, monitor, and maintain your Matrix homeserver from one clean interface. Built for small private servers and large federated communities alike. Formerly Synapse Admin.</strong></p>
</p>

---

![Login](./docs/screenshots/light/login.webp)
![Users List](./docs/screenshots/light/users-list.webp)

[View all screenshots →](./docs/screenshots/README.md)

## 📖 About

Ketesa is the evolution of Synapse Admin — a fully independent admin interface for Matrix homeservers.
What began as a fork of [Awesome-Technologies/synapse-admin](https://github.com/Awesome-Technologies/synapse-admin)
has grown into its own project, with a redesigned interface, comprehensive API coverage,
multi-language support, and powerful management tools that go far beyond the original.

**Ketesa is a fully compatible drop-in replacement for Synapse Admin.** Migration is straightforward
and requires no configuration changes:

| | Method | How |
|---|---|---|
| ☁️ | **Hosted (CDN)** | Use [admin.etke.cc](https://admin.etke.cc) — nothing to install |
| 🐳 | **Docker** | Replace the image tag with `ghcr.io/etkecc/ketesa:latest` |
| 📦 | **Static files** | Replace your existing dist directory with the Ketesa release tarball |

Whether you're managing a small private server or a large federated community, Ketesa gives you
the visibility and control you need — all from a clean, responsive web interface.

> 💬 Questions? Join the [community room](https://matrix.to/#/#ketesa:etke.cc) or open an issue on GitHub.

---

## ✨ Features

### 👥 Complete user management

Ketesa covers the full lifecycle of a Matrix user account. You can suspend, [shadow-ban](./docs/user-management.md#-shadow-ban),
[deactivate, or permanently erase](./docs/user-management.md#-deactivation-vs-erasure) users. Fine-grained controls let you manage [rate limits](./docs/user-management.md#-rate-limits),
[experimental features](./docs/user-management.md#-experimental-features), and [account data](./docs/user-management.md#-account-data). You can view and manage third-party identifiers,
connected devices (create, rename, delete), room memberships, and cross-signing keys — all
from one place. Need to onboard many users at once? [Bulk registration via CSV import](./docs/csv-import.md) handles
it, including third-party identifiers. Passwords can be generated randomly or reset manually.
User avatars carry [role badges](./docs/user-badges.md) (admin, bot, support, federated, system-managed)
so you can identify account types at a glance.

When [Matrix Authentication Service (MAS)](https://github.com/element-hq/matrix-authentication-service)
is in use, Ketesa extends user management with [MAS-native capabilities](./docs/user-management.md#-mas-user-management): browsing and revoking
active sessions (compat, OAuth2, and personal), managing linked email addresses, reviewing
upstream OAuth provider links, and creating users through MAS directly.

[📄 User management guide](./docs/user-management.md)

### 🏠 Powerful room management

Get a full picture of every room on your server. Block or unblock rooms, purge history,
and delete rooms entirely. The [messages viewer](./docs/room-management.md#-messages-viewer) lets you browse room history with filters
and jump-to-date navigation. [Spaces are handled natively](./docs/room-management.md#-room-hierarchy) with a dedicated room hierarchy
tab. You can assign room admins and join users to rooms directly from the UI.
[Media](./docs/media.md) can be quarantined, protected, or deleted at file, user, or room scope.

[📄 Room management guide](./docs/room-management.md) · [📄 Media management guide](./docs/media.md)

### 🔐 Flexible authentication

Log in with a username and password, a raw access token, or via OIDC/SSO — whatever your
server setup requires. Ketesa has first-class support for [Matrix Authentication Service (MAS)](https://github.com/element-hq/matrix-authentication-service),
including full session management and [registration token administration](./docs/registration-tokens.md). It also ships a
dedicated [external auth provider mode](./docs/external-auth-provider.md) that adapts the interface
when Synapse delegates authentication to an external system.

[📄 Registration tokens guide](./docs/registration-tokens.md)

### ⚙️ Deep customization

Every data table in Ketesa is built with [react-admin's Configurable](https://marmelab.com/react-admin/Configurable.html)
component, so users can show, hide, and reorder columns to match their workflow — no code changes needed.

[📄 Configurable columns guide](./docs/configurable-columns.md)

Beyond the per-user table preferences, Ketesa can be tailored at the deployment level through a
[`config.json`](./docs/config.md) file (or via `/.well-known/matrix/client`):
[restrict which homeservers](./docs/restrict-hs.md) users can connect to,
[add custom navigation menu items](./docs/custom-menu.md),
[pre-fill the login form](./docs/prefill-login-form.md),
[configure CORS credentials](./docs/cors-credentials.md),
and [protect appservice-managed users](./docs/system-users.md) (bridge puppets) from accidental edits.

### 📊 Server statistics and insights

Monitor your server with built-in statistics views: [per-user media usage and database room
stats](./docs/server-statistics.md) give you a clear picture of what's consuming space. The [federation overview](./docs/federation.md) shows
the health and reachability of remote destinations. [Reported events](./docs/event-reports.md) can be reviewed and
acted upon directly from the reports list.

[📄 Server statistics guide](./docs/server-statistics.md) · [📄 Federation guide](./docs/federation.md) · [📄 Event reports guide](./docs/event-reports.md)

### 🌍 Available in 10 languages

Ketesa ships with full translations in English, German, French, Japanese, Russian, Persian,
Ukrainian, Chinese, Italian, and Portuguese — every string is fully translated, with no untranslated
English stubs left behind.

### 📱 Mobile-friendly by design

The interface is fully responsive. Wherever you are, you can manage your server from a
phone or tablet without sacrificing functionality. Tables collapse to readable mobile lists,
long identifiers wrap gracefully, and every action is reachable on small screens.

### 🌟 Built by etke.cc — and it shows

Ketesa is built and actively maintained by [etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=ketesa),
a managed Matrix hosting provider with a genuine [open-source-first](https://github.com/etkecc) philosophy.
Every feature in this project is open source, developed in the open, and free to use by anyone.

If you run your Matrix server on etke.cc, Ketesa becomes something even more powerful: a **unified
control plane for your entire server**. Instead of juggling separate dashboards, log files, and support
channels, everything you need is right here — in the same interface you already use for user and room
management:

| | Feature | What it does |
|---|---|---|
| 🟢 | **Server health** | Live status badge in the toolbar + a full dashboard showing every server component with color-coded indicators, error details, and suggested actions. Know what's wrong before your users do. |
| 🔔 | **Notifications** | Important server events surface as an in-app feed with an unread badge — nothing slips through the cracks. |
| ⚡ | **Server actions** | Trigger management commands on demand, schedule them for a precise date and time, or set up recurring weekly jobs. Routine maintenance becomes a few clicks, not a cron job. |
| 🧩 | **Components** | Browse, add, and remove server add-ons — bridges, bots, apps — from a self-service catalogue. See what's active, preview the cost impact, and request changes with one click. |
| 💳 | **Billing** | View payment history, transaction details, and download invoices without ever leaving the admin panel. |
| 💬 | **Support** | Open support requests, track their progress, and exchange messages with the etke.cc support — right from the interface you use every day. |
| 🎨 | **White-labelling** | Custom name, logo, favicon, and background applied automatically from the platform. No extra configuration, no deploy step. |

> 💡 **Interested?** [Learn more about etke.cc managed Matrix hosting →](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=ketesa)

---

## 📦 Availability

| | Where | Details |
|---|---|---|
| 🏠 | **[etke.cc](https://etke.cc/?utm_source=github&utm_medium=readme&utm_campaign=ketesa)** | Managed hosting with Ketesa built in |
| 🌐 | **[admin.etke.cc](https://admin.etke.cc)** | Hosted instance, always on the latest development version |
| 📦 | **[GitHub Releases](https://github.com/etkecc/ketesa/releases)** | Official prebuilt tarballs for root-path and `/admin` deployments |
| 🐳 | **[GHCR](https://github.com/etkecc/ketesa/pkgs/container/ketesa) / [Docker Hub](https://hub.docker.com/r/etkecc/ketesa/tags)** | Official container images |
| 🔧 | **[Source](https://github.com/etkecc/ketesa)** | Build from source or track `main` directly |

Official static builds:

- **`ketesa.tar.gz`** for root path deployment, such as `https://admin.example.com`
- **`ketesa-subpath-admin.tar.gz`** for `/admin` deployments, such as `https://example.com/admin`

For nightly builds, distro packages, Ansible integrations, and IPFS,
see the [full availability guide](./docs/availability.md).

---

## ⚙️ Configuration

Ketesa can be configured via a `config.json` file placed in the deployment root.
Additionally, your homeserver's `/.well-known/matrix/client` file can carry Ketesa-specific
configuration under the `cc.etke.ketesa` key — any instance of Ketesa will pick it up
automatically, making it easy to distribute settings to your users without touching the
deployment itself. Settings in `/.well-known/matrix/client` take precedence over `config.json`.

> **Note:** The legacy key `cc.etke.synapse-admin` is still supported for backward compatibility, but is deprecated.
> Please migrate to `cc.etke.ketesa` at your convenience.

If you use [spantaleev/matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy) or
[etkecc/ansible](https://github.com/etkecc/ansible),
configuration is automatically written to `/.well-known/matrix/client` for you.

[📄 Full configuration reference](./docs/config.md)

To inject a `config.json` into a Docker container, use a bind mount:

```yml
services:
  ketesa:
    ...
    volumes:
      - ./config.json:/var/public/config.json:ro
    ...
```

### 🔗 Prefilling the login form

Every field on the login page can be pre-filled via URL query parameters — handy for
sharing direct-access links with your users.

[Documentation](./docs/prefill-login-form.md)

### 🔒 Restricting available homeservers

Lock down the homeserver selection so users can only connect to servers you approve.
Useful for managed deployments where the homeserver should never change.

[Documentation](./docs/restrict-hs.md)

### 🌐 Configuring CORS credentials

Fine-tune the CORS credentials mode for your Ketesa deployment to match your server's
cross-origin policies.

[Documentation](./docs/cors-credentials.md)

### 🛡️ Protecting appservice-managed users

Bridge puppets and other appservice-managed accounts can be shielded from accidental
edits. Specify a list of MXID patterns (as regular expressions) to be restricted to
display name and avatar changes only.

[Documentation](./docs/system-users.md)

### 📋 Adding custom menu items

Extend the navigation menu with links to your own tools or documentation — no rebuild required.

[Documentation](./docs/custom-menu.md)

### 🔑 External auth provider mode

When Synapse delegates authentication to an external provider (OIDC, LDAP, and similar),
enable this mode to adjust Ketesa's behavior accordingly and avoid confusing UI elements
that don't apply in your setup.

[Documentation](./docs/external-auth-provider.md)

#### Matrix Authentication Service (MAS)

MAS requires a small amount of additional configuration to enable its admin API. See the
[designated MAS section](./docs/external-auth-provider.md#matrix-authentication-service-mas) for the details.

---

## 🚀 Usage

### Supported APIs

See [📄 Supported APIs](./docs/apis.md) for a full list of API endpoints used by Ketesa.

### Supported Synapse versions

Ketesa requires [Synapse](https://github.com/element-hq/synapse) **v1.150.0 or newer** for all
features to work correctly.

You can verify your server version by calling `/_synapse/admin/v1/server_version`,
or simply look at the version indicator that appears below the homeserver URL field on
the Ketesa login page.

See also: [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html)

### Prerequisites

Your browser needs access to the following endpoints on your homeserver:

- `/_matrix`
- `/_synapse/admin`

See also: [Synapse administration endpoints](https://element-hq.github.io/synapse/latest/reverse_proxy.html#synapse-administration-endpoints)

### ☁️ Use without installing anything

The hosted version at [admin.etke.cc](https://admin.etke.cc) is always up to date and
requires no installation. Just open it in your browser, enter your homeserver URL, and
log in with your admin account.

> 🔒 Your browser must be able to reach `/_synapse/admin` on your homeserver. The endpoints
> do not need to be exposed to the public internet — access from your local network is sufficient.

### 📥 Step-by-step installation

Choose your preferred method:

| | Method | Best for |
|---|---|---|
| [1️⃣](#steps-for-1) | **Tarball + webserver** | Any static hosting, full control |
| [2️⃣](#steps-for-2) | **Source + Node.js** | Development or custom builds |
| [3️⃣](#steps-for-3) | **Docker** | Containerized deployments |

#### Steps for 1)

- Make sure you have a webserver installed that can serve static files (nginx, Apache, Caddy, or anything else will work)
- Configure a virtual host for Ketesa on your webserver
- Download the appropriate `.tar.gz` from the [latest release](https://github.com/etkecc/ketesa/releases/latest):
  - `ketesa.tar.gz` for root path deployment (e.g., `https://admin.example.com`)
  - `ketesa-subpath-admin.tar.gz` for `/admin` subpath deployment (e.g., `https://example.com/admin`)
- Unpack the archive and place the contents in your virtual host's document root
- Open the URL in your browser

[📄 Reverse proxy configuration examples](./docs/reverse-proxy.md)

#### Steps for 2)

- Make sure you have git, yarn, and Node.js installed
- Clone the repository: `git clone https://github.com/etkecc/ketesa.git`
- Enter the directory: `cd ketesa`
- Install dependencies: `yarn install`
- Start the development server: `yarn start`

#### Steps for 3)

- Run the Docker container: `docker run -p 8080:8080 ghcr.io/etkecc/ketesa`

  Or use the provided [docker-compose.yml](docker/docker-compose.yml):

  ```sh
  docker-compose -f docker/docker-compose.yml up -d
  ```

  > **Note:** If you're building on a non-amd64 architecture (e.g., Raspberry Pi), set a Node.js
  > memory cap to prevent OOM failures during the build: `NODE_OPTIONS="--max_old_space_size=1024"`.

  > **Note:** On IPv4-only systems, set `SERVER_HOST=0.0.0.0` so Ketesa binds correctly.

  To build your own image from source:

  ```yml
  services:
    ketesa:
      container_name: ketesa
      hostname: ketesa
      build:
        context: https://github.com/etkecc/ketesa.git
        dockerfile: docker/Dockerfile.build
        args:
          - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
        #   - NODE_OPTIONS="--max_old_space_size=1024"
        #   - BASE_PATH="/ketesa"
      ports:
        - "8080:8080"
      restart: unless-stopped
  ```

- Open http://localhost:8080 in your browser

### 🛤️ Serving Ketesa under a custom path

The base path is baked in at build time and cannot be changed at runtime.

- For `/admin` specifically: use the prebuilt `ketesa-subpath-admin` tarball from [GitHub Releases](https://github.com/etkecc/ketesa/releases) or the `dist-subpath-admin` artifact from [GitHub Actions](https://github.com/etkecc/ketesa/actions/workflows/workflow.yml), or the `*-subpath-admin` Docker image tag.
- For root path: use `ketesa.tar.gz` or the `dist-root` artifact.
- For any other prefix: build from source with `yarn build --base=/my-prefix`, or pass the `BASE_PATH` build argument to Docker.

If you need a reverse proxy to expose Ketesa under a different base path without rebuilding,
here is a Traefik example:

`docker-compose.yml`

```yml
services:
  traefik:
    image: traefik:v3
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

  ketesa:
    image: ghcr.io/etkecc/ketesa:latest
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.admin.rule=Host(`example.com`) && PathPrefix(`/admin`)"
      - "traefik.http.services.admin.loadbalancer.server.port=8080"
      - "traefik.http.middlewares.admin-slashless-redirect.redirectregex.regex=(/admin)$$"
      - "traefik.http.middlewares.admin-slashless-redirect.redirectregex.replacement=$${1}/"
      - "traefik.http.middlewares.admin-strip-prefix.stripprefix.prefixes=/admin"
      - "traefik.http.routers.admin.middlewares=admin-slashless-redirect,admin-strip-prefix"
```

---

## 🛠️ Development

- See https://yarnpkg.com/getting-started/editor-sdks for IDE setup instructions

| Command | What it does |
|---|---|
| `yarn lint` | Run all style and linter checks |
| `yarn test` | Run all unit tests |
| `yarn fix` | Auto-fix coding style issues |
| `just run-dev` | Spin up the full local development stack |

`just run-dev` launches a complete local environment: a Synapse homeserver, Element Web, and a Postgres
database. The app starts in development mode at `http://localhost:5173`.
(If user creation fails on first run, re-run the command — the server may still be starting up.)

Open [http://localhost:5173](http://localhost:5173?username=admin&password=admin&server=http://localhost:8008) and log in with:

| Field | Value |
|---|---|
| Login | `admin` |
| Password | `admin` |
| Homeserver URL | `http://localhost:8008` |

Element Web is available at http://localhost:8080.


================================================
FILE: REUSE.toml
================================================
version = 1

[[annotations]]
path = ["**"]
SPDX-FileCopyrightText = [
  "2018-2023 Awesome Technologies Innovationslabor GmbH",
  "2024-2026 etke.cc team <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/Datagrid.test.tsx"]
SPDX-FileCopyrightText = "2026 Nikita Chernyi <https://etke.cc>"
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".dockerignore"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [".editorconfig"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [".gitattributes"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [".github/CONTRIBUTING.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".github/ISSUE_TEMPLATE/bug_report.md"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".github/ISSUE_TEMPLATE/feature_request.md"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".github/SECURITY.md"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".github/dependabot.yml"]
SPDX-FileCopyrightText = [
  "2023 Dirk Klimpel",
  "2024 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".github/workflows/reuse.yml"]
SPDX-FileCopyrightText = "2022 Free Software Foundation Europe e.V. <https://fsfe.org>"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [".github/workflows/workflow.yml"]
SPDX-FileCopyrightText = [
  "2021 sakkiii",
  "2022 Dominik Fuchß",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [".gitignore"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [".prettierignore"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = [".watchmanconfig"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = ["docker/Dockerfile"]
SPDX-FileCopyrightText = [
  "2019-2024 Manuel Stahl",
  "2021 Aaron Raimist",
  "2022 Leon Schmidt",
  "2023 Michael Albert",
  "2024 Gavin Mogan",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2025 Dirk Klimpel",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docker/Dockerfile.build"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["LICENSE"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["README.md"]
SPDX-FileCopyrightText = [
  "2020-2024 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2020-2021 Michael Albert",
  "2021 csett86",
  "2021 Lukas Wolfsteiner",
  "2022 Leon Schmidt",
  "2023 Sebastian Wagner",
  "2023-2024 sebix",
  "2024 Andreas Schildbach",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 DrMaxNix",
  "2025 Max",
  "2026 mawise",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docker/docker-compose-dev.yml"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docker/docker-compose.yml"]
SPDX-FileCopyrightText = [
  "2021 Lukas Wolfsteiner",
  "2023 Dirk Klimpel",
  "2023 Michael Albert",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/README.md"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/components.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/config.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/configurable-columns.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/cors-credentials.md"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/csv-import.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/custom-menu.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/event-reports.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/external-auth-provider.md"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
  "2026 cy1der",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/well-known-discovery.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/federation.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/media.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/prefill-login-form.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/registration-tokens.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/restrict-hs.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/reverse-proxy.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 cy1der",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/room-management.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/server-statistics.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/system-users.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/user-badges.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/user-management.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/user-search.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["eslint.config.js"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/vitest.setup.ts"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["justfile"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["package.json"]
SPDX-FileCopyrightText = [
  "2020-2024 Manuel Stahl",
  "2020-2022 Michael Albert",
  "2021 Nya Candy",
  "2022-2025 Dirk Klimpel",
  "2023 Charlie Calendre",
  "2023 Francesco Carmelo Capria",
  "2024 Fateme Shamohammadi",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Suguru Hirahara <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["public/config.json"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["public/data/example.csv"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2024 Manuel Stahl",
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["public/favicon.ico"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2024 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["public/images/logo.webp"]
SPDX-FileCopyrightText = [
  "2024 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["public/robots.txt"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2024 Dirk Klimpel",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/screenshots/dark/**"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/screenshots/light/**"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"


[[annotations]]
path = ["src/App.test.tsx"]
SPDX-FileCopyrightText = [
  "2020-2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Dirk Klimpel",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/App.tsx"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2021 Nya Candy",
  "2023 Charlie Calendre",
  "2023 Francesco Carmelo Capria",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Suguru Hirahara <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/Context.tsx"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/README.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/AdminLayout.tsx"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/fields/AvatarField.test.tsx"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/fields/AvatarField.tsx"]
SPDX-FileCopyrightText = [
  "2023 Dirk Klimpel",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/DeleteRoomButton.tsx"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/DeleteUserButton.tsx"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/DeviceRemoveButton.tsx"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
  "2024-2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/ExperimentalFeatures.tsx"]
SPDX-FileCopyrightText = [
  "2024-2025 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/Footer.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/Footer.tsx"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/MatrixWordmark.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/DonatePage.tsx", "src/pages/DonatePage.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/LoginFormBox.tsx"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/index.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/ServerNotices.tsx"]
SPDX-FileCopyrightText = [
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/UserAccountData.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/UserRateLimits.tsx"]
SPDX-FileCopyrightText = [
  "2024-2025 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/BillingStatusBadge.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/BillingPage.tsx"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
  "2025-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/CurrentlyRunningCommand.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ComponentsPage.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/EtkeAttribution.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/EtkeAttribution.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/InstanceConfig.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/README.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/RichTextEditor.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerActionsPage.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerCommandsPanel.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerNotificationsBadge.tsx"]
SPDX-FileCopyrightText = [
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerNotificationsPage.tsx"]
SPDX-FileCopyrightText = [
  "2024-2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerNotificationsUnavailable.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerNotificationsBadge.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerNotificationsUnavailable.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/etke.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerStatusBadge.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerStatusBadge.tsx"]
SPDX-FileCopyrightText = [
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerStatusPage.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/ServerStatusPage.tsx"]
SPDX-FileCopyrightText = [
  "2024-2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/SupportAttachments.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/SupportPage.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/SupportRequestPage.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/hooks/useServerCommands.ts"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
  "2025-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/recurring/RecurringCommandEdit.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/recurring/RecurringDeleteButton.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/scheduled/ScheduledCommandShow.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/components/scheduled/ScheduledDeleteButton.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/hooks/useRecurringCommands.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/schedules/hooks/useScheduledCommands.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/hooks/useDocTitle.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/hooks/useDocTitle.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/media/DeleteMediaButton.tsx"]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2024 Alexander Tumin",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/media/index.ts"]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2024 Alexander Tumin",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/media/ProtectMediaButton.tsx"]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2024 Alexander Tumin",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/media/PurgeRemoteMediaButton.tsx"]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2024 Alexander Tumin",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/media/QuarantineMediaButton.tsx"]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2024 Alexander Tumin",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/media/ViewMedia.tsx"]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2024 Alexander Tumin",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/ConflictModeCard.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/ErrorsCard.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/ResultsCard.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/StartImportCard.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/StatsCard.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/UploadCard.tsx"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/UserImport.tsx"]
SPDX-FileCopyrightText = [
  "2024 jamazi",
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025 milkomeda",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/types.ts"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/useImportFile.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/user-import/useImportFile.tsx"]
SPDX-FileCopyrightText = [
  "2025 Borislav Pantaleev <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/README.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/de/common.ts",
  "src/i18n/de/index.ts",
  "src/i18n/de/mas.ts",
  "src/i18n/de/misc_resources.ts",
  "src/i18n/de/reports.ts",
  "src/i18n/de/rooms.ts",
  "src/i18n/de/users.ts",
]
SPDX-FileCopyrightText = [
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2020-2021 Michael Albert",
  "2022 Nya Candy",
  "2023 Przemysław Romanik",
  "2024 Alexander Tumin",
  "2024 ll-SKY-ll",
  "2024 Steffo",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/en/common.ts",
  "src/i18n/en/index.ts",
  "src/i18n/en/mas.ts",
  "src/i18n/en/misc_resources.ts",
  "src/i18n/en/reports.ts",
  "src/i18n/en/rooms.ts",
  "src/i18n/en/users.ts",
]
SPDX-FileCopyrightText = [
  "2020-2024 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2020-2022 Michael Albert",
  "2021 John Francis Sukamto",
  "2022 Nya Candy",
  "2023 Charlie Calendre",
  "2023 Przemysław Romanik",
  "2024 Alexander Tumin",
  "2024 rkfg",
  "2024 Steffo",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
  "2025 Suguru Hirahara <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/fa/common.ts",
  "src/i18n/fa/index.ts",
  "src/i18n/fa/mas.ts",
  "src/i18n/fa/misc_resources.ts",
  "src/i18n/fa/reports.ts",
  "src/i18n/fa/rooms.ts",
  "src/i18n/fa/users.ts",
]
SPDX-FileCopyrightText = [
  "2024 Fateme Shamohammadi",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/fr/common.ts",
  "src/i18n/fr/index.ts",
  "src/i18n/fr/mas.ts",
  "src/i18n/fr/misc_resources.ts",
  "src/i18n/fr/reports.ts",
  "src/i18n/fr/rooms.ts",
  "src/i18n/fr/users.ts",
]
SPDX-FileCopyrightText = [
  "2023 Charlie Calendre",
  "2023 Michael Albert",
  "2024 Dirk Klimpel",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/i18n-keys.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/it/common.ts",
  "src/i18n/it/index.ts",
  "src/i18n/it/mas.ts",
  "src/i18n/it/misc_resources.ts",
  "src/i18n/it/reports.ts",
  "src/i18n/it/rooms.ts",
  "src/i18n/it/users.ts",
]
SPDX-FileCopyrightText = [
  "2023 Francesco Carmelo Capria",
  "2024 Dirk Klimpel",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/ja/common.ts",
  "src/i18n/ja/index.ts",
  "src/i18n/ja/mas.ts",
  "src/i18n/ja/misc_resources.ts",
  "src/i18n/ja/reports.ts",
  "src/i18n/ja/rooms.ts",
  "src/i18n/ja/users.ts",
]
SPDX-FileCopyrightText = [
  "2025 Suguru Hirahara <https://etke.cc>",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/ru/common.ts",
  "src/i18n/ru/index.ts",
  "src/i18n/ru/mas.ts",
  "src/i18n/ru/misc_resources.ts",
  "src/i18n/ru/reports.ts",
  "src/i18n/ru/rooms.ts",
  "src/i18n/ru/users.ts",
]
SPDX-FileCopyrightText = [
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Dirk Klimpel",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/uk/common.ts",
  "src/i18n/uk/index.ts",
  "src/i18n/uk/mas.ts",
  "src/i18n/uk/misc_resources.ts",
  "src/i18n/uk/reports.ts",
  "src/i18n/uk/rooms.ts",
  "src/i18n/uk/users.ts",
]
SPDX-FileCopyrightText = [
  "2025 khvalera",
  "2025-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
  "src/i18n/zh/common.ts",
  "src/i18n/zh/index.ts",
  "src/i18n/zh/mas.ts",
  "src/i18n/zh/misc_resources.ts",
  "src/i18n/zh/reports.ts",
  "src/i18n/zh/rooms.ts",
  "src/i18n/zh/users.ts",
]
SPDX-FileCopyrightText = [
  "2021-2025 Dirk Klimpel",
  "2021-2024 Manuel Stahl",
  "2021-2022 Nya Candy",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 WriteMemory",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/de/base.ts"]
SPDX-FileCopyrightText = ["Haleos UG (haftungsbeschränkt)"]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["src/i18n/fa/base.ts"]
SPDX-FileCopyrightText = ["Hamid Feizabadi"]
SPDX-License-Identifier = "BSD-2-Clause"

[[annotations]]
path = ["src/i18n/it/base.ts"]
SPDX-FileCopyrightText = ["Stefano Savanelli"]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["src/i18n/ja/base.ts"]
SPDX-FileCopyrightText = ["2019 Oishi Takanori"]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["src/i18n/ru/base.ts"]
SPDX-FileCopyrightText = ["Klucherev Alexey"]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["src/i18n/uk/base.ts"]
SPDX-FileCopyrightText = ["Vasyl Boroviak"]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["src/i18n/zh/base.ts"]
SPDX-FileCopyrightText = ["Haxqer", "Moca-Tech"]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = [
  "src/i18n/pt/common.ts",
  "src/i18n/pt/index.ts",
  "src/i18n/pt/mas.ts",
  "src/i18n/pt/misc_resources.ts",
  "src/i18n/pt/reports.ts",
  "src/i18n/pt/rooms.ts",
  "src/i18n/pt/users.ts",
]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/pt/base.ts"]
SPDX-FileCopyrightText = [
  "2017 Wedney Yuri",
  "2017 Willian Ribeiro Angelo",
  "2018-2020 Gabriel Marques",
  "2018 Giuseppe Menti",
  "2019 Marcos Fernando Costa",
  "2019 Matheus Wichman",
  "2020 Alexsander da Rosa",
  "2020 Eduardo Gomes Bonilha",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "MIT"

[[annotations]]
path = ["src/index.tsx"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2025 Dirk Klimpel",
  "2025 Patrick Kranz",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resourceMap.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/LoginPage.test.tsx"]
SPDX-FileCopyrightText = [
  "2020-2024 Manuel Stahl",
  "2023 Dirk Klimpel",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/LoginPage.tsx"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020 teutates",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2021-2022 Nya Candy",
  "2023 Charlie Calendre",
  "2023 Francesco Carmelo Capria",
  "2023 Stefan Möhrle",
  "2024 Fateme Shamohammadi",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Hugo Renard",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/README.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/destinations/index.ts", "src/resources/destinations/List.tsx"]
SPDX-FileCopyrightText = [
  "2023-2024 Dirk Klimpel",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2025 rkfg",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/registration-tokens/Create.tsx", "src/resources/registration-tokens/Edit.tsx", "src/resources/registration-tokens/index.ts", "src/resources/registration-tokens/List.tsx"]
SPDX-FileCopyrightText = [
  "2022-2024 Dirk Klimpel",
  "2023-2024 sebix",
  "2024 Manuel Stahl",
  "2024 Sebastian Wagner",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/reports/index.ts", "src/resources/reports/List.tsx", "src/resources/reports/Show.tsx"]
SPDX-FileCopyrightText = [
  "2021-2024 Dirk Klimpel",
  "2023 Ezwen",
  "2024 Alexander Tumin",
  "2024 Manuel Stahl",
  "2024 Steffo",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/room-directory/index.tsx"]
SPDX-FileCopyrightText = [
  "2021 Michael Albert",
  "2021-2025 Dirk Klimpel",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/statistics/DatabaseRooms.tsx", "src/resources/statistics/index.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/rooms/index.ts"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/rooms/List.tsx"]
SPDX-FileCopyrightText = [
  "2020-2024 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2020-2021 Michael Albert",
  "2022 Timo Gurr",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/rooms/Show.tsx"]
SPDX-FileCopyrightText = [
  "2020-2024 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2020-2021 Michael Albert",
  "2022 Timo Gurr",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/statistics/UserMedia.tsx"]
SPDX-FileCopyrightText = [
  "2021-2024 Dirk Klimpel",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/CompatSessions.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/index.ts"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/OAuth2Sessions.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/PersonalSessions.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/shared.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/UpstreamOAuthLinks.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/UpstreamOAuthProviders.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/UserEmails.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/mas/UserSessions.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/users/Create.tsx"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2023 Przemysław Romanik",
  "2024 Alexander Tumin",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/users/Edit.tsx"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2023 Przemysław Romanik",
  "2024 Alexander Tumin",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/users/index.ts"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2023 Przemysław Romanik",
  "2024 Alexander Tumin",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/users/List.tsx"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2023 Przemysław Romanik",
  "2024 Alexander Tumin",
  "2024 rkfg",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/config.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/config.ts"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/date.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/date.ts"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/error.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/error.ts"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/fetchMedia.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/fetchMedia.ts"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/icons.ts"]
SPDX-FileCopyrightText = [
  "2024-2025 Nikita Chernyi <https://etke.cc>",
  "2025 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/logger.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/mxid.ts"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/mxid.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/password.ts"]
SPDX-FileCopyrightText = [
  "2024-2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/password.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/element/config.json"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/element/nginx.conf"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/mas/config.yaml"]
SPDX-FileCopyrightText = [
  "2025-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 cy1der",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/nginx/nginx.conf"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/postgres.initdb/mas.sql"]
SPDX-FileCopyrightText = [
  "2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/synapse/homeserver.yaml"]
SPDX-FileCopyrightText = [
  "2024-2025 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/synapse/synapse.log.config"]
SPDX-FileCopyrightText = [
  "2024 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/testdata/synapse/synapse.signing.key"]
SPDX-FileCopyrightText = [
  "2024 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["tsconfig.eslint.json"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["tsconfig.json"]
SPDX-FileCopyrightText = [
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["tsconfig.vite.json"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["vite.config.ts"]
SPDX-FileCopyrightText = [
  "2024 Dirk Klimpel",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["yarn.lock"]
SPDX-FileCopyrightText = "NONE"
SPDX-License-Identifier = "CC0-1.0"

[[annotations]]
path = ["src/entrypoints/index.html"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/README.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/auth/index.ts"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2020 teutates",
  "2020-2024 Manuel Stahl",
  "2021 Aaron Raimist",
  "2021-2025 Dirk Klimpel",
  "2022 Nya Candy",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/auth/index.test.ts"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Manuel Stahl",
  "2025 Dirk Klimpel",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/index.ts"]
SPDX-FileCopyrightText = [
  "2020 Lukas Winkler",
  "2020 Michael Albert",
  "2020 rkfg",
  "2020 teutates",
  "2020-2025 Dirk Klimpel",
  "2020-2024 Manuel Stahl",
  "2023 Charlie Calendre",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
  "2026 cy1der",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/index.test.ts"]
SPDX-FileCopyrightText = [
  "2020-2026 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/matrix.ts"]
SPDX-FileCopyrightText = [
  "2024 Alexander Tumin",
  "2024 Manuel Stahl",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 cy1der",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/matrix.test.ts"]
SPDX-FileCopyrightText = [
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/types.d.ts"]
SPDX-FileCopyrightText = [
  "2024 Dirk Klimpel",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2024-2026 Borislav Pantaleev <https://etke.cc>",
  "2025 Huw Carpenter",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/etke.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/http.ts"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/mas.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/serverVersion.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/synapse.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/lifecycle.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/mas-actions.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/mas-utils.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/mas-utils.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/scan.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/data/synapse-actions.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/common.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/destinations.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/etke.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/index.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/mas.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/reports.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/rooms.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/providers/types/users.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/index.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/i18n/index.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/assets/webfonts/Inter-*"]
SPDX-FileCopyrightText = "2016 The Inter Project Authors (https://github.com/rsms/inter)"
SPDX-License-Identifier = "OFL-1.1"

[[annotations]]
path = ["src/assets/webfonts/WorkSans-*"]
SPDX-FileCopyrightText = "2015 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans)"
SPDX-License-Identifier = "OFL-1.1"

[[annotations]]
path = ["docker/Dockerfile.subpath-admin"]
SPDX-FileCopyrightText = [
  "2019-2024 Manuel Stahl",
  "2021 Aaron Raimist",
  "2022 Leon Schmidt",
  "2023 Michael Albert",
  "2024 Gavin Mogan",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2025 Dirk Klimpel",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/apis.md"]
SPDX-FileCopyrightText = [
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/update-api-docs.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/screenshots/prepare.js"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["docs/screenshots/README.md"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/assets/fonts.css"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/assets/theme.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/AdminClientConfigItems.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/AllowCrossSigningButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/BlockRoomButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/DeviceCreateButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/DeviceDisplayNameInput.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/fields/EditableAvatarField.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/Datagrid.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/EmptyState.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/EmptyState.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Borislav Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/layout/List.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/FindUserButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/LoginAsUserButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/PurgeHistoryButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/DeleteAllMediaButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/QuarantineAllMediaButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/RenewAccountValidityButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/buttons/ResetPasswordButton.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/rooms/RoomHierarchy.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/rooms/RoomHierarchy.test.ts"]
SPDX-FileCopyrightText = ["2026 Nikita Chernyi <https://etke.cc>"]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/rooms/EventLookupDialog.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/rooms/RoomMessages.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/users/UserCounts.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/components/etke.cc/hooks/useUnits.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/entrypoints/auth-callback.html"]
SPDX-FileCopyrightText = [
  "2020 Michael Albert",
  "2024 Manuel Stahl",
  "2024 Borislav Pantaleev <https://etke.cc>",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/auth-callback-error.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/auth-callback-error.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/version.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/auth-callback.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/auth-callback.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
  "2026 Slavi Pantaleev <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/MASPolicyDataPage.test.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/pages/MASPolicyDataPage.tsx"]
SPDX-FileCopyrightText = [
  "2026 Borislav Pantaleev <https://etke.cc>",
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/resources/scheduled-tasks/index.ts", "src/resources/scheduled-tasks/List.tsx"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/formatBytes.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/formatBytes.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/safety.test.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/safety.ts"]
SPDX-FileCopyrightText = [
  "2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = ["src/utils/version.ts"]
SPDX-FileCopyrightText = [
  "2024 Dirk Klimpel",
  "2024 Manuel Stahl",
  "2024-2026 Nikita Chernyi <https://etke.cc>",
]
SPDX-License-Identifier = "Apache-2.0"


================================================
FILE: docker/Dockerfile
================================================
FROM ghcr.io/static-web-server/static-web-server:2-alpine

ARG VERSION=dev
ARG VCS_REF=unknown
ARG BUILD_DATE

LABEL org.opencontainers.image.title="Ketesa" \
      org.opencontainers.image.description="Admin UI for Matrix servers, formerly Synapse Admin" \
      org.opencontainers.image.url="https://admin.etke.cc" \
      org.opencontainers.image.documentation="https://github.com/etkecc/ketesa#readme" \
      org.opencontainers.image.source="https://github.com/etkecc/ketesa" \
      org.opencontainers.image.vendor="etke.cc" \
      org.opencontainers.image.authors="etke.cc" \
      org.opencontainers.image.licenses="Apache-2.0" \
      org.opencontainers.image.version="$VERSION" \
      org.opencontainers.image.revision="$VCS_REF" \
      org.opencontainers.image.created="$BUILD_DATE" \
      org.opencontainers.image.base.name="ghcr.io/static-web-server/static-web-server:2-alpine" \
      description="Admin UI for Matrix servers, formerly Synapse Admin" \
      version="$VERSION" \
      maintainer="etke.cc"

# You can set environment variables as `docker run` arguments too,
# full list: https://static-web-server.net/configuration/environment-variables/
ENV SERVER_FALLBACK_PAGE=/var/public/index.html
ENV SERVER_PORT=8080
ENV SERVER_HEALTH=true

HEALTHCHECK --interval=30s --timeout=10s CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

USER $SERVER_USER_NAME:$SERVER_GROUP_NAME

COPY --chown=$SERVER_USER_NAME:$SERVER_GROUP_NAME ./dist /home/$SERVER_USER_NAME/public


================================================
FILE: docker/Dockerfile.build
================================================
FROM node:lts AS builder
ARG BASE_PATH=./
WORKDIR /src
COPY . /src
RUN yarn config set enableTelemetry 0 && \
    yarn install --immutable --network-timeout=300000 --pure-lockfile && \
    NODE_ENV=production yarn build --base=$BASE_PATH

FROM ghcr.io/static-web-server/static-web-server:2-alpine
ARG VERSION=dev
ARG VCS_REF=unknown
ARG BUILD_DATE

LABEL org.opencontainers.image.title="Ketesa" \
      org.opencontainers.image.description="Admin UI for Matrix servers, formerly Synapse Admin" \
      org.opencontainers.image.url="https://admin.etke.cc" \
      org.opencontainers.image.documentation="https://github.com/etkecc/ketesa#readme" \
      org.opencontainers.image.source="https://github.com/etkecc/ketesa" \
      org.opencontainers.image.vendor="etke.cc" \
      org.opencontainers.image.authors="etke.cc" \
      org.opencontainers.image.licenses="Apache-2.0" \
      org.opencontainers.image.version="$VERSION" \
      org.opencontainers.image.revision="$VCS_REF" \
      org.opencontainers.image.created="$BUILD_DATE" \
      org.opencontainers.image.base.name="ghcr.io/static-web-server/static-web-server:2-alpine" \
      description="Admin UI for Matrix servers, formerly Synapse Admin" \
      version="$VERSION" \
      maintainer="etke.cc"

# You can set environment variables as `docker run` arguments too,
# full list: https://static-web-server.net/configuration/environment-variables/
ENV SERVER_FALLBACK_PAGE=/var/public/index.html
ENV SERVER_PORT=8080
ENV SERVER_HEALTH=true

HEALTHCHECK --interval=30s --timeout=10s CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

USER $SERVER_USER_NAME:$SERVER_GROUP_NAME

COPY --from=builder --chown=$SERVER_USER_NAME:$SERVER_GROUP_NAME /src/dist /home/$SERVER_USER_NAME/public


================================================
FILE: docker/Dockerfile.subpath-admin
================================================
FROM ghcr.io/static-web-server/static-web-server:2-alpine

ARG VERSION=dev
ARG VCS_REF=unknown
ARG BUILD_DATE

LABEL org.opencontainers.image.title="Ketesa (/admin)" \
      org.opencontainers.image.description="Admin UI for Matrix servers, formerly Synapse Admin, served from the /admin subpath" \
      org.opencontainers.image.url="https://admin.etke.cc" \
      org.opencontainers.image.documentation="https://github.com/etkecc/ketesa#readme" \
      org.opencontainers.image.source="https://github.com/etkecc/ketesa" \
      org.opencontainers.image.vendor="etke.cc" \
      org.opencontainers.image.authors="etke.cc" \
      org.opencontainers.image.licenses="Apache-2.0" \
      org.opencontainers.image.version="$VERSION" \
      org.opencontainers.image.revision="$VCS_REF" \
      org.opencontainers.image.created="$BUILD_DATE" \
      org.opencontainers.image.base.name="ghcr.io/static-web-server/static-web-server:2-alpine" \
      description="Admin UI for Matrix servers, formerly Synapse Admin, served from the /admin subpath" \
      version="$VERSION" \
      maintainer="etke.cc"

# You can set environment variables as `docker run` arguments too,
# full list: https://static-web-server.net/configuration/environment-variables/
ENV SERVER_FALLBACK_PAGE=/var/public/admin/index.html
ENV SERVER_PORT=8080
ENV SERVER_HEALTH=true

HEALTHCHECK --interval=30s --timeout=10s CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

USER $SERVER_USER_NAME:$SERVER_GROUP_NAME

COPY --chown=$SERVER_USER_NAME:$SERVER_GROUP_NAME ./dist /home/$SERVER_USER_NAME/public/admin


================================================
FILE: docker/docker-compose-dev.yml
================================================
services:
  synapse:
    image: ghcr.io/element-hq/synapse:latest
    entrypoint: python
    command: "-m synapse.app.homeserver -c /config/homeserver.yaml"
    volumes:
      - ../docs/testdata/synapse:/config
      - ../docs/testdata/synapse.data:/media-store

  mas:
    image: ghcr.io/element-hq/matrix-authentication-service:latest
    ports:
      - "8007:8007"
    volumes:
      - ../docs/testdata/mas/config.yaml:/config.yaml:ro

  nginx:
    image: nginx:latest
    ports:
      - "8008:8008"
    volumes:
      - ../docs/testdata/nginx/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro
      - ../dist:/var/www/html/admin:ro

  ketesa-prod:
    image: ghcr.io/etkecc/ketesa

  element:
    image: docker.io/vectorim/element-web:latest
    depends_on:
      synapse:
        condition: service_healthy
        restart: true
    ports:
      - "8080:8080"
    volumes:
      - ../docs/testdata/element/nginx.conf:/etc/nginx/nginx.conf:ro
      - /dev/null:/etc/nginx/conf.d/default.conf:ro
      - ../docs/testdata/element/config.json:/app/config.json:ro

  mock-oidc:
    image: ghcr.io/navikt/mock-oauth2-server:2.1.10
    environment:
      SERVER_PORT: 8006
      LOG_LEVEL: DEBUG
      JSON_CONFIG: >
        {
          "interactiveLogin": false,
          "tokenCallbacks": [
            {
              "issuerId": "default",
              "requestMappings": [
                {
                  "requestParam": "grant_type",
                  "match": ".*",
                  "claims": {
                    "sub": "mock-user",
                    "preferred_username": "mockuser",
                    "name": "Mock User",
                    "email": "mock@localhost"
                  }
                }
              ]
            }
          ]
        }
    ports:
      - "8006:8006"

  postgres:
    image: postgres:17-alpine
    ports:
      - "5432:5432"
    volumes:
      - ../docs/testdata/postgres.data:/var/lib/postgresql/data
      - ../docs/testdata/postgres.initdb:/docker-entrypoint-initdb.d:ro
    environment:
      POSTGRES_USER: synapse
      POSTGRES_PASSWORD: synapse
      POSTGRES_DB: synapse
      POSTGRES_INITDB_ARGS: "--lc-collate C --lc-ctype C --encoding UTF8"


================================================
FILE: docker/docker-compose.yml
================================================
services:
  ketesa:
    container_name: ketesa
    hostname: ketesa
    image: ghcr.io/etkecc/ketesa:latest
    # build:
    #   context: .
    #   dockerfile: docker/Dockerfile.build

    # to use the docker-compose as standalone without a local repo clone,
    # replace the context definition with this:
    # context: https://github.com/etkecc/ketesa.git

    #  args:
    #    - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
    #    if you're building on an architecture other than amd64, make sure
    #    to define a maximum ram for node. otherwise the build will fail.
    #    - NODE_OPTIONS="--max_old_space_size=1024"
    #    - BASE_PATH="/ketesa"
    ports:
      - "8080:8080"
    restart: unless-stopped


================================================
FILE: docs/README.md
================================================
# 📚 Documentation

Welcome to the Ketesa documentation! This is the central index for all guides covering configuration, supported APIs, and available features.

> 📝 **Note:** Documentation is actively evolving — PRs are greatly appreciated!

---

## ⚙️ Configuration

[Full configuration reference →](./config.md)

Specific topics:

| Guide | What it covers |
|-------|----------------|
| [Customizing CORS credentials](./cors-credentials.md) | Fine-tune cross-origin request behavior |
| [Restricting available homeservers](./restrict-hs.md) | Lock down which homeservers users can connect to |
| [System / Appservice-managed users](./system-users.md) | Protect bridge puppets from accidental edits |
| [Custom menu items](./custom-menu.md) | Add your own links to the navigation menu |
| [External auth provider](./external-auth-provider.md) | Adjust behavior when Synapse delegates auth externally |

---

## 🔌 APIs

* [Supported APIs](./apis.md) — full list of Synapse and MAS endpoints used by Ketesa

---

## ✨ Features

* [User badges](./user-badges.md) — role indicators on user avatars (admin, bot, system-managed, etc.)
* [Prefilling the login form](./prefill-login-form.md) — pre-populate login fields via URL parameters
* [Configurable columns](./configurable-columns.md) — show, hide, and reorder table columns per your workflow

### 👥 User Management

| Guide | What it covers |
|-------|----------------|
| [User management](./user-management.md) | Login-as-user, shadow ban, rate limits, experimental features, account data, server notices, MAS user management, MAS policy data |
| [User search](./user-search.md) | Normal and reverse (`!`) MXID/displayname search |
| [Bulk CSV import](./csv-import.md) | Import many users at once from a CSV file |
| [Registration tokens](./registration-tokens.md) | Create and manage invite tokens for Synapse and MAS |

### 🏠 Room Management

| Guide | What it covers |
|-------|----------------|
| [Room management](./room-management.md) | Block/unblock, purge history, delete, join users, assign admins, members/state/extremities tabs, messages viewer, Space hierarchy |
| [Media management](./media.md) | Quarantine, protect, and delete media at file/user/room scope |

### 🔍 Moderation

* [Event reports](./event-reports.md) — review abuse reports submitted by users, use the event lookup tool

### 📡 Federation

* [Federation overview](./federation.md) — monitor remote server connections, reconnect failed destinations

### 📊 Statistics & Tasks

* [Server statistics & scheduled tasks](./server-statistics.md) — database room sizes, user media usage, background task monitoring

---

### 🌟 etke.cc exclusive features

> ⚠️ **Note:** The following features are only available for [etke.cc](https://etke.cc) customers. Due to the specifics of their implementation, they are not available for any other Ketesa deployment.

* [Server Status icon](../src/components/etke.cc/README.md#server-status-icon)
* [Server Status page](../src/components/etke.cc/README.md#server-status-page)
* [Server Actions page](../src/components/etke.cc/README.md#server-actions-page)
* [Server Commands Panel](../src/components/etke.cc/README.md#server-commands-panel)
* [Server Notifications icon](../src/components/etke.cc/README.md#server-notifications-icon)
* [Server Notifications page](../src/components/etke.cc/README.md#server-notifications-page)
* [Components page](./components.md)
* [Billing page](../src/components/etke.cc/README.md#billing-page)
* [Support page](../src/components/etke.cc/README.md#support-page)
* [Instance config](../src/components/etke.cc/README.md#instance-config)

---

## 🚀 Deployment

* [Availability](./availability.md) — where to get Ketesa: official builds, distro packages, integrations, mirrors, and legacy compatibility links
* [Serving Ketesa behind a reverse proxy](./reverse-proxy.md)


================================================
FILE: docs/apis.md
================================================
# 🔌 Supported APIs

Ketesa uses various APIs to manage Matrix homeservers and related services.
This document lists all supported APIs and their implementation status.

> 📝 **Note:** This file was compiled based on Synapse **v1.151.0** and MAS **v1.15.0** documentation.
> It is not updated often and is provided just for reference purposes.

**Legend:** ✅ fully implemented · 🟡 in progress · ❌ not implemented · ⏭️ superseded (newer version available)

---

## ✅ Synapse Admin API

[Synapse Admin API documentation →](https://element-hq.github.io/synapse/latest/usage/administration/admin_api/index.html)

### ✅ Server Version

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/server_version` | GET | Get running Synapse version | ✅ |

### ✅ Users

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v2/users` | GET | List all local user accounts | ⏭️ |
| `/_synapse/admin/v3/users` | GET | List all local user accounts (v3) | ✅ |
| `/_synapse/admin/v2/users/<user_id>` | GET | Query user account details | ✅ |
| `/_synapse/admin/v2/users/<user_id>` | PUT | Create or modify user account | ✅ |
| `/_synapse/admin/v1/whois/<user_id>` | GET | Query user sessions/connections | ✅ |
| `/_synapse/admin/v1/deactivate/<user_id>` | POST | Deactivate/erase user account | ✅ |
| `/_synapse/admin/v1/suspend/<user_id>` | PUT | Suspend or unsuspend user | ✅ |
| `/_synapse/admin/v1/reset_password/<user_id>` | POST | Reset user password | ✅ |
| `/_synapse/admin/v1/users/<user_id>/admin` | GET | Check if user is admin | ⏭️ |
| `/_synapse/admin/v1/users/<user_id>/admin` | PUT | Change user admin status | ⏭️ |
| `/_synapse/admin/v1/users/<user_id>/joined_rooms` | GET | List user's joined rooms | ✅ |
| `/_synapse/admin/v1/users/<user_id>/memberships` | GET | List user's room memberships | ✅ |
| `/_synapse/admin/v1/users/<user_id>/media` | GET | List media uploaded by user | ✅ |
| `/_synapse/admin/v1/users/<user_id>/media` | DELETE | Delete all media uploaded by user | ✅ |
| `/_synapse/admin/v1/users/<user_id>/accountdata` | GET | Get user account data | ✅ |
| `/_synapse/admin/v1/users/<user_id>/pushers` | GET | List user pushers | ✅ |
| `/_synapse/admin/v1/users/<user_id>/override_ratelimit` | GET | Get user ratelimit overrides | ✅ |
| `/_synapse/admin/v1/users/<user_id>/override_ratelimit` | POST | Set user ratelimit overrides | ✅ |
| `/_synapse/admin/v1/users/<user_id>/override_ratelimit` | DELETE | Delete user ratelimit overrides | ✅ |
| `/_synapse/admin/v1/users/<user_id>/login` | POST | Login as user (get access token) | ✅ |
| `/_synapse/admin/v1/users/<user_id>/shadow_ban` | POST | Shadow-ban a user | ✅ |
| `/_synapse/admin/v1/users/<user_id>/shadow_ban` | DELETE | Remove shadow-ban from user | ✅ |
| `/_synapse/admin/v1/users/<user_id>/_allow_cross_signing_replacement_without_uia` | POST | Allow cross-signing replacement without UIA | ✅ |
| `/_synapse/admin/v1/users/<user_id>/sent_invite_count` | GET | Count invites sent by user | ✅ |
| `/_synapse/admin/v1/users/<user_id>/cumulative_joined_room_count` | GET | Cumulative joined room count | ✅ |
| `/_synapse/admin/v1/username_available` | GET | Check username availability | ✅ |
| `/_synapse/admin/v1/auth_providers/<provider>/users/<external_id>` | GET | Find user by auth provider ID | ✅ |
| `/_synapse/admin/v1/threepid/<medium>/users/<address>` | GET | Find user by third-party ID | ✅ |
| `/_synapse/admin/v1/user/<user_id>/redact` | POST | Redact all events from a user | ✅ |
| `/_synapse/admin/v1/user/redact_status/<redact_id>` | GET | Check user redaction status | ✅ |

### ✅ User Devices

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v2/users/<user_id>/devices` | GET | List all devices for user | ✅ |
| `/_synapse/admin/v2/users/<user_id>/devices` | POST | Create a device for user | ✅ |
| `/_synapse/admin/v2/users/<user_id>/devices/<device_id>` | GET | Get single device info | ⏭️ |
| `/_synapse/admin/v2/users/<user_id>/devices/<device_id>` | PUT | Update device metadata | ✅ |
| `/_synapse/admin/v2/users/<user_id>/devices/<device_id>` | DELETE | Delete a device | ✅ |
| `/_synapse/admin/v2/users/<user_id>/delete_devices` | POST | Delete multiple devices | ✅ |

### ✅ Rooms

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/rooms` | GET | List rooms on server | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>` | GET | Get room details | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/members` | GET | Get room members | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/state` | GET | Get room state events | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/messages` | GET | Get messages from a room | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/timestamp_to_event` | GET | Find event by timestamp | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/context/<event_id>` | GET | Get event context | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/hierarchy` | GET | Get space/room hierarchy | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/block` | PUT | Block or unblock a room | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>/block` | GET | Get room block status | ✅ |
| `/_synapse/admin/v1/rooms/<room_id>` | DELETE | Delete a room (v1, synchronous) | ⏭️ |
| `/_synapse/admin/v2/rooms/<room_id>` | DELETE | Delete a room (v2, asynchronous) | ✅ |
| `/_synapse/admin/v2/rooms/<room_id>/delete_status` | GET | Query room delete status | ⏭️ |
| `/_synapse/admin/v2/rooms/delete_status/<delete_id>` | GET | Query delete status by ID | ✅ |
| `/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin` | POST | Grant user highest power level | ✅ |
| `/_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities` | GET | Check forward extremities | ✅ |
| `/_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities` | DELETE | Delete forward extremities | ✅ |

### ✅ Registration Tokens

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/registration_tokens` | GET | List all registration tokens | ✅ |
| `/_synapse/admin/v1/registration_tokens/<token>` | GET | Get specific registration token | ✅ |
| `/_synapse/admin/v1/registration_tokens/new` | POST | Create a registration token | ✅ |
| `/_synapse/admin/v1/registration_tokens/<token>` | PUT | Update a registration token | ✅ |
| `/_synapse/admin/v1/registration_tokens/<token>` | DELETE | Delete a registration token | ✅ |

### ✅ Media

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/room/<room_id>/media` | GET | List all media in a room | ✅ |
| `/_synapse/admin/v1/media/<origin>/<media_id>` | GET | Query media by ID | ⏭️ |
| `/_synapse/admin/v1/media/<server_name>/<media_id>` | DELETE | Delete specific local media | ✅ |
| `/_synapse/admin/v1/media/delete` | POST | Delete local media by date or size | ✅ |
| `/_synapse/admin/v1/media/<server_name>/delete` | POST | Delete local media by date or size (deprecated) | ⏭️ |
| `/_synapse/admin/v1/purge_media_cache` | POST | Purge old cached remote media | ✅ |
| `/_synapse/admin/v1/media/quarantine/<server_name>/<media_id>` | POST | Quarantine media by ID | ✅ |
| `/_synapse/admin/v1/media/unquarantine/<server_name>/<media_id>` | POST | Remove media from quarantine | ✅ |
| `/_synapse/admin/v1/room/<room_id>/media/quarantine` | POST | Quarantine all media in a room | ✅ |
| `/_synapse/admin/v1/quarantine_media/<room_id>` | POST | Quarantine room media (deprecated) | ⏭️ |
| `/_synapse/admin/v1/user/<user_id>/media/quarantine` | POST | Quarantine all media of a user | ✅ |
| `/_synapse/admin/v1/media/protect/<media_id>` | POST | Protect media from quarantine | ✅ |
| `/_synapse/admin/v1/media/unprotect/<media_id>` | POST | Unprotect media from quarantine | ✅ |

### ✅ Event Reports

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/event_reports` | GET | List reported events | ✅ |
| `/_synapse/admin/v1/event_reports/<report_id>` | GET | Get specific event report details | ✅ |
| `/_synapse/admin/v1/event_reports/<report_id>` | DELETE | Delete a specific event report | ✅ |

### ✅ Server Notices

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/send_server_notice` | POST | Send a server notice to a user | ✅ |
| `/_synapse/admin/v1/send_server_notice/{txnId}` | PUT | Send server notice with transaction ID | ⏭️ |

### ✅ Federation

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/federation/destinations` | GET | List federation destinations | ✅ |
| `/_synapse/admin/v1/federation/destinations/<destination>` | GET | Get destination details | ✅ |
| `/_synapse/admin/v1/federation/destinations/<destination>/rooms` | GET | List rooms for destination | ✅ |
| `/_synapse/admin/v1/federation/destinations/<destination>/reset_connection` | POST | Reset federation connection | ✅ |

### ✅ Experimental Features

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/experimental_features/<user_id>` | GET | List experimental features for user | ✅ |
| `/_synapse/admin/v1/experimental_features/<user_id>` | PUT | Enable/disable experimental features | ✅ |

### ✅ Statistics

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/statistics/users/media` | GET | Get users' media usage statistics | ✅ |
| `/_synapse/admin/v1/statistics/database/rooms` | GET | Get largest rooms by database size | ✅ |

### ✅ Account Validity

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/account_validity/validity` | POST | Renew account validity | ✅ |

### ✅ Purge History

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/purge_history/<room_id>[/<event_id>]` | POST | Purge room history | ✅ |
| `/_synapse/admin/v1/purge_history_status/<purge_id>` | GET | Query purge status | ✅ |

### ✅ Fetch Event

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/fetch_event/<event_id>` | GET | Fetch event by ID | ✅ |

### ⏭️ Register (Shared-Secret Registration) — superseded

Superseded: redundant with existing user creation via User Admin API (already implemented). Shared-secret registration is designed for CLI bootstrapping without an admin token — pointless when already authenticated in Ketesa.

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/register` | GET | Get registration nonce | ⏭️ |
| `/_synapse/admin/v1/register` | POST | Create user via shared-secret | ⏭️ |

### ✅ Room Membership

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/join/<room_id_or_alias>` | POST | Join a user to a room | ✅ |

### ✅ Scheduled Tasks

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_synapse/admin/v1/scheduled_tasks` | GET | Show scheduled tasks | ✅ |

### ✅ Client-Server API Extensions

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/_matrix/client/v3/user/<user_id>/account_data/io.element.synapse.admin_client_config` | GET | Get admin client configuration | ✅ |
| `/_matrix/client/v3/user/<user_id>/account_data/io.element.synapse.admin_client_config` | PUT | Set admin client configuration | ✅ |

---

## ✅ Matrix Authentication Service (MAS) Admin API

[MAS Admin API specification →](https://element-hq.github.io/matrix-authentication-service/api/spec.json)

### ✅ OAuth 2.0

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/oauth2/token` | POST | Refresh access token | ✅ |

### ✅ Server

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/site-config` | GET | Retrieve instance configuration | ✅ |
| `/api/admin/v1/version` | GET | Retrieve the currently running version | ✅ |

### ✅ Registration Tokens

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/user-registration-tokens` | GET | List registration tokens | ✅ |
| `/api/admin/v1/user-registration-tokens/{id}` | GET | Get a registration token | ✅ |
| `/api/admin/v1/user-registration-tokens` | POST | Create a registration token | ✅ |
| `/api/admin/v1/user-registration-tokens/{id}` | PUT | Update a registration token | ✅ |
| `/api/admin/v1/user-registration-tokens/{id}/revoke` | POST | Revoke a registration token | ✅ |
| `/api/admin/v1/user-registration-tokens/{id}/unrevoke` | POST | Unrevoke a registration token | ✅ |

### ✅ Users

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/users` | GET | List users | ✅ |
| `/api/admin/v1/users` | POST | Create a new user | ✅ |
| `/api/admin/v1/users/{id}` | GET | Get user by ID | ✅ |
| `/api/admin/v1/users/by-username/{username}` | GET | Get user by username | ⏭️ |
| `/api/admin/v1/users/{id}/set-password` | POST | Set user password | ✅ |
| `/api/admin/v1/users/{id}/set-admin` | POST | Toggle admin flag | ✅ |
| `/api/admin/v1/users/{id}/deactivate` | POST | Deactivate user | ✅ |
| `/api/admin/v1/users/{id}/reactivate` | POST | Reactivate user | ✅ |
| `/api/admin/v1/users/{id}/lock` | POST | Lock user | ✅ |
| `/api/admin/v1/users/{id}/unlock` | POST | Unlock user | ✅ |

### ✅ User Emails

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/user-emails` | GET | List user emails | ✅ |
| `/api/admin/v1/user-emails` | POST | Add email to user | ✅ |
| `/api/admin/v1/user-emails/{id}` | GET | Get email details | ✅ |
| `/api/admin/v1/user-emails/{id}` | DELETE | Remove email from user | ✅ |

### ✅ Compat Sessions

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/compat-sessions` | GET | List compatibility sessions | ✅ |
| `/api/admin/v1/compat-sessions/{id}` | GET | Get a compatibility session | ✅ |
| `/api/admin/v1/compat-sessions/{id}/finish` | POST | Terminate a compatibility session | ✅ |

### ✅ OAuth 2.0 Sessions

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/oauth2-sessions` | GET | List OAuth 2.0 sessions | ✅ |
| `/api/admin/v1/oauth2-sessions/{id}` | GET | Get an OAuth 2.0 session | ✅ |
| `/api/admin/v1/oauth2-sessions/{id}/finish` | POST | Terminate an OAuth 2.0 session | ✅ |

### ✅ Personal Sessions

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/personal-sessions` | GET | List personal sessions | ✅ |
| `/api/admin/v1/personal-sessions` | POST | Create a personal session | ✅ |
| `/api/admin/v1/personal-sessions/{id}` | GET | Get personal session details | ✅ |
| `/api/admin/v1/personal-sessions/{id}/revoke` | POST | Revoke a personal session | ✅ |
| `/api/admin/v1/personal-sessions/{id}/regenerate` | POST | Regenerate personal session token | ✅ |

### ✅ Browser Sessions

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/user-sessions` | GET | List browser sessions | ✅ |
| `/api/admin/v1/user-sessions/{id}` | GET | Get a browser session | ✅ |
| `/api/admin/v1/user-sessions/{id}/finish` | POST | Terminate a browser session | ✅ |

### ✅ Upstream OAuth Links

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/upstream-oauth-links` | GET | List upstream OAuth links | ✅ |
| `/api/admin/v1/upstream-oauth-links` | POST | Create an upstream OAuth link | ✅ |
| `/api/admin/v1/upstream-oauth-links/{id}` | GET | Get an upstream OAuth link | ✅ |
| `/api/admin/v1/upstream-oauth-links/{id}` | DELETE | Remove an upstream OAuth link | ✅ |

### ✅ Upstream OAuth Providers

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/upstream-oauth-providers` | GET | List upstream OAuth providers | ✅ |
| `/api/admin/v1/upstream-oauth-providers/{id}` | GET | Get an upstream OAuth provider | ✅ |

### ✅ Policy Data

| Endpoint | Method | Description | Status |
|----------|--------|-------------|:------:|
| `/api/admin/v1/policy-data` | POST | Set policy data | ✅ |
| `/api/admin/v1/policy-data/latest` | GET | Get latest policy data | ✅ |
| `/api/admin/v1/policy-data/{id}` | GET | Get policy data by ID | ⏭️ |


================================================
FILE: docs/availability.md
================================================
# 📦 Availability

This is the canonical reference for obtaining Ketesa.

> ⚠️ Community-maintained and third-party entries may lag behind official releases.
> When in doubt, prefer the official release tarballs, containers, or the hosted instance.

## ✅ Official channels

| Channel | Type | Maintainer | Best for | Notes |
|---|---|---|---|---|
| [etke.cc](https://etke.cc/?utm_source=github&utm_medium=docs&utm_campaign=ketesa) | Managed hosting | Official | Fully managed deployments | Ketesa is [a core component](https://etke.cc/help/faq#what-are-the-base-matrix-components-installed-on-the-server) installed by default |
| [admin.etke.cc](https://admin.etke.cc) | Hosted instance | Official | No installation needed | Always on the latest development version |
| [GitHub Releases](https://github.com/etkecc/ketesa/releases) | Static builds | Official | Self-hosting behind any web server | Includes `ketesa.tar.gz` and `ketesa-subpath-admin.tar.gz` |
| [GHCR](https://github.com/etkecc/ketesa/pkgs/container/ketesa) | Container image | Official | Docker and OCI-based deployments | Main image registry |
| [Docker Hub](https://hub.docker.com/r/etkecc/ketesa/tags) | Container image | Official | Docker-first users | Mirrors the official container images |
| [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy/blob/master/docs/configuring-playbook-ketesa.md) | Ansible integration | Official | Operators using the MDAD playbook | Maintained by the etke.cc team |
| [etkecc/ansible](https://github.com/etkecc/ansible) | Ansible integration | Official | etke.cc-flavoured self-hosted automation | Maintained by the etke.cc team |
| [Source repository](https://github.com/etkecc/ketesa) | Source code | Official | Building from source or contributing | For custom builds and development checkouts |

### Prebuilt distributions

Ketesa publishes two static distributions:

- `ketesa.tar.gz` for root path deployments such as `https://admin.example.com`
- `ketesa-subpath-admin.tar.gz` for `/admin` subpath deployments such as `https://example.com/admin`

For custom prefixes other than `/admin`, build from source with `yarn build --base=/your-prefix/`
or pass the `BASE_PATH` Docker build argument.

## 🌙 Nightly and development builds

To get the latest unreleased changes:

- Use [admin.etke.cc](https://admin.etke.cc) for the hosted development version
- Pull `latest` or `latest-subpath-admin` from [GHCR](https://github.com/etkecc/ketesa/pkgs/container/ketesa) or [Docker Hub](https://hub.docker.com/r/etkecc/ketesa/tags)
- Download `dist-root` or `dist-subpath-admin` from the latest successful [GitHub Actions run](https://github.com/etkecc/ketesa/actions/workflows/workflow.yml)
- Build the `main` branch from [source](https://github.com/etkecc/ketesa)

## 🐧 Distro packages

| Channel | Type | Maintainer | Best for | Notes |
|---|---|---|---|---|
| [Nixpkgs](https://search.nixos.org/packages?channel=unstable&show=ketesa) | Package index | [@Defelo](https://github.com/Defelo) | Nix and NixOS users | |
| [YunoHost](https://apps.yunohost.org/app/ketesa) | App catalog package | [@Josue-T](https://github.com/Josue-T) | YunoHost-based deployments | |

## 🤝 Community-packaged options

| Channel | Type | Maintainer | Best for | Notes |
|---|---|---|---|---|
| [IPFS](#ipfs) | Mirror / alternative delivery | [Fеnикs (@fenuks:sibnsk.net)](https://matrix.to/#/@fenuks:sibnsk.net) | Content-addressed distribution | See the [IPFS](#ipfs) section below for addresses |
| [Arch Linux AUR](https://aur.archlinux.org/packages/synapse-admin-etke-git) | AUR package | [@drygdryg](https://github.com/drygdryg) | Arch users who prefer AUR packaging | Legacy package naming — maintained outside this repository |

## IPFS

> Maintained by [Fеnикs (@fenuks:sibnsk.net)](https://matrix.to/#/@fenuks:sibnsk.net)

**Latest version:** `/ipns/ketesa.sibnsk.net`
(`dnslink` key `/ipns/k51qzi5uqu5dj91scsxoqu0ebmy7uqajrt9ohl98vs7fl7l429h0chgozk58i2`)

**Archive:** `/ipns/ketesa-archive.sibnsk.net`
(`dnslink` key `/ipns/k51qzi5uqu5dhxwc36sld1hn6jn935k71ww8rdyqomrnqcqucixy7re08qeu7z`)

## 📝 Want your package listed?

Open a pull request if you maintain a Ketesa package, mirror, or deployment integration — we welcome all distribution channels.
Please include:

- the package or project link
- whether it is official, community-maintained, or a third-party mirror
- who maintains it
- any important caveats, especially if it still uses legacy `synapse-admin` naming


================================================
FILE: docs/components.md
================================================
# 🧩 Components

| Light | Dark |
|-------|------|
| ![Components (light)](./screenshots/light/components.webp) | ![Components (dark)](./screenshots/dark/components.webp) |

Accessible via the **Components** sidebar menu item.

The Components page is your self-service catalogue for everything your Matrix server can do. From a single screen you can see what's running, explore what's available, toggle services on or off, and review the cost impact — all without opening a support ticket.

---

## What you can do

### See exactly what's on your server

The **Your Server** card shows every component that is currently active on your server — chat bridges, bots, apps, and the services that come included with your plan. Each item displays its monthly price so there are no surprises on your next invoice.

### Explore available add-ons

Add-on sections — **Bridges**, **Extras**, **Matrix Apps**, **Matrix Bots**, **Matrix Extras** — list every service available to you. If you have an active pack (for example, the Bridges pack), all components in that section are shown as **Available**, meaning they are included in your pack and ready to enable at no extra charge. Individual add-ons outside a pack show their monthly price upfront.

### Add or remove components in seconds

Toggle any component using the switch next to it. The row highlights immediately to confirm your selection. You can stage as many changes as you like before committing — nothing changes on your server until you click **Request changes**.

### Preview the cost before you commit

The price strip at the bottom of the page always shows your **current monthly total**. The moment you stage a change, a preview chip appears showing what your new total will be. You see the exact cost before anything is applied.

### Submit changes with one click

When you're happy with your selection, click **Request changes**. This automatically opens a support ticket with your staged changes — no manual description needed. The etke.cc team receives it and applies the changes to your server.

---

## Component labels

| Label | Meaning |
|-------|---------|
| **Included** (green, filled) | Part of your plan — no extra charge |
| **Free** (green, outlined) | Available at no cost — not yet enabled |
| **€X/mo** (green, outlined) | Available — costs this amount per month if added |
| **Available** (themed, outlined) | Included in your active pack — enable at no extra charge |

When you toggle a component on, its label turns **primary** (blue on light theme, orange on dark). When you toggle one off, it dims — a clear visual cue that you're staging a removal.

---

## Sections

| Section | What's in it |
|---------|-------------|
| **Your Server** | Your currently active components |
| **Bridges** | Protocol bridges — connect Matrix to Slack, Discord, Telegram, WhatsApp, IRC, and more |
| **Extras** | Standalone add-ons and productivity services |
| **Matrix Apps** | Full Matrix applications and bots for your community |
| **Matrix Bots** | Automation bots — moderation, utilities, integrations |
| **Matrix Extras** | Additional Matrix-ecosystem services |

> 💡 Sections only appear if they contain components available to your server.

---

> ⚠️ This page is exclusive to [etke.cc](https://etke.cc) customers and is not available in standalone Ketesa deployments.


================================================
FILE: docs/config.md
================================================
# ⚙️ Configuration

Ketesa is flexible by design — configure it once and let it work across any number of deployments or homeservers.

There are two ways to configure Ketesa (both are optional, and both can be used together):

| Method | Best for |
|--------|----------|
| `config.json` alongside the deployment ([example](https://admin.etke.cc/config.json)) | Self-hosted deployments where you control the Ketesa files |
| `cc.etke.ketesa` key in `/.well-known/matrix/client` ([example](https://demo.etke.host/.well-known/matrix/client)) | Any homeserver — works even if you don't host Ketesa yourself |

> 📝 **Existing configurations using the legacy `cc.etke.synapse-admin` key continue to work** — Ketesa reads both keys automatically and you don't need to change anything. Migrating to `cc.etke.ketesa` is optional and can be done at your convenience.

If you are an [etke.cc](https://etke.cc) customer,
or use [spantaleev/matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy),
or [etkecc/ansible](https://github.com/etkecc/ansible),
configuration is added automatically to the `/.well-known/matrix/client` file.

> 💡 **Why `/.well-known/matrix/client`?**
>
> Because any instance of Ketesa will automatically pick up the configuration from the homeserver.
> A common use case is when you have a Synapse server running, but don't want (or can't) to deploy Ketesa alongside it.
> In this case, you could provide the configuration in the `/.well-known/matrix/client` file,
> and any Ketesa instance (e.g., [admin.etke.cc](https://admin.etke.cc)) will pick it up.
>
> Another common case is when you have multiple Synapse servers running and want to use a single Ketesa instance to manage them all.
> In this case, you could provide the configuration in the `/.well-known/matrix/client` file for each of the servers.

## 🔧 Configuration options

* `restrictBaseUrl` — restricts the Ketesa instance to work only with specific homeserver(-s).
  Accepts both a string and an array of strings.
  The homeserver URL should be the _actual_ homeserver URL, and not the delegated one.
  Example: `https://matrix.example.com` or `https://synapse.example.net`
  [More details](restrict-hs.md)

* `externalAuthProvider` — set if an external authentication provider is used (e.g., OIDC, LDAP, etc).
  Accepts a boolean value.
  [More details](external-auth-provider.md)

* `wellKnownDiscovery` — control automatic URL canonicalization via `/.well-known/matrix/client`.
  Accepts a boolean value. Default: `true` (discovery enabled, per Matrix spec).
  Set to `false` when the `/_synapse/admin` API is hosted on a separate domain not advertised
  in well-known (e.g. a VPN-only admin endpoint). When disabled, MXID-based URL auto-fill
  uses the domain portion of the MXID directly without a well-known lookup.
  [More details](well-known-discovery.md)

* `corsCredentials` — configure the CORS credentials for the Ketesa instance.
  Accepts the following values:

  | Value | Behavior |
  |---|---|
  | `same-origin` (default) | Cookies are sent only if the request is made from the same origin as the server |
  | `include` | Cookies are sent regardless of the origin of the request |
  | `omit` | Cookies are not sent with the request |

  [More details](cors-credentials.md)

* `asManagedUsers` — protect system user accounts managed by appservices (such as bridges) / system (such as bots) from accidental changes.
  By defining a list of MXID regex patterns, you can protect these accounts from accidental changes.
  Example: `^@baibot:example\\.com$`, `^@slackbot:example\\.com$`, `^@slack_[a-zA-Z0-9\\-]+:example\\.com$`, `^@telegrambot:example\\.com$`, `^@telegram_[a-zA-Z0-9]+:example\\.com$`
  [More details](system-users.md)

* `menu` — add custom menu items to the main menu (sidebar) by providing a `menu` array in the config.
  Each `menu` item can contain the following fields:

  | Field | Required | Description |
  |---|---|---|
  | `label` | ✅ Yes | The text to display in the menu |
  | `icon` | No | Icon name from [src/utils/icons.ts](../src/utils/icons.ts); falls back to a default icon |
  | `url` | ✅ Yes | The URL to navigate to when the menu item is clicked |

  [More details](custom-menu.md)

## 📋 Examples

### config.json

```json
{
  "restrictBaseUrl": [
    "https://matrix.example.com",
    "https://synapse.example.net"
  ],
  "asManagedUsers": [
    "^@baibot:example\\.com$",
    "^@slackbot:example\\.com$",
    "^@slack_[a-zA-Z0-9\\-]+:example\\.com$",
    "^@telegrambot:example\\.com$",
    "^@telegram_[a-zA-Z0-9]+:example\\.com$"
  ],
  "menu": [
    {
      "label": "Contact support",
      "icon": "SupportAgent",
      "url": "https://github.com/etkecc/ketesa/issues"
    }
  ]
}
```

### `/.well-known/matrix/client`

```json
{
  "cc.etke.ketesa": {
    "restrictBaseUrl": [
      "https://matrix.example.com",
      "https://synapse.example.net"
    ],
    "asManagedUsers": [
      "^@baibot:example\\.com$",
      "^@slackbot:example\\.com$",
      "^@slack_[a-zA-Z0-9\\-]+:example\\.com$",
      "^@telegrambot:example\\.com$",
      "^@telegram_[a-zA-Z0-9]+:example\\.com$"
    ],
    "menu": [
      {
        "label": "Contact support",
        "icon": "SupportAgent",
        "url": "https://github.com/etkecc/ketesa/issues"
      }
    ]
  }
}
```


================================================
FILE: docs/configurable-columns.md
================================================
# 🗂️ Configurable Table Columns

| Light | Dark |
|-------|------|
| ![Rooms List (light)](./screenshots/light/rooms-list.webp) | ![Rooms List (dark)](./screenshots/dark/rooms-list.webp) |

Every data table in Ketesa supports configurable columns — you can show, hide, and reorder columns to match your workflow. Preferences are stored in your browser (localStorage), not on the server, so each browser or device maintains independent settings.

---

## 📋 Supported Tables

The following tables support column configuration:

| Table | Location |
|---|---|
| Users list | **Users** → main list |
| Rooms list | **Rooms** → main list |
| Room directory | **Room Directory** → main list |
| Reports list | **Reports** → main list |
| Registration tokens list | **Registration Tokens** → main list |
| Scheduled tasks list | **Scheduled Tasks** → main list |
| Federation (destinations) list | **Federation** → main list |
| Users' media statistics | **Statistics** → Users' media |
| Database room statistics | **Statistics** → Database rooms |
| Room detail — Members tab | **Rooms** → room detail → Members |
| Room detail — Media tab | **Rooms** → room detail → Media |
| Room detail — State events tab | **Rooms** → room detail → State events |
| Room detail — Forward Extremities tab | **Rooms** → room detail → Forward Extremities |
| Federation destination — Rooms tab | **Federation** → destination detail → Rooms |
| User detail — Devices tab | **Users** → user detail → Devices |
| User detail — Connections tab | **Users** → user detail → Connections |
| User detail — Media tab | **Users** → user detail → Media |
| User detail — Rooms tab | **Users** → user detail → Rooms |
| User detail — Memberships tab | **Users** → user detail → Memberships |
| User detail — Pushers tab | **Users** → user detail → Pushers |
| MAS Upstream OAuth Providers | **MAS** → OAuth Providers |
| MAS Upstream OAuth Links | **MAS** → user detail → Upstream OAuth Links |
| MAS Browser Sessions | **MAS** → user detail → Browser Sessions |
| MAS OAuth2 Sessions | **MAS** → user detail → OAuth2 Sessions |
| MAS Compat Sessions | **MAS** → user detail → Compat Sessions |
| MAS User Emails | **MAS** → user detail → Emails |
| MAS Personal Sessions | **MAS** → user detail → Personal Sessions |

---

## ⚙️ How to Open the Column Configurator

1. Navigate to any supported table (see list above).
2. Look for the **⚙️ settings icon** at the top-right corner of the table toolbar.
3. Click it to open the column picker panel.

> 💡 The icon only appears on desktop-width viewports. On mobile, tables switch to a simplified list layout that does not expose the column configurator.

---

## 👁️ How to Show or Hide a Column

1. Open the column picker panel (see above).
2. Find the column name you want to toggle.
3. Click the **checkbox** next to the column name to show or hide it.
4. The table updates immediately — no save button needed.

---

## 🔀 How to Reorder Columns

1. Open the column picker panel (see above).
2. Locate the **drag handle** (⠿ or similar grip icon) to the left of a column name.
3. Click and drag the row to the desired position in the list.
4. Release to drop — the table column order updates immediately.

---

## 📝 Settings Persistence

> 📝 Column visibility and order settings persist across page reloads and browser sessions as long as you use the same browser profile. Clearing your browser's site data (cookies, localStorage) will reset all column preferences to their defaults.

---

**See also:** [User management](./user-management.md) · [Room management](./room-management.md) · [Documentation index](./README.md)


================================================
FILE: docs/cors-credentials.md
================================================
# 🔐 CORS Credentials

Controls how Ketesa sends cookies and credentials when making API requests. Most deployments don't need to touch this — the default works fine for standard setups. You'll typically only need it when adding a reverse-proxy authentication layer in front of your homeserver.

**When to change it:**

- **`include`** — use this when you have cookie-based auth in front of your homeserver (e.g., [ForwardAuth with Authelia](https://github.com/Awesome-Technologies/synapse-admin/issues/655)). Cookies will be forwarded with every request regardless of origin.
- **`omit`** — use this if your setup explicitly must not send any cookies (rare; usually for strict security policies).
- **`same-origin`** — the default; works for the vast majority of deployments.

## ⚙️ Configuration

> 📚 [MDN reference: credentials option](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials)

| Value | When to use | Behavior |
|-------|-------------|----------|
| `same-origin` *(default)* | Standard deployments | Cookies sent only for same-origin requests |
| `include` | Cookie-based auth (ForwardAuth, Authelia, etc.) | Cookies sent with every request |
| `omit` | Strict no-cookie policies | Cookies never sent |

[Configuration options](config.md)

### config.json

```json
{
  "corsCredentials": "include"
}
```

### `/.well-known/matrix/client`

```json
{
  "cc.etke.ketesa": {
    "corsCredentials": "include"
  }
}
```


================================================
FILE: docs/csv-import.md
================================================
# 🎯 Bulk CSV User Import

The bulk CSV import feature lets you create many Matrix user accounts at once by uploading a structured CSV file. Instead of manually creating accounts one by one through the UI, you can prepare a spreadsheet, export it as CSV, and provision hundreds of users in a single operation — ideal for onboarding a new organisation, migrating users from another platform, or pre-seeding accounts before launch.

## ✨ Overview / Capabilities

- Upload any RFC-4180 CSV file with standard user fields
- Validate the file and preview parsed statistics before committing any changes
- Dry-run mode: simulate the full import without writing anything to the server
- Three conflict modes to control what happens when a user ID already exists
- Automatic random password generation for rows that have no password
- Automatic random MXID generation for rows that have no ID
- Third-party ID (3PID, e.g. email) provisioning via a compact inline syntax
- Download a CSV of skipped records after the import so you can fix and re-run them

---

## 📋 CSV Format

The CSV file must have a header row. Column names are **case-insensitive** and leading/trailing whitespace is stripped automatically.

### 🔑 Required columns

| Column | Description |
|---|---|
| `id` | Matrix user ID. Accepts a full MXID (`@alice:example.com`) or a bare localpart (`alice`). If empty or missing per-row, a random MXID is generated (see [User ID mode](#️-import-options)). |
| `displayname` | Human-readable display name shown in Matrix clients. |

> ⚠️ The import will be rejected immediately if either `id` or `displayname` is missing from the header row.

### 🗂️ Optional columns

| Column | Type | Description |
|---|---|---|
| `password` | string | Plaintext password for the account. If empty, a random password is generated (controlled by [Password mode](#️-import-options)). |
| `admin` | boolean | Whether the user is a server administrator. Accepts `1`, `true`, `yes`, `on` for true; `0`, `false`, `no`, `off`, `null`, `undefined`, or empty for false. |
| `is_guest` | boolean | Whether the account is a guest. Same boolean values as `admin`. |
| `deactivated` | boolean | Whether the account is deactivated on creation. Same boolean values as `admin`. |
| `avatar_url` | string | `mxc://` URI for the user's avatar image. |
| `user_type` | string | Synapse user type identifier (e.g. `bot`). Leave empty for a regular user. |
| `threepids` | string | Comma-separated list of third-party identifiers in `medium:address` format (e.g. `email:alice@example.com,msisdn:+1234567890`). Pairs with an invalid format are silently ignored. |

> 📝 The columns `name` and `is_admin` are recognised as legacy aliases produced by earlier react-admin CSV exports and are silently discarded during import. Use `id` and `admin` respectively.

> 💡 Column order in the CSV does not matter. Extra columns not listed above are passed through to the server as-is.

### 📄 Example

```csv
id,displayname,password,admin,is_guest,deactivated,avatar_url,threepids
alice,Alice Example,s3cr3t,false,false,false,,email:alice@example.com
bob,Bob Example,,false,false,false,,
@carol:example.com,Carol Example,hunter2,true,false,false,mxc://example.com/abc123,email:carol@example.com
```

---

## ⚙️ Import Options

These options are presented in the UI after a valid CSV has been loaded, before the import is started.

| Option | UI control | Values | Default | Description |
|---|---|---|---|---|
| **Dry-run mode** | Checkbox | enabled / disabled | enabled | When enabled, the import runs through all validation and conflict checks but does **not** create any accounts on the server. Always recommended for a first pass. |
| **Conflict mode** | Dropdown | `stop`, `skip` | `stop` | What to do when a user ID from the CSV already exists on the server (see [Handling conflicts](#-how-to-handle-conflicts)). |
| **Password mode** | Checkbox | enabled / disabled | enabled | When enabled, passwords supplied in the CSV are used as-is and missing passwords get a randomly generated one. When disabled, no password is set or updated for any user. |
| **User ID mode** | Dropdown | `ignore`, `update` | `update` | Controls whether IDs present in the CSV are used (`update`) or discarded in favour of freshly generated random MXIDs (`ignore`). Only shown when at least one row in the CSV has a non-empty `id` value. |

---

## 🛠️ How to Import Users

1. **Navigate to the import page.** In the left sidebar, open **Users**, then click **Import**.
2. **Prepare your CSV.** Create a file with at minimum the `id` and `displayname` columns. Add any optional columns you need (see [CSV Format](#-csv-format)).
3. **Upload the file.** Click the file input and select your CSV. The file is parsed immediately in the browser — no data is sent to the server yet.
4. **Review the parsed statistics.** The stats card shows the total number of users found, how many have guest or admin flags set, how many have explicit IDs, and how many have passwords. Verify these numbers match your expectations.
5. **Configure import options.** Set conflict mode, password mode, and user ID mode as needed (see [Import Options](#️-import-options)).
6. **Run a dry run first.** Make sure the **Simulate only** checkbox is ticked, then click **Run import**. Review the results — check the success count and whether any records were skipped.
7. **Run the real import.** Uncheck **Simulate only**, then click **Run import** again. Progress is shown inline as records are processed.
8. **Review results.** The results card appears when the import completes (see [Reading the Results](#-how-to-read-results)).

> ⚠️ Files larger than 100 MB are rejected. For very large imports, split the CSV into chunks below this limit.

> 💡 The dry run uses the exact same logic as the real import, including conflict detection. If the dry run shows zero skipped records, the real run will also skip none — assuming no concurrent changes on the server.

---

## ⚔️ How to Handle Conflicts

A conflict occurs when the `id` of a CSV row resolves to a Matrix user that already exists on the server. The **Conflict mode** setting controls what happens:

| Mode | Value | Behaviour | When to use |
|---|---|---|---|
| **Stop** | `stop` | The import halts immediately at the first conflicting record. Records processed before the conflict are kept (or simulated). The offending ID is reported in the error card. | Safe default. Use when your CSV should contain only new users and any existing ID indicates a data problem. |
| **Skip** | `skip` | Conflicting records are silently skipped and added to the skipped-records list. The import continues with the remaining rows. | Use when you are re-running a partially completed import or when your CSV intentionally mixes new and existing users. |

> 📝 In `skip` mode, all skipped records are available for download as a CSV after the import finishes (see [Reading the Results](#-how-to-read-results)). You can edit the downloaded file and re-import only the skipped rows.

> ⚠️ There is no **Update** conflict mode. Existing user accounts are never modified by the import, regardless of conflict mode setting. To modify existing users, use the user edit form or the user list bulk actions.

---

## 📊 How to Read Results

When the import finishes the results card replaces the import controls and shows:

| Item | Description |
|---|---|
| **Total** | The total number of rows that were processed from the CSV. |
| **Successful** | The count of accounts that were (or would be, in dry-run) successfully created. A list of their display names is shown below the count. |
| **Skipped** | The count of records that were skipped due to a conflict (only in `skip` conflict mode). A **Download skipped records** button appears — clicking it downloads `skippedRecords.csv` containing those rows in the original CSV format. |
| **Errored** | The count of records that could not be processed due to an error. |
| **Simulated only** warning | A yellow alert is shown when the results are from a dry run and no accounts were actually created. |

After reviewing the results, click **Back** to return to the user list.

> 💡 The downloaded `skippedRecords.csv` is a valid import CSV. You can correct it and upload it for a follow-up import without needing to filter out the already-successful rows from your original file.

---

**See also:** [User management](./user-management.md) · [Documentation index](./README.md)


================================================
FILE: docs/custom-menu.md
================================================
# 🎯 Custom Menu Items

Extend Ketesa's sidebar navigation with your own links — no rebuild required. Custom menu items appear alongside the built-in navigation and work like any other menu entry.

**Popular uses:**
- Link to your internal runbook or documentation wiki
- Add a shortcut to your monitoring dashboard or status page
- Point to a support ticketing system or help desk
- Add a link to your organization's Matrix community room

Items support translations via the `i18n` field, so multilingual teams see the label in their own language automatically.

## ⚙️ Configuration

The examples below add a link to the [Ketesa issues](https://github.com/etkecc/ketesa/issues).

Each `menu` item can contain the following fields:

| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `url` | ✓ | string | The URL to navigate to when the menu item is clicked. |
| `label` | ✓ | string | The text to display in the menu. |
| `i18n` | | object | Dictionary of translations for the label. The keys should be [BCP 47 language tags](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g., `en`, `fr`, `de`) supported by Ketesa (see [src/i18n/](../src/i18n)). |
| `icon` | | string | The icon to display next to the label, one of the [src/utils/icons.ts](../src/utils/icons.ts) icons, otherwise a default icon will be used. |

[Configuration options](config.md)

### config.json

```json
{
  "menu": [
    {
      "label": "Contact support",
      "i18n": {
        "de": "Support kontaktieren",
        "fr": "Contacter le support",
        "zh": "联系支持"
      },
      "icon": "SupportAgent",
      "url": "https://github.com/etkecc/ketesa/issues"
    }
  ]
}
```

### `/.well-known/matrix/client`

```json
{
  "cc.etke.ketesa": {
    "menu": [
      {
        "label": "Contact support",
        "i18n": {
          "de": "Support kontaktieren",
          "fr": "Contacter le support",
          "zh": "联系支持"
        },
        "icon": "SupportAgent",
        "url": "https://github.com/etkecc/ketesa/issues"
      }
    ]
  }
}
```


================================================
FILE: docs/event-reports.md
================================================
# 🚨 Event Reports

Event reports are abuse reports submitted by Matrix users via their client apps. When a user flags a message or event as harmful, inappropriate, or otherwise policy-violating, a report is created on the homeserver. Ketesa lets you review those reports, investigate the flagged content, and dismiss reports once you have handled them.

> 📝 Reports in this view correspond to Matrix's `/report` endpoint. Only events reported by users on your homeserver appear here.

---

## 📋 Reports List

The reports list displays all pending abuse reports, sorted by most recent first by default.

### 🗂️ Columns

| Column | Field | Description |
|---|---|---|
| **ID** | `id` | Unique numeric identifier for the report. |
| **Report Time** | `received_ts` | Date and time the report was received by the homeserver. |
| **Reporter** | `user_id` | The Matrix user who submitted the report. |
| **Room Name** | `name` | Display name of the room containing the flagged event. |
| **Score** | `score` | Severity score assigned by the reporter (see [Severity Score](#-severity-score) below). |

> 📝 The reporting user's ID and the room name are truncated in the list view. Click a report to see full values.

---

## 🔢 Severity Score

The **Score** field is a numeric value set by the reporter at the time they submitted the report. Per the Matrix specification, the range is **−100 to 0**, where −100 represents the most severe content and 0 represents the least. Not all clients enforce this range — some may send positive values or omit the field entirely.

> ⚠️ The score reflects the reporter's subjective judgement, not an objective severity level. Use it as a triage signal, but always review the actual event content before taking action.

Common guidance (for spec-compliant clients):

| Score range | Typical reporter intent |
|---|---|
| −100 to −71 | High concern — serious violations (e.g. harassment, illegal content) |
| −70 to −31 | Moderate concern — clearly unwanted or offensive content |
| −30 to 0 | Low concern — mildly inappropriate or borderline content |

---

## 🔍 Report Detail View

Click any row in the reports list to open the detail view for that report. The detail view has two tabs.

### 🗒️ Basic Tab

The **Basic** tab displays report metadata and links to the involved parties.

| Field | Description |
|---|---|
| **ID** | Numeric report ID. |
| **Report Time** | Timestamp the report was received. |
| **Score** | Severity score set by the reporter. |
| **Reason** | Free-text reason provided by the reporter (may be empty). |
| **Announcer** | The user who filed the report — clicking their avatar or ID navigates to their user detail page. |
| **Sender** | The user who sent the flagged event — clicking their avatar or ID navigates to their user detail page. |
| **Room** | The room containing the flagged event — clicking navigates to the room detail page. |
| **Event ID** | The Matrix event ID of the flagged event. |

> 💡 Use the direct links to the reporter, sender, and room to jump straight to the relevant resource without leaving the workflow.

### 🧾 Details Tab

The **Details** tab shows the raw JSON of the flagged event (`event_json`). This is the full Matrix event object as stored by the homeserver — including the event type, content, sender, timestamps, and signatures.

Use this tab to see the exact message or media content that was reported without having to look it up elsewhere.

> 📝 If the event JSON is absent (e.g. the event has been redacted or is no longer available), the Details tab will be empty.

---

## 🔎 Event Lookup Tool

The **Event Lookup** tool lets you retrieve any event by its ID, independent of reports. It is useful when you have an event ID from another source (e.g. a user complaint, a log entry, or another admin tool) and want to inspect its content.

**Location:** The "Event Lookup" button is in the toolbar at the top-right of the reports list page.

**How it works:** Clicking the button opens a dialog. Enter the event ID and press **Fetch** (or press Enter). The raw event JSON is displayed inline in the dialog.

> 💡 The Event Lookup tool queries the homeserver directly and is not limited to reported events. You can look up any event ID your admin account has access to.

---

## 📖 How to Review a Report

1. Open **Reported Events** from the left navigation menu.
2. Find the report you want to review. You can sort by **Report Time** to prioritise the most recent ones.
3. Click the report row to open the detail view.
4. Read the **Basic** tab: note the reporter (**Announcer**), the message author (**Sender**), and the **Reason** provided.
5. Switch to the **Details** tab to read the raw event JSON and see the exact content that was flagged.
6. If you need more context, use the links on the **Basic** tab to navigate to the sender's user page or the room page.
7. Take any necessary moderation action on the user or room (e.g. deactivate account, kick from room) through the respective management pages.
8. Return to the report detail view and delete the report to dismiss it (see [How to Delete a Report](#-how-to-delete-a-report)).

---

## 🔎 How to Use the Event Lookup Tool

1. Open **Reported Events** from the left navigation menu.
2. Click the **Event Lookup** button in the top-right toolbar.
3. In the dialog that opens, paste or type the event ID into the **Event ID** field.
4. Press **Fetch** or hit Enter.
5. The raw event JSON is displayed in the dialog.
6. Review the content as needed, then click **Cancel** to close the dialog.

> 💡 If the event cannot be found or your account lacks access, an error message is shown in the dialog.

---

## 🗑️ How to Delete a Report

1. Open the report detail view by clicking the report in the list.
2. Click the **Delete** button in the top-right toolbar of the detail view.
3. A confirmation dialog appears asking you to confirm the deletion.
4. Confirm the deletion.

> 📝 Deleting a report removes it from the list but does not affect the reported user or the event itself. Take any moderation action on the user or room separately.

---

**See also:** [Room management](./room-management.md) · [Documentation index](./README.md)


================================================
FILE: docs/external-auth-provider.md
================================================
# 🔑 External Auth Provider

When Synapse delegates authentication to an external provider (OIDC, LDAP, and similar), the Synapse API doesn't announce which provider is in use — especially with seamless/hidden password providers that don't announce themselves. Ketesa needs a hint to adapt its interface accordingly.

Setting `externalAuthProvider: true` tells Ketesa to adjust its behavior for these setups. Currently, it makes the following changes:

- **No password required when reactivating a user** — because the password lives in the external provider, not in Synapse
- **Hides the guests filter** in the users list — external auth providers typically don
Download .txt
gitextract_pt5yit7t/

├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── CONTRIBUTING.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── SECURITY.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── reuse.yml
│       └── workflow.yml
├── .gitignore
├── .prettierignore
├── .watchmanconfig
├── LICENSE
├── LICENSES/
│   ├── Apache-2.0.txt
│   ├── BSD-2-Clause.txt
│   ├── CC0-1.0.txt
│   ├── MIT.txt
│   └── OFL-1.1.txt
├── README.md
├── REUSE.toml
├── docker/
│   ├── Dockerfile
│   ├── Dockerfile.build
│   ├── Dockerfile.subpath-admin
│   ├── docker-compose-dev.yml
│   └── docker-compose.yml
├── docs/
│   ├── README.md
│   ├── apis.md
│   ├── availability.md
│   ├── components.md
│   ├── config.md
│   ├── configurable-columns.md
│   ├── cors-credentials.md
│   ├── csv-import.md
│   ├── custom-menu.md
│   ├── event-reports.md
│   ├── external-auth-provider.md
│   ├── federation.md
│   ├── media.md
│   ├── prefill-login-form.md
│   ├── registration-tokens.md
│   ├── restrict-hs.md
│   ├── reverse-proxy.md
│   ├── room-management.md
│   ├── screenshots/
│   │   ├── README.md
│   │   └── prepare.js
│   ├── server-statistics.md
│   ├── system-users.md
│   ├── testdata/
│   │   ├── element/
│   │   │   ├── config.json
│   │   │   └── nginx.conf
│   │   ├── mas/
│   │   │   └── config.yaml
│   │   ├── nginx/
│   │   │   └── nginx.conf
│   │   ├── postgres.initdb/
│   │   │   └── mas.sql
│   │   └── synapse/
│   │       ├── homeserver.yaml
│   │       ├── synapse.log.config
│   │       └── synapse.signing.key
│   ├── update-api-docs.ts
│   ├── user-badges.md
│   ├── user-management.md
│   ├── user-search.md
│   └── well-known-discovery.md
├── eslint.config.js
├── justfile
├── package.json
├── public/
│   ├── config.json
│   ├── data/
│   │   └── example.csv
│   └── robots.txt
├── src/
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── Context.tsx
│   ├── TEST_COVERAGE_TODO.md
│   ├── assets/
│   │   ├── fonts.css
│   │   └── theme.ts
│   ├── components/
│   │   ├── MatrixWordmark.tsx
│   │   ├── README.md
│   │   ├── etke.cc/
│   │   │   ├── BillingPage.tsx
│   │   │   ├── BillingStatusBadge.tsx
│   │   │   ├── ComponentsPage.tsx
│   │   │   ├── CurrentlyRunningCommand.tsx
│   │   │   ├── EtkeAttribution.test.tsx
│   │   │   ├── EtkeAttribution.tsx
│   │   │   ├── InstanceConfig.tsx
│   │   │   ├── README.md
│   │   │   ├── RichTextEditor.tsx
│   │   │   ├── ServerActionsPage.tsx
│   │   │   ├── ServerCommandsPanel.tsx
│   │   │   ├── ServerNotificationsBadge.test.tsx
│   │   │   ├── ServerNotificationsBadge.tsx
│   │   │   ├── ServerNotificationsPage.tsx
│   │   │   ├── ServerNotificationsUnavailable.test.tsx
│   │   │   ├── ServerNotificationsUnavailable.tsx
│   │   │   ├── ServerStatusBadge.test.tsx
│   │   │   ├── ServerStatusBadge.tsx
│   │   │   ├── ServerStatusPage.test.tsx
│   │   │   ├── ServerStatusPage.tsx
│   │   │   ├── SupportAttachments.tsx
│   │   │   ├── SupportPage.tsx
│   │   │   ├── SupportRequestPage.tsx
│   │   │   ├── hooks/
│   │   │   │   ├── useServerCommands.ts
│   │   │   │   └── useUnits.ts
│   │   │   └── schedules/
│   │   │       ├── components/
│   │   │       │   ├── recurring/
│   │   │       │   │   ├── RecurringCommandEdit.tsx
│   │   │       │   │   ├── RecurringCommandsList.tsx
│   │   │       │   │   └── RecurringDeleteButton.tsx
│   │   │       │   └── scheduled/
│   │   │       │       ├── ScheduledCommandEdit.tsx
│   │   │       │       ├── ScheduledCommandShow.tsx
│   │   │       │       ├── ScheduledCommandsList.tsx
│   │   │       │       └── ScheduledDeleteButton.tsx
│   │   │       └── hooks/
│   │   │           ├── useRecurringCommands.tsx
│   │   │           └── useScheduledCommands.tsx
│   │   ├── hooks/
│   │   │   ├── useDocTitle.test.tsx
│   │   │   └── useDocTitle.tsx
│   │   ├── layout/
│   │   │   ├── AdminLayout.test.tsx
│   │   │   ├── AdminLayout.tsx
│   │   │   ├── Datagrid.test.tsx
│   │   │   ├── Datagrid.tsx
│   │   │   ├── EmptyState.test.tsx
│   │   │   ├── EmptyState.tsx
│   │   │   ├── Footer.test.tsx
│   │   │   ├── Footer.tsx
│   │   │   ├── List.tsx
│   │   │   ├── LoginFormBox.tsx
│   │   │   └── index.ts
│   │   ├── media/
│   │   │   ├── DeleteMediaButton.tsx
│   │   │   ├── ProtectMediaButton.tsx
│   │   │   ├── PurgeRemoteMediaButton.tsx
│   │   │   ├── QuarantineMediaButton.tsx
│   │   │   ├── ViewMedia.tsx
│   │   │   └── index.ts
│   │   ├── rooms/
│   │   │   ├── EventLookupDialog.tsx
│   │   │   ├── RoomHierarchy.test.ts
│   │   │   ├── RoomHierarchy.tsx
│   │   │   └── RoomMessages.tsx
│   │   ├── user-import/
│   │   │   ├── ConflictModeCard.tsx
│   │   │   ├── ErrorsCard.tsx
│   │   │   ├── ResultsCard.tsx
│   │   │   ├── StartImportCard.tsx
│   │   │   ├── StatsCard.tsx
│   │   │   ├── UploadCard.tsx
│   │   │   ├── UserImport.tsx
│   │   │   ├── types.ts
│   │   │   ├── useImportFile.test.ts
│   │   │   └── useImportFile.tsx
│   │   └── users/
│   │       ├── AdminClientConfigItems.tsx
│   │       ├── DeviceDisplayNameInput.tsx
│   │       ├── ExperimentalFeatures.tsx
│   │       ├── ServerNotices.tsx
│   │       ├── UserAccountData.tsx
│   │       ├── UserCounts.tsx
│   │       ├── UserRateLimits.tsx
│   │       ├── buttons/
│   │       │   ├── AllowCrossSigningButton.tsx
│   │       │   ├── BlockRoomButton.tsx
│   │       │   ├── DeleteAllMediaButton.tsx
│   │       │   ├── DeleteRoomButton.tsx
│   │       │   ├── DeleteUserButton.tsx
│   │       │   ├── DeviceCreateButton.tsx
│   │       │   ├── DeviceRemoveButton.tsx
│   │       │   ├── FindUserButton.tsx
│   │       │   ├── LoginAsUserButton.tsx
│   │       │   ├── PurgeHistoryButton.tsx
│   │       │   ├── QuarantineAllMediaButton.tsx
│   │       │   ├── RenewAccountValidityButton.tsx
│   │       │   └── ResetPasswordButton.tsx
│   │       └── fields/
│   │           ├── AvatarField.test.tsx
│   │           ├── AvatarField.tsx
│   │           └── EditableAvatarField.tsx
│   ├── entrypoints/
│   │   ├── auth-callback.html
│   │   └── index.html
│   ├── i18n/
│   │   ├── README.md
│   │   ├── de/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── en/
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── fa/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── fr/
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── i18n-keys.test.ts
│   │   ├── index.test.ts
│   │   ├── index.ts
│   │   ├── it/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── ja/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── pt/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── ru/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   ├── types.d.ts
│   │   ├── uk/
│   │   │   ├── base.ts
│   │   │   ├── common.ts
│   │   │   ├── index.ts
│   │   │   ├── mas.ts
│   │   │   ├── misc_resources.ts
│   │   │   ├── reports.ts
│   │   │   ├── rooms.ts
│   │   │   └── users.ts
│   │   └── zh/
│   │       ├── base.ts
│   │       ├── common.ts
│   │       ├── index.ts
│   │       ├── mas.ts
│   │       ├── misc_resources.ts
│   │       ├── reports.ts
│   │       ├── rooms.ts
│   │       └── users.ts
│   ├── index.tsx
│   ├── pages/
│   │   ├── DonatePage.test.tsx
│   │   ├── DonatePage.tsx
│   │   ├── LoginPage.test.tsx
│   │   ├── LoginPage.tsx
│   │   ├── MASPolicyDataPage.test.tsx
│   │   ├── MASPolicyDataPage.tsx
│   │   ├── auth-callback-error.test.tsx
│   │   ├── auth-callback-error.tsx
│   │   ├── auth-callback.test.tsx
│   │   └── auth-callback.tsx
│   ├── providers/
│   │   ├── README.md
│   │   ├── auth/
│   │   │   ├── index.test.ts
│   │   │   └── index.ts
│   │   ├── data/
│   │   │   ├── etke.test.ts
│   │   │   ├── etke.ts
│   │   │   ├── index.test.ts
│   │   │   ├── index.ts
│   │   │   ├── lifecycle.ts
│   │   │   ├── mas-actions.ts
│   │   │   ├── mas-utils.test.ts
│   │   │   ├── mas-utils.ts
│   │   │   ├── mas.ts
│   │   │   ├── scan.ts
│   │   │   ├── synapse-actions.ts
│   │   │   └── synapse.ts
│   │   ├── http.ts
│   │   ├── matrix.test.ts
│   │   ├── matrix.ts
│   │   ├── serverVersion.ts
│   │   └── types/
│   │       ├── common.ts
│   │       ├── destinations.ts
│   │       ├── etke.ts
│   │       ├── index.ts
│   │       ├── mas.ts
│   │       ├── reports.ts
│   │       ├── rooms.ts
│   │       └── users.ts
│   ├── resourceMap.ts
│   ├── resources/
│   │   ├── README.md
│   │   ├── destinations/
│   │   │   ├── List.tsx
│   │   │   └── index.ts
│   │   ├── mas/
│   │   │   ├── CompatSessions.tsx
│   │   │   ├── OAuth2Sessions.tsx
│   │   │   ├── PersonalSessions.tsx
│   │   │   ├── UpstreamOAuthLinks.tsx
│   │   │   ├── UpstreamOAuthProviders.tsx
│   │   │   ├── UserEmails.tsx
│   │   │   ├── UserSessions.tsx
│   │   │   ├── index.ts
│   │   │   └── shared.tsx
│   │   ├── registration-tokens/
│   │   │   ├── Create.tsx
│   │   │   ├── Edit.tsx
│   │   │   ├── List.tsx
│   │   │   └── index.ts
│   │   ├── reports/
│   │   │   ├── List.tsx
│   │   │   ├── Show.tsx
│   │   │   └── index.ts
│   │   ├── room-directory/
│   │   │   └── index.tsx
│   │   ├── rooms/
│   │   │   ├── List.tsx
│   │   │   ├── Show.tsx
│   │   │   └── index.ts
│   │   ├── scheduled-tasks/
│   │   │   ├── List.tsx
│   │   │   └── index.ts
│   │   ├── statistics/
│   │   │   ├── DatabaseRooms.tsx
│   │   │   ├── UserMedia.tsx
│   │   │   └── index.ts
│   │   └── users/
│   │       ├── Create.tsx
│   │       ├── Edit.tsx
│   │       ├── List.tsx
│   │       └── index.ts
│   ├── utils/
│   │   ├── config.test.ts
│   │   ├── config.ts
│   │   ├── date.test.ts
│   │   ├── date.ts
│   │   ├── error.test.ts
│   │   ├── error.ts
│   │   ├── fetchMedia.test.ts
│   │   ├── fetchMedia.ts
│   │   ├── formatBytes.test.ts
│   │   ├── formatBytes.ts
│   │   ├── icons.ts
│   │   ├── logger.test.ts
│   │   ├── logger.ts
│   │   ├── mxid.test.ts
│   │   ├── mxid.ts
│   │   ├── password.test.ts
│   │   ├── password.ts
│   │   ├── safety.test.ts
│   │   ├── safety.ts
│   │   ├── version.test.ts
│   │   └── version.ts
│   └── vitest.setup.ts
├── tsconfig.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (226 symbols across 61 files)

FILE: docs/screenshots/prepare.js
  function replaceInNode (line 29) | function replaceInNode(node) {

FILE: docs/update-api-docs.ts
  constant MAS_URL (line 13) | const MAS_URL = "https://element-hq.github.io/matrix-authentication-serv...
  constant MAS_DEST (line 14) | const MAS_DEST = "docs/apis/mas.json";
  constant SYNAPSE_REPO (line 16) | const SYNAPSE_REPO = "https://github.com/element-hq/synapse.git";
  constant SYNAPSE_BRANCH (line 17) | const SYNAPSE_BRANCH = "develop";
  constant TMP_DIR (line 18) | const TMP_DIR = ".tmp_synapse_repo";
  constant SYNAPSE_SRC (line 19) | const SYNAPSE_SRC = "docs/admin_api";
  constant SYNAPSE_DEST (line 20) | const SYNAPSE_DEST = "docs/apis/synapse";
  constant MATRIX_SPEC_REPO (line 22) | const MATRIX_SPEC_REPO = "https://github.com/matrix-org/matrix-spec.git";
  constant MATRIX_SPEC_BRANCH (line 23) | const MATRIX_SPEC_BRANCH = "main";
  constant TMP_DIR_MATRIX_SPEC (line 24) | const TMP_DIR_MATRIX_SPEC = ".tmp_matrix_spec_repo";
  constant MATRIX_SPEC_SRCS (line 28) | const MATRIX_SPEC_SRCS = ["data/api", "data-definitions"];
  constant MATRIX_SPEC_DEST (line 29) | const MATRIX_SPEC_DEST = "docs/apis/matrix-spec";
  function ensureDir (line 48) | function ensureDir(p: string) {
  function retry (line 53) | async function retry<T>(fn: () => Promise<T>, label: string, retries = 5...
  function download (line 76) | function download(url: string, dest: string): Promise<void> {
  function copyDir (line 110) | function copyDir(src: string, dest: string) {
  function runGit (line 126) | function runGit(args: string[]) {
  function stepMAS (line 138) | async function stepMAS() {
  function stepSynapse (line 155) | async function stepSynapse() {
  function stepMatrixSpec (line 186) | async function stepMatrixSpec() {

FILE: eslint.config.js
  method create (line 16) | create(context) {

FILE: src/components/MatrixWordmark.tsx
  constant MATRIX_TRADEMARK_TITLE (line 3) | const MATRIX_TRADEMARK_TITLE =
  type MatrixWordmarkProps (line 6) | interface MatrixWordmarkProps {

FILE: src/components/etke.cc/BillingStatusBadge.tsx
  type StyledBadgeProps (line 15) | interface StyledBadgeProps extends BadgeProps {

FILE: src/components/etke.cc/InstanceConfig.tsx
  type InstanceConfig (line 7) | interface InstanceConfig {
  type DisableFeatures (line 15) | interface DisableFeatures {
  type InstanceConfigListener (line 34) | type InstanceConfigListener = () => void;

FILE: src/components/etke.cc/RichTextEditor.tsx
  type RichTextEditorProps (line 11) | interface RichTextEditorProps {

FILE: src/components/etke.cc/ServerNotificationsBadge.test.tsx
  type RenderOpts (line 23) | interface RenderOpts {

FILE: src/components/etke.cc/ServerNotificationsBadge.tsx
  constant SERVER_NOTIFICATIONS_INTERVAL_TIME (line 29) | const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000;

FILE: src/components/etke.cc/ServerNotificationsUnavailable.tsx
  type Props (line 4) | interface Props {

FILE: src/components/etke.cc/ServerStatusBadge.tsx
  type StyledBadgeProps (line 13) | interface StyledBadgeProps extends BadgeProps {
  constant SERVER_STATUS_INTERVAL_TIME (line 51) | const SERVER_STATUS_INTERVAL_TIME = 5 * 60 * 1000;
  constant SERVER_CURRENT_PROCCESS_INTERVAL_TIME (line 53) | const SERVER_CURRENT_PROCCESS_INTERVAL_TIME = 5 * 1000;

FILE: src/components/etke.cc/SupportAttachments.tsx
  constant MAX_FILES (line 9) | const MAX_FILES = 5;
  constant MAX_FILE_BYTES (line 10) | const MAX_FILE_BYTES = 5 * 1024 * 1024;
  constant MAX_TOTAL_BYTES (line 11) | const MAX_TOTAL_BYTES = 10 * 1024 * 1024;
  type Props (line 13) | interface Props {
  type AttachmentMeta (line 36) | interface AttachmentMeta extends SupportAttachment {

FILE: src/components/etke.cc/SupportRequestPage.tsx
  type ResolvedProfile (line 43) | interface ResolvedProfile {
  constant MXID_REGEX (line 48) | const MXID_REGEX = /^@[^:]+:[^:]+$/;

FILE: src/components/layout/AdminLayout.tsx
  constant MAS_RESOURCE_PREFIX (line 214) | const MAS_RESOURCE_PREFIX = "mas_";
  constant MAS_SESSION_RESOURCES (line 221) | const MAS_SESSION_RESOURCES = ["mas_upstream_oauth_providers"];

FILE: src/components/layout/Datagrid.test.tsx
  constant TRANSLATIONS (line 110) | const TRANSLATIONS: Record<string, string> = {
  constant RESOLVED_USERS (line 155) | const RESOLVED_USERS: Record<string, Record<string, unknown>> = {
  constant MEDIA_RECORD (line 176) | const MEDIA_RECORD = {
  constant USER_RECORD (line 197) | const USER_RECORD = {
  constant MEMBER_RECORD (line 214) | const MEMBER_RECORD = {
  function renderWith (line 571) | function renderWith(

FILE: src/components/layout/Datagrid.tsx
  type DatagridBodyProps (line 26) | type DatagridBodyProps = React.ComponentPropsWithRef<typeof DatagridBody>;
  type DatagridRowProps (line 27) | type DatagridRowProps = React.ComponentPropsWithRef<typeof DatagridRow>;
  type DatagridConfigurableProps (line 28) | type DatagridConfigurableProps = React.ComponentProps<typeof DatagridCon...
  type RowLabel (line 31) | type RowLabel = ((record: RaRecord) => string) | string;
  type AccessibleRowProps (line 33) | type AccessibleRowProps = DatagridRowProps & {
  type AccessibleBodyProps (line 38) | type AccessibleBodyProps = Omit<DatagridBodyProps, "row"> & {
  type DatagridProps (line 42) | type DatagridProps = DatagridConfigurableProps & {
  type Translator (line 51) | type Translator = ReturnType<typeof useTranslate>;

FILE: src/components/layout/LoginFormBox.tsx
  type LoginFormBoxProps (line 4) | interface LoginFormBoxProps extends BoxProps {

FILE: src/components/rooms/RoomHierarchy.tsx
  type TreeNode (line 28) | interface TreeNode {

FILE: src/components/rooms/RoomMessages.tsx
  constant COMMON_EVENT_TYPES (line 29) | const COMMON_EVENT_TYPES = [
  constant PAGE_SIZE (line 186) | const PAGE_SIZE = 20;
  type RoomEventFilter (line 188) | interface RoomEventFilter {

FILE: src/components/user-import/types.ts
  type ImportLine (line 3) | interface ImportLine {
  type ParsedStats (line 17) | interface ParsedStats {
  type ChangeStats (line 28) | interface ChangeStats {
  type Progress (line 36) | type Progress = {
  type ImportResult (line 41) | interface ImportResult {

FILE: src/components/user-import/useImportFile.tsx
  constant LOGGING (line 14) | const LOGGING = true;
  constant EXPECTED_FIELDS (line 16) | const EXPECTED_FIELDS = ["id", "displayname"].sort();
  constant FALSE_VALUES (line 18) | const FALSE_VALUES = ["", "0", "false", "no", "off", "null", "undefined"];
  constant TRUE_VALUES (line 19) | const TRUE_VALUES = ["1", "true", "yes", "on"];
  type CsvValidationResult (line 44) | interface CsvValidationResult {

FILE: src/components/users/buttons/DeleteAllMediaButton.tsx
  type DeletionStatus (line 31) | type DeletionStatus = "idle" | "active" | "done";

FILE: src/components/users/buttons/DeleteRoomButton.tsx
  type DeleteRoomButtonProps (line 34) | interface DeleteRoomButtonProps {

FILE: src/components/users/buttons/DeleteUserButton.tsx
  type DeleteUserButtonProps (line 34) | interface DeleteUserButtonProps {

FILE: src/components/users/buttons/FindUserButton.tsx
  type LookupType (line 25) | type LookupType = "threepid" | "auth_provider";

FILE: src/components/users/fields/EditableAvatarField.tsx
  type EditableAvatarFieldProps (line 10) | interface EditableAvatarFieldProps {

FILE: src/i18n/index.ts
  type SupportedLocale (line 8) | type SupportedLocale = (typeof supportedLocales)[number];
  constant RA_STORE_LOCALE_KEY (line 38) | const RA_STORE_LOCALE_KEY = "RaStore.locale";
  function isSupportedLocale (line 40) | function isSupportedLocale(locale: string): locale is SupportedLocale {
  function resolveInitialLocale (line 44) | function resolveInitialLocale(): SupportedLocale {
  function createI18nProvider (line 60) | async function createI18nProvider() {

FILE: src/i18n/types.d.ts
  type SynapseTranslationMessages (line 3) | interface SynapseTranslationMessages extends TranslationMessages {

FILE: src/pages/DonatePage.tsx
  constant DONATE_URL (line 10) | const DONATE_URL = "https://github.com/sponsors/etkecc";

FILE: src/pages/LoginPage.tsx
  type LoginMethod (line 51) | type LoginMethod = "credentials" | "accessToken";
  function useRestrictedBaseUrl (line 57) | function useRestrictedBaseUrl(): [string | null, string[] | null] {

FILE: src/pages/auth-callback.tsx
  type AuthProviderLike (line 12) | interface AuthProviderLike {
  type LocationLike (line 16) | interface LocationLike {
  type Window (line 105) | interface Window {

FILE: src/providers/data/index.ts
  function filterNullValues (line 171) | function filterNullValues(key: string, value: any) {
  function getSearchOrder (line 180) | function getSearchOrder(order: "ASC" | "DESC") {

FILE: src/providers/data/mas-utils.test.ts
  method constructor (line 5) | constructor(

FILE: src/providers/data/scan.ts
  constant SYSTEM_USERS_SCAN_CHUNK_SIZE (line 10) | const SYSTEM_USERS_SCAN_CHUNK_SIZE = 250;
  type SystemUsersScanCacheEntry (line 12) | interface SystemUsersScanCacheEntry {
  type RunVirtualScanOpts (line 44) | interface RunVirtualScanOpts {
  function runVirtualScan (line 72) | async function runVirtualScan(opts: RunVirtualScanOpts): Promise<GetList...

FILE: src/providers/data/synapse.ts
  constant CACHED_MANY_REF (line 34) | const CACHED_MANY_REF: Record<string, any> = {};

FILE: src/providers/matrix.ts
  type ClientRegistration (line 168) | interface ClientRegistration {
  type AuthMetadata (line 216) | interface AuthMetadata {
  type OIDCAuthParams (line 232) | interface OIDCAuthParams {

FILE: src/providers/serverVersion.ts
  type ServerVersions (line 6) | interface ServerVersions {

FILE: src/providers/types/common.ts
  type RaServerNotice (line 21) | interface RaServerNotice {
  type ScheduledTask (line 26) | interface ScheduledTask {
  type DeleteMediaParams (line 36) | interface DeleteMediaParams {
  type DeleteMediaResult (line 42) | interface DeleteMediaResult {
  type UploadMediaParams (line 47) | interface UploadMediaParams {
  type UploadMediaResult (line 53) | interface UploadMediaResult {
  type DatabaseRoomStatistic (line 57) | interface DatabaseRoomStatistic {
  type UserMediaStatistic (line 62) | interface UserMediaStatistic {
  type AdminClientConfig (line 69) | interface AdminClientConfig {
  type SynapseDataProvider (line 74) | interface SynapseDataProvider extends DataProvider {

FILE: src/providers/types/destinations.ts
  type Destination (line 1) | interface Destination {
  type DestinationRoom (line 9) | interface DestinationRoom {

FILE: src/providers/types/etke.ts
  type ServerStatusComponent (line 1) | interface ServerStatusComponent {
  type ServerStatusResponse (line 14) | interface ServerStatusResponse {
  type ServerProcessResponse (line 22) | interface ServerProcessResponse {
  type ServerNotification (line 28) | interface ServerNotification {
  type NotificationsStatus (line 34) | type NotificationsStatus = "ok" | "advisory" | "unavailable";
  type ServerNotificationsResponse (line 36) | interface ServerNotificationsResponse {
  type ServerCommand (line 42) | interface ServerCommand {
  type ServerCommandsResponse (line 51) | type ServerCommandsResponse = Record<string, ServerCommand>;
  type ScheduledCommand (line 53) | interface ScheduledCommand {
  type RecurringCommand (line 61) | interface RecurringCommand {
  type Payment (line 69) | interface Payment {
  type PaymentStatus (line 79) | interface PaymentStatus {
  type PaymentsResponse (line 86) | interface PaymentsResponse {
  type Component (line 93) | interface Component {
  type ComponentSection (line 102) | interface ComponentSection {
  type ComponentsResponse (line 112) | interface ComponentsResponse {
  type SupportRequest (line 119) | interface SupportRequest {
  type SupportMessage (line 127) | interface SupportMessage {
  type SupportRequestDetail (line 139) | interface SupportRequestDetail extends SupportRequest {
  type SupportAttachment (line 143) | interface SupportAttachment {

FILE: src/providers/types/mas.ts
  type MasPaginationLinks (line 3) | interface MasPaginationLinks {
  type MasPageMeta (line 11) | interface MasPageMeta {
  type MASRegistrationTokenAttributes (line 17) | interface MASRegistrationTokenAttributes {
  type MASRegistrationTokenResource (line 28) | interface MASRegistrationTokenResource {
  type MASRegistrationToken (line 38) | interface MASRegistrationToken {
  type MASRegistrationTokenListResponse (line 45) | interface MASRegistrationTokenListResponse {
  type BaseRegistrationTokensResource (line 53) | interface BaseRegistrationTokensResource {
  type RegistrationToken (line 60) | interface RegistrationToken {
  type SynapseRegistrationTokensResourceType (line 72) | interface SynapseRegistrationTokensResourceType extends BaseRegistration...
  type MASRegistrationTokensResourceType (line 78) | interface MASRegistrationTokensResourceType extends BaseRegistrationToke...
  type RegistrationTokensResource (line 88) | type RegistrationTokensResource = SynapseRegistrationTokensResourceType ...
  type MASUserAttributes (line 90) | interface MASUserAttributes {
  type MASUserResource (line 99) | interface MASUserResource {
  type MASUserResponse (line 107) | interface MASUserResponse {
  type MASUserListResponse (line 112) | interface MASUserListResponse {
  type MASUserEmailAttributes (line 118) | interface MASUserEmailAttributes {
  type MASUserEmailResource (line 124) | interface MASUserEmailResource {
  type MASUserEmailListResponse (line 132) | interface MASUserEmailListResponse {
  type MASCompatSessionAttributes (line 138) | interface MASCompatSessionAttributes {
  type MASCompatSessionResource (line 151) | interface MASCompatSessionResource {
  type MASCompatSessionListResponse (line 159) | interface MASCompatSessionListResponse {
  type MASOAuth2SessionAttributes (line 165) | interface MASOAuth2SessionAttributes {
  type MASOAuth2SessionResource (line 178) | interface MASOAuth2SessionResource {
  type MASOAuth2SessionListResponse (line 186) | interface MASOAuth2SessionListResponse {
  type MASPersonalSessionAttributes (line 192) | interface MASPersonalSessionAttributes {
  type MASPersonalSessionResource (line 205) | interface MASPersonalSessionResource {
  type MASPersonalSessionListResponse (line 213) | interface MASPersonalSessionListResponse {
  type MASUserSessionAttributes (line 219) | interface MASUserSessionAttributes {
  type MASUserSessionResource (line 228) | interface MASUserSessionResource {
  type MASUserSessionListResponse (line 236) | interface MASUserSessionListResponse {
  type MASPolicyDataAttributes (line 242) | interface MASPolicyDataAttributes {
  type MASPolicyDataResource (line 247) | interface MASPolicyDataResource {
  type MASPolicyData (line 254) | interface MASPolicyData {
  type MASUpstreamOAuthLinkAttributes (line 260) | interface MASUpstreamOAuthLinkAttributes {
  type MASUpstreamOAuthLinkResource (line 268) | interface MASUpstreamOAuthLinkResource {
  type MASUpstreamOAuthLinkListResponse (line 276) | interface MASUpstreamOAuthLinkListResponse {
  type MASUpstreamOAuthProviderAttributes (line 282) | interface MASUpstreamOAuthProviderAttributes {
  type MASUpstreamOAuthProviderResource (line 290) | interface MASUpstreamOAuthProviderResource {
  type MASUpstreamOAuthProviderListResponse (line 298) | interface MASUpstreamOAuthProviderListResponse {

FILE: src/providers/types/reports.ts
  type EventReport (line 1) | interface EventReport {

FILE: src/providers/types/rooms.ts
  type Room (line 1) | interface Room {
  type RoomState (line 20) | interface RoomState {
  type ForwardExtremity (line 37) | interface ForwardExtremity {
  type TimestampToEventResult (line 44) | interface TimestampToEventResult {
  type RoomEvent (line 49) | interface RoomEvent {
  type EventContextResult (line 60) | interface EventContextResult {
  type RoomMessagesResult (line 69) | interface RoomMessagesResult {
  type HierarchyRoom (line 76) | interface HierarchyRoom {
  type RoomHierarchyResult (line 101) | interface RoomHierarchyResult {

FILE: src/providers/types/users.ts
  type Threepid (line 1) | interface Threepid {
  type ExternalId (line 8) | interface ExternalId {
  type User (line 13) | interface User {
  type Device (line 35) | interface Device {
  type Connection (line 45) | interface Connection {
  type Membership (line 51) | interface Membership {
  type Whois (line 56) | interface Whois {
  type Pusher (line 68) | interface Pusher {
  type UserMedia (line 84) | interface UserMedia {
  type ExperimentalFeaturesModel (line 95) | interface ExperimentalFeaturesModel {
  type RateLimitsModel (line 99) | interface RateLimitsModel {
  type AccountDataModel (line 104) | interface AccountDataModel {
  type UsernameAvailabilityResult (line 111) | interface UsernameAvailabilityResult {

FILE: src/resources/mas/CompatSessions.tsx
  function MASCompatSessionsList (line 86) | function MASCompatSessionsList(props: ListProps) {

FILE: src/resources/mas/OAuth2Sessions.tsx
  function MASOAuth2SessionsList (line 88) | function MASOAuth2SessionsList(props: ListProps) {

FILE: src/resources/mas/PersonalSessions.tsx
  function MASPersonalSessionsList (line 104) | function MASPersonalSessionsList(props: ListProps) {

FILE: src/resources/mas/UpstreamOAuthLinks.tsx
  function MASUpstreamOAuthLinksList (line 73) | function MASUpstreamOAuthLinksList(props: ListProps) {

FILE: src/resources/mas/UpstreamOAuthProviders.tsx
  function MASUpstreamOAuthProvidersList (line 18) | function MASUpstreamOAuthProvidersList(props: ListProps) {

FILE: src/resources/mas/UserEmails.tsx
  function MASUserEmailsList (line 81) | function MASUserEmailsList(props: ListProps) {

FILE: src/resources/mas/UserSessions.tsx
  function MASUserSessionsList (line 86) | function MASUserSessionsList(props: ListProps) {

FILE: src/utils/config.ts
  type Config (line 5) | interface Config {
  type MenuItem (line 15) | interface MenuItem {
  type ConfigListener (line 25) | type ConfigListener = () => void;

FILE: src/utils/date.ts
  constant DATE_FORMAT (line 1) | const DATE_FORMAT: Intl.DateTimeFormatOptions = {
  type TimeSinceResult (line 58) | interface TimeSinceResult {

FILE: src/utils/error.ts
  type MatrixError (line 1) | interface MatrixError {

FILE: src/utils/fetchMedia.ts
  type MediaType (line 16) | type MediaType = "thumbnail" | "original";

FILE: src/utils/mxid.ts
  function generateRandomMXID (line 45) | function generateRandomMXID(): string {
  function getLocalpart (line 59) | function getLocalpart(id: string | Identifier): string {
  function returnMXID (line 72) | function returnMXID(input: string | Identifier): string {

FILE: src/utils/password.test.ts
  constant ALLOWED_CHARS (line 3) | const ALLOWED_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm...
  constant ALPHANUMERIC (line 4) | const ALPHANUMERIC = /^[A-Za-z0-9]+$/;

FILE: src/utils/password.ts
  function generateRandomPassword (line 5) | function generateRandomPassword(length = 64): string {

FILE: src/utils/version.ts
  function resolveVersion (line 3) | function resolveVersion(): string {
  function injectVersion (line 19) | function injectVersion(html: string, version: string): string {

FILE: src/vitest.setup.ts
  method length (line 22) | get length() {

FILE: vite.config.ts
  method configResolved (line 53) | configResolved(config) {
  method closeBundle (line 57) | async closeBundle() {
  method configureServer (line 121) | configureServer(server) {
  method generateBundle (line 140) | generateBundle(_options, bundle) {
  method handler (line 150) | handler(html) {
  method transformIndexHtml (line 161) | transformIndexHtml(html) {
  method generateBundle (line 168) | generateBundle() {
Condensed preview — 345 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,276K chars).
[
  {
    "path": ".dockerignore",
    "chars": 15,
    "preview": "/docs/testdata\n"
  },
  {
    "path": ".editorconfig",
    "chars": 239,
    "preview": "# EditorConfig https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\n"
  },
  {
    "path": ".gitattributes",
    "chars": 17,
    "preview": "yarn*.cjs binary\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 2562,
    "preview": "# Contribution Guidelines\n\nTable of Contents:\n\n<!-- vim-markdown-toc GFM -->\n\n* [Did you find a bug?](#did-you-find-a-bu"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 692,
    "preview": "---\nname: Bug report\nabout: Report a Ketesa bug\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear a"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 758,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for Ketesa\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your "
  },
  {
    "path": ".github/SECURITY.md",
    "chars": 706,
    "preview": "# Security Policy\n\n## Supported Versions\n\nOnly [the last published version](https://github.com/etkecc/ketesa/releases/la"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 396,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull"
  },
  {
    "path": ".github/workflows/reuse.yml",
    "chars": 337,
    "preview": "---\nname: REUSE Compliance Check\n\non: [push, pull_request]\n\npermissions:\n  contents: read\n\njobs:\n  reuse-compliance-chec"
  },
  {
    "path": ".github/workflows/workflow.yml",
    "chars": 7225,
    "preview": "name: CI\non:\n  push:\n    branches: [ \"main\" ]\n    tags: [ \"v*\" ]\nenv:\n  bunny_version: v0.1.0\n  base_path: ./\n  NODE_OPT"
  },
  {
    "path": ".gitignore",
    "chars": 3011,
    "preview": "# Created by https://www.toptal.com/developers/gitignore/api/node,yarn,react,visualstudiocode\n# Edit at https://www.topt"
  },
  {
    "path": ".prettierignore",
    "chars": 33,
    "preview": ".vscode\n.yarn\n**/*.md\n**/*.woff2\n"
  },
  {
    "path": ".watchmanconfig",
    "chars": 39,
    "preview": "{\n  \"ignore_dirs\": [\"docs/testdata\"]\n}\n"
  },
  {
    "path": "LICENSE",
    "chars": 10174,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "LICENSES/Apache-2.0.txt",
    "chars": 10280,
    "preview": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AN"
  },
  {
    "path": "LICENSES/BSD-2-Clause.txt",
    "chars": 1444,
    "preview": "Copyright (c) <<var;name=copyright;original= <year> <owner>;match=.+>> All rights reserved.\r\n\r\nRedistribution and use in"
  },
  {
    "path": "LICENSES/CC0-1.0.txt",
    "chars": 7048,
    "preview": "Creative Commons Legal Code\n\nCC0 1.0 Universal\n\n    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n"
  },
  {
    "path": "LICENSES/MIT.txt",
    "chars": 1077,
    "preview": "MIT License\n\nCopyright (c) <year> <copyright holders>\n\nPermission is hereby granted, free of charge, to any person obtai"
  },
  {
    "path": "LICENSES/OFL-1.1.txt",
    "chars": 4012,
    "preview": "SIL OPEN FONT LICENSE\n\nVersion 1.1 - 26 February 2007\n\nPREAMBLE\n\nThe goals of the Open Font License (OFL) are to stimula"
  },
  {
    "path": "README.md",
    "chars": 18907,
    "preview": "<p align=\"center\">\n  <img alt=\"Ketesa Logo\" src=\"./public/images/logo.webp\" height=\"140\" />\n  <h3 align=\"center\">\n    Ke"
  },
  {
    "path": "REUSE.toml",
    "chars": 62181,
    "preview": "version = 1\n\n[[annotations]]\npath = [\"**\"]\nSPDX-FileCopyrightText = [\n  \"2018-2023 Awesome Technologies Innovationslabor"
  },
  {
    "path": "docker/Dockerfile",
    "chars": 1519,
    "preview": "FROM ghcr.io/static-web-server/static-web-server:2-alpine\n\nARG VERSION=dev\nARG VCS_REF=unknown\nARG BUILD_DATE\n\nLABEL org"
  },
  {
    "path": "docker/Dockerfile.build",
    "chars": 1775,
    "preview": "FROM node:lts AS builder\nARG BASE_PATH=./\nWORKDIR /src\nCOPY . /src\nRUN yarn config set enableTelemetry 0 && \\\n    yarn i"
  },
  {
    "path": "docker/Dockerfile.subpath-admin",
    "chars": 1604,
    "preview": "FROM ghcr.io/static-web-server/static-web-server:2-alpine\n\nARG VERSION=dev\nARG VCS_REF=unknown\nARG BUILD_DATE\n\nLABEL org"
  },
  {
    "path": "docker/docker-compose-dev.yml",
    "chars": 2205,
    "preview": "services:\n  synapse:\n    image: ghcr.io/element-hq/synapse:latest\n    entrypoint: python\n    command: \"-m synapse.app.ho"
  },
  {
    "path": "docker/docker-compose.yml",
    "chars": 707,
    "preview": "services:\n  ketesa:\n    container_name: ketesa\n    hostname: ketesa\n    image: ghcr.io/etkecc/ketesa:latest\n    # build:"
  },
  {
    "path": "docs/README.md",
    "chars": 3866,
    "preview": "# 📚 Documentation\n\nWelcome to the Ketesa documentation! This is the central index for all guides covering configuration,"
  },
  {
    "path": "docs/apis.md",
    "chars": 16800,
    "preview": "# 🔌 Supported APIs\n\nKetesa uses various APIs to manage Matrix homeservers and related services.\nThis document lists all "
  },
  {
    "path": "docs/availability.md",
    "chars": 4499,
    "preview": "# 📦 Availability\n\nThis is the canonical reference for obtaining Ketesa.\n\n> ⚠️ Community-maintained and third-party entri"
  },
  {
    "path": "docs/components.md",
    "chars": 3346,
    "preview": "# 🧩 Components\n\n| Light | Dark |\n|-------|------|\n| ![Components (light)](./screenshots/light/components.webp) | ![Compo"
  },
  {
    "path": "docs/config.md",
    "chars": 5331,
    "preview": "# ⚙️ Configuration\n\nKetesa is flexible by design — configure it once and let it work across any number of deployments or"
  },
  {
    "path": "docs/configurable-columns.md",
    "chars": 3640,
    "preview": "# 🗂️ Configurable Table Columns\n\n| Light | Dark |\n|-------|------|\n| ![Rooms List (light)](./screenshots/light/rooms-lis"
  },
  {
    "path": "docs/cors-credentials.md",
    "chars": 1470,
    "preview": "# 🔐 CORS Credentials\n\nControls how Ketesa sends cookies and credentials when making API requests. Most deployments don't"
  },
  {
    "path": "docs/csv-import.md",
    "chars": 8464,
    "preview": "# 🎯 Bulk CSV User Import\n\nThe bulk CSV import feature lets you create many Matrix user accounts at once by uploading a s"
  },
  {
    "path": "docs/custom-menu.md",
    "chars": 2062,
    "preview": "# 🎯 Custom Menu Items\n\nExtend Ketesa's sidebar navigation with your own links — no rebuild required. Custom menu items a"
  },
  {
    "path": "docs/event-reports.md",
    "chars": 6190,
    "preview": "# 🚨 Event Reports\n\nEvent reports are abuse reports submitted by Matrix users via their client apps. When a user flags a "
  },
  {
    "path": "docs/external-auth-provider.md",
    "chars": 3648,
    "preview": "# 🔑 External Auth Provider\n\nWhen Synapse delegates authentication to an external provider (OIDC, LDAP, and similar), the"
  },
  {
    "path": "docs/federation.md",
    "chars": 5903,
    "preview": "# 🌐 Federation Overview\n\nThe Federation overview shows every remote Matrix server your homeserver communicates (or has c"
  },
  {
    "path": "docs/media.md",
    "chars": 9257,
    "preview": "# 🖼️ Media Management\n\nKetesa provides granular media controls at the file, user, and room level — useful for content mo"
  },
  {
    "path": "docs/prefill-login-form.md",
    "chars": 2320,
    "preview": "# 🔗 Prefilling the Login Form\n\nKetesa's login form can be pre-populated via URL query parameters — handy for sharing a d"
  },
  {
    "path": "docs/registration-tokens.md",
    "chars": 6521,
    "preview": "# 🎟️ Registration Tokens\n\nRegistration tokens are invite codes that users must provide during account registration. They"
  },
  {
    "path": "docs/restrict-hs.md",
    "chars": 1787,
    "preview": "# 🏠 Restricting Available Homeservers\n\nBy default, Ketesa lets users connect to any Matrix homeserver. For managed deplo"
  },
  {
    "path": "docs/reverse-proxy.md",
    "chars": 4412,
    "preview": "# 🌐 Serving Ketesa behind a reverse proxy\n\nRunning Ketesa behind a reverse proxy is the recommended approach for any int"
  },
  {
    "path": "docs/room-management.md",
    "chars": 13790,
    "preview": "# 🏠 Room Management\n\nKetesa gives you deep visibility and control over every room on your server. You can inspect conten"
  },
  {
    "path": "docs/screenshots/README.md",
    "chars": 5628,
    "preview": "# 📸 Screenshots\n\nScreenshots are organized by theme. Each section shows both light and dark variants where available.\n\n-"
  },
  {
    "path": "docs/screenshots/prepare.js",
    "chars": 2445,
    "preview": "/**\n * Screenshot preparation script\n *\n * PURPOSE:\n *   Replaces the real homeserver hostname in the Ketesa UI with a g"
  },
  {
    "path": "docs/server-statistics.md",
    "chars": 6859,
    "preview": "# 📊 Server Statistics & Scheduled Tasks\n\nKetesa exposes three read-focused views that help administrators understand ser"
  },
  {
    "path": "docs/system-users.md",
    "chars": 2525,
    "preview": "# 🤖 System / Appservice-managed Users\n\nMatrix bridges work by creating \"puppet\" accounts for every bridged user — a Tele"
  },
  {
    "path": "docs/testdata/element/config.json",
    "chars": 291,
    "preview": "{\n  \"default_hs_url\": \"http://localhost:8008\",\n  \"default_is_url\": \"https://vector.im\",\n  \"integrations_ui_url\": \"https:"
  },
  {
    "path": "docs/testdata/element/nginx.conf",
    "chars": 912,
    "preview": "worker_processes  1;\nerror_log /var/log/nginx/error.log warn;\npid /tmp/nginx.pid;\nevents {\n\tworker_connections  1024;\n}\n"
  },
  {
    "path": "docs/testdata/mas/config.yaml",
    "chars": 4863,
    "preview": "http:\n  listeners:\n  - name: web\n    resources:\n    - name: discovery\n    - name: human\n    - name: oauth\n    - name: co"
  },
  {
    "path": "docs/testdata/nginx/nginx.conf",
    "chars": 1894,
    "preview": "server {\n    listen 8008;\n    listen [::]:8008;\n\n    # For the federation port\n    listen 8448 default_server;\n    liste"
  },
  {
    "path": "docs/testdata/postgres.initdb/mas.sql",
    "chars": 70,
    "preview": "CREATE DATABASE mas;\nGRANT ALL PRIVILEGES ON DATABASE mas TO synapse;\n"
  },
  {
    "path": "docs/testdata/synapse/homeserver.yaml",
    "chars": 3980,
    "preview": "account_threepid_delegates:\n  msisdn: ''\nalias_creation_rules:\n- action: allow\n  alias: '*'\n  room_id: '*'\n  user_id: '*"
  },
  {
    "path": "docs/testdata/synapse/synapse.log.config",
    "chars": 698,
    "preview": "version: 1\nformatters:\n    precise:\n        format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s -"
  },
  {
    "path": "docs/testdata/synapse/synapse.signing.key",
    "chars": 59,
    "preview": "ed25519 a_FswB rsh+VxdR4YUv6rFM6393VmSEJJxzaDrdwlVwLe2rcRo\n"
  },
  {
    "path": "docs/update-api-docs.ts",
    "chars": 6548,
    "preview": "#!/usr/bin/env node\n\nimport { spawn } from \"child_process\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport https f"
  },
  {
    "path": "docs/user-badges.md",
    "chars": 1986,
    "preview": "# 🏷️ User Badges\n\nKetesa displays role badges on user avatars throughout the interface — in the users list, the user det"
  },
  {
    "path": "docs/user-management.md",
    "chars": 14724,
    "preview": "# 👤 User Management\n\n| Light | Dark |\n|-------|------|\n| ![Users List (light)](./screenshots/light/users-list.webp) | !["
  },
  {
    "path": "docs/user-search.md",
    "chars": 625,
    "preview": "# 🔍 User Search\n\nThe Users list includes a search field that filters by MXID (user ID) and display name.\n\n## Normal sear"
  },
  {
    "path": "docs/well-known-discovery.md",
    "chars": 1781,
    "preview": "# 🔍 Well-Known Discovery\n\nBy default, Ketesa resolves the homeserver URL you enter on the login page via `/.well-known/m"
  },
  {
    "path": "eslint.config.js",
    "chars": 3509,
    "preview": "import js from \"@eslint/js\";\nimport jsxA11y from \"eslint-plugin-jsx-a11y\";\nimport importPlugin from \"eslint-plugin-impor"
  },
  {
    "path": "justfile",
    "chars": 2701,
    "preview": "# Shows help\ndefault:\n    @just --list --justfile {{ justfile() }}\n\n# build the app\nbuild: __install\n    @-rm -rf dist\n "
  },
  {
    "path": "package.json",
    "chars": 3318,
    "preview": "{\n  \"name\": \"ketesa\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Admin UI for Matrix servers, formerly Synapse Admin\",\n  \"k"
  },
  {
    "path": "public/config.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "public/data/example.csv",
    "chars": 234,
    "preview": "id,displayname,password,is_guest,admin,deactivated,threepids\ntestuser22,Jane Doe,secretpassword,false,true,false,\"email:"
  },
  {
    "path": "public/robots.txt",
    "chars": 69,
    "preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow: /\n"
  },
  {
    "path": "src/App.test.tsx",
    "chars": 735,
    "preview": "import { render, screen } from \"@testing-library/react\";\n\nvi.mock(\"./providers/auth\", () => ({\n  __esModule: true,\n  def"
  },
  {
    "path": "src/App.tsx",
    "chars": 5994,
    "preview": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { Admin, CustomRoutes, Resource, reactR"
  },
  {
    "path": "src/Context.tsx",
    "chars": 606,
    "preview": "import { createContext, useContext, useEffect, useState } from \"react\";\n\nimport { Config, GetConfig, SubscribeConfig } f"
  },
  {
    "path": "src/TEST_COVERAGE_TODO.md",
    "chars": 4719,
    "preview": "# Test Coverage TODO — Tier 4\n\nThese areas were intentionally deferred after the Tier 1–3 coverage pass.\nEach item requi"
  },
  {
    "path": "src/assets/fonts.css",
    "chars": 1515,
    "preview": "/*\n * DatagridConfigurable wraps its content in a styled <span class=\"RaConfigurable-root\">.\n * That span has display:in"
  },
  {
    "path": "src/assets/theme.ts",
    "chars": 10900,
    "preview": "import { createTheme, keyframes, type ThemeOptions } from \"@mui/material/styles\";\n\nconst headingFont = '\"Work Sans\", sys"
  },
  {
    "path": "src/components/MatrixWordmark.tsx",
    "chars": 5185,
    "preview": "import { Box, SxProps, Theme, useTheme } from \"@mui/material\";\n\nexport const MATRIX_TRADEMARK_TITLE =\n  \"This logo is th"
  },
  {
    "path": "src/components/README.md",
    "chars": 785,
    "preview": "# components/\n\nShared UI components, organized by feature domain.\n\n## Structure\n\n- `layout/` — App-level layout: AdminLa"
  },
  {
    "path": "src/components/etke.cc/BillingPage.tsx",
    "chars": 17391,
    "preview": "import BuildIcon from \"@mui/icons-material/Build\";\nimport EuroSymbolIcon from \"@mui/icons-material/EuroSymbol\";\nimport S"
  },
  {
    "path": "src/components/etke.cc/BillingStatusBadge.tsx",
    "chars": 3946,
    "preview": "import PaymentIcon from \"@mui/icons-material/Payment\";\nimport { Badge, Theme } from \"@mui/material\";\nimport { BadgeProps"
  },
  {
    "path": "src/components/etke.cc/ComponentsPage.tsx",
    "chars": 19454,
    "preview": "import ExtensionIcon from \"@mui/icons-material/Extension\";\nimport SupportAgentIcon from \"@mui/icons-material/SupportAgen"
  },
  {
    "path": "src/components/etke.cc/CurrentlyRunningCommand.tsx",
    "chars": 1717,
    "preview": "import EngineeringIcon from \"@mui/icons-material/Engineering\";\nimport { Tooltip, Typography, Link, Alert } from \"@mui/ma"
  },
  {
    "path": "src/components/etke.cc/EtkeAttribution.test.tsx",
    "chars": 1068,
    "preview": "import { render, screen } from \"@testing-library/react\";\n\nimport { EtkeAttribution } from \"./EtkeAttribution\";\n\nvi.mock("
  },
  {
    "path": "src/components/etke.cc/EtkeAttribution.tsx",
    "chars": 303,
    "preview": "import { PropsWithChildren } from \"react\";\n\nimport { useInstanceConfig } from \"./InstanceConfig\";\n\nexport const EtkeAttr"
  },
  {
    "path": "src/components/etke.cc/InstanceConfig.tsx",
    "chars": 2374,
    "preview": "import { useSyncExternalStore } from \"react\";\n\nimport createLogger from \"../../utils/logger\";\n\nconst log = createLogger("
  },
  {
    "path": "src/components/etke.cc/README.md",
    "chars": 8677,
    "preview": "# 🌟 etke.cc-specific components\n\nThis is where the etke.cc magic lives. We build everything open-source wherever possibl"
  },
  {
    "path": "src/components/etke.cc/RichTextEditor.tsx",
    "chars": 4832,
    "preview": "import FormatBoldIcon from \"@mui/icons-material/FormatBold\";\nimport FormatItalicIcon from \"@mui/icons-material/FormatIta"
  },
  {
    "path": "src/components/etke.cc/ServerActionsPage.tsx",
    "chars": 2503,
    "preview": "import RestoreIcon from \"@mui/icons-material/Restore\";\nimport ScheduleIcon from \"@mui/icons-material/Schedule\";\nimport {"
  },
  {
    "path": "src/components/etke.cc/ServerCommandsPanel.tsx",
    "chars": 12697,
    "preview": "import { PlayArrow, CheckCircle, HelpCenter, Construction } from \"@mui/icons-material\";\nimport {\n  Table,\n  TableBody,\n "
  },
  {
    "path": "src/components/etke.cc/ServerNotificationsBadge.test.tsx",
    "chars": 4172,
    "preview": "import { act, fireEvent, render, screen } from \"@testing-library/react\";\nimport { memoryStore } from \"ra-core\";\nimport p"
  },
  {
    "path": "src/components/etke.cc/ServerNotificationsBadge.tsx",
    "chars": 9536,
    "preview": "import DeleteIcon from \"@mui/icons-material/Delete\";\nimport NotificationsIcon from \"@mui/icons-material/Notifications\";\n"
  },
  {
    "path": "src/components/etke.cc/ServerNotificationsPage.tsx",
    "chars": 3165,
    "preview": "import DeleteIcon from \"@mui/icons-material/Delete\";\nimport { Box, Typography, Paper, Button } from \"@mui/material\";\nimp"
  },
  {
    "path": "src/components/etke.cc/ServerNotificationsUnavailable.test.tsx",
    "chars": 2122,
    "preview": "import { fireEvent, render, screen } from \"@testing-library/react\";\nimport polyglotI18nProvider from \"ra-i18n-polyglot\";"
  },
  {
    "path": "src/components/etke.cc/ServerNotificationsUnavailable.tsx",
    "chars": 1616,
    "preview": "import { Box, Button, Link, List, ListItem, Typography } from \"@mui/material\";\nimport { useTranslate } from \"react-admin"
  },
  {
    "path": "src/components/etke.cc/ServerStatusBadge.test.tsx",
    "chars": 2323,
    "preview": "import { act, render, waitFor } from \"@testing-library/react\";\nimport { memoryStore } from \"ra-core\";\nimport polyglotI18"
  },
  {
    "path": "src/components/etke.cc/ServerStatusBadge.tsx",
    "chars": 7171,
    "preview": "import MonitorHeartIcon from \"@mui/icons-material/MonitorHeart\";\nimport { Avatar, Badge, Box, Theme } from \"@mui/materia"
  },
  {
    "path": "src/components/etke.cc/ServerStatusPage.test.tsx",
    "chars": 3293,
    "preview": "import { render, screen } from \"@testing-library/react\";\nimport { memoryStore } from \"ra-core\";\nimport polyglotI18nProvi"
  },
  {
    "path": "src/components/etke.cc/ServerStatusPage.tsx",
    "chars": 6817,
    "preview": "import CheckIcon from \"@mui/icons-material/Check\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport Engineering"
  },
  {
    "path": "src/components/etke.cc/SupportAttachments.tsx",
    "chars": 4388,
    "preview": "import AttachFileIcon from \"@mui/icons-material/AttachFile\";\nimport CloseIcon from \"@mui/icons-material/Close\";\nimport {"
  },
  {
    "path": "src/components/etke.cc/SupportPage.tsx",
    "chars": 11503,
    "preview": "import AddIcon from \"@mui/icons-material/Add\";\nimport SupportAgentIcon from \"@mui/icons-material/SupportAgent\";\nimport {"
  },
  {
    "path": "src/components/etke.cc/SupportRequestPage.tsx",
    "chars": 14180,
    "preview": "import ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport SendIcon from \"@mui/icons-material/Send\";\nimport {\n  A"
  },
  {
    "path": "src/components/etke.cc/hooks/useServerCommands.ts",
    "chars": 1650,
    "preview": "import { useState, useEffect } from \"react\";\nimport { useDataProvider, useLocale } from \"react-admin\";\n\nimport { useAppC"
  },
  {
    "path": "src/components/etke.cc/hooks/useUnits.ts",
    "chars": 1100,
    "preview": "import { useState, useEffect } from \"react\";\nimport { useDataProvider, useLocale } from \"react-admin\";\n\nimport { useAppC"
  },
  {
    "path": "src/components/etke.cc/schedules/components/recurring/RecurringCommandEdit.tsx",
    "chars": 9283,
    "preview": "import ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport { Card, CardContent, CardHeader, Box, Alert, Autocompl"
  },
  {
    "path": "src/components/etke.cc/schedules/components/recurring/RecurringCommandsList.tsx",
    "chars": 3798,
    "preview": "import AddIcon from \"@mui/icons-material/Add\";\nimport { Paper } from \"@mui/material\";\nimport { useTheme } from \"@mui/mat"
  },
  {
    "path": "src/components/etke.cc/schedules/components/recurring/RecurringDeleteButton.tsx",
    "chars": 2166,
    "preview": "import DeleteIcon from \"@mui/icons-material/Delete\";\nimport { useTheme } from \"@mui/material/styles\";\nimport { useState "
  },
  {
    "path": "src/components/etke.cc/schedules/components/scheduled/ScheduledCommandEdit.tsx",
    "chars": 6929,
    "preview": "import ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport { Card, CardContent, CardHeader, Box, Autocomplete, Te"
  },
  {
    "path": "src/components/etke.cc/schedules/components/scheduled/ScheduledCommandShow.tsx",
    "chars": 4166,
    "preview": "import ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport { Alert, Box, Card, CardContent, CardHeader, TextField"
  },
  {
    "path": "src/components/etke.cc/schedules/components/scheduled/ScheduledCommandsList.tsx",
    "chars": 2858,
    "preview": "import AddIcon from \"@mui/icons-material/Add\";\nimport { Paper } from \"@mui/material\";\nimport { useTheme } from \"@mui/mat"
  },
  {
    "path": "src/components/etke.cc/schedules/components/scheduled/ScheduledDeleteButton.tsx",
    "chars": 2166,
    "preview": "import DeleteIcon from \"@mui/icons-material/Delete\";\nimport { useTheme } from \"@mui/material/styles\";\nimport { useState "
  },
  {
    "path": "src/components/etke.cc/schedules/hooks/useRecurringCommands.tsx",
    "chars": 537,
    "preview": "import { useQuery } from \"@tanstack/react-query\";\nimport { useDataProvider, useLocale } from \"react-admin\";\n\nimport { us"
  },
  {
    "path": "src/components/etke.cc/schedules/hooks/useScheduledCommands.tsx",
    "chars": 537,
    "preview": "import { useQuery } from \"@tanstack/react-query\";\nimport { useDataProvider, useLocale } from \"react-admin\";\n\nimport { us"
  },
  {
    "path": "src/components/hooks/useDocTitle.test.tsx",
    "chars": 1220,
    "preview": "import { renderHook } from \"@testing-library/react\";\n\nimport { useDocTitle } from \"./useDocTitle\";\n\ndescribe(\"useDocTitl"
  },
  {
    "path": "src/components/hooks/useDocTitle.tsx",
    "chars": 508,
    "preview": "import { useEffect } from \"react\";\n\n/**\n * Custom hook to set the document title dynamically.\n * Appends the provided ti"
  },
  {
    "path": "src/components/layout/AdminLayout.test.tsx",
    "chars": 6279,
    "preview": "import { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { a"
  },
  {
    "path": "src/components/layout/AdminLayout.tsx",
    "chars": 15821,
    "preview": "import GavelIcon from \"@mui/icons-material/Gavel\";\nimport InfoOutlinedIcon from \"@mui/icons-material/InfoOutlined\";\nimpo"
  },
  {
    "path": "src/components/layout/Datagrid.test.tsx",
    "chars": 50170,
    "preview": "// SPDX-FileCopyrightText: 2026 Nikita Chernyi <https://etke.cc>\n// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Regressi"
  },
  {
    "path": "src/components/layout/Datagrid.tsx",
    "chars": 13186,
    "preview": "// SPDX-FileCopyrightText: 2026 Nikita Chernyi\n// SPDX-License-Identifier: Apache-2.0\nimport React, { ReactNode, useCall"
  },
  {
    "path": "src/components/layout/EmptyState.test.tsx",
    "chars": 2805,
    "preview": "import { render, screen } from \"@testing-library/react\";\n\nimport EmptyState from \"./EmptyState\";\n\nvi.mock(\"react-admin\","
  },
  {
    "path": "src/components/layout/EmptyState.tsx",
    "chars": 5342,
    "preview": "import InboxIcon from \"@mui/icons-material/Inbox\";\nimport { Box, Typography, keyframes } from \"@mui/material\";\nimport { "
  },
  {
    "path": "src/components/layout/Footer.test.tsx",
    "chars": 2018,
    "preview": "import { render, screen } from \"@testing-library/react\";\nimport { createTheme, ThemeProvider } from \"@mui/material/style"
  },
  {
    "path": "src/components/layout/Footer.tsx",
    "chars": 3589,
    "preview": "import { Avatar, Box, Link } from \"@mui/material\";\nimport { useTheme } from \"@mui/material/styles\";\nimport { useEffect, "
  },
  {
    "path": "src/components/layout/List.tsx",
    "chars": 1182,
    "preview": "import { cloneElement, isValidElement } from \"react\";\nimport { List as RaList, ListProps } from \"react-admin\";\n\nimport E"
  },
  {
    "path": "src/components/layout/LoginFormBox.tsx",
    "chars": 6162,
    "preview": "import { Box, BoxProps, keyframes } from \"@mui/material\";\nimport { styled } from \"@mui/material/styles\";\n\ninterface Logi"
  },
  {
    "path": "src/components/layout/index.ts",
    "chars": 456,
    "preview": "// SPDX-FileCopyrightText: 2026 Nikita Chernyi\n// SPDX-License-Identifier: Apache-2.0\nexport { default as AdminLayout, A"
  },
  {
    "path": "src/components/media/DeleteMediaButton.tsx",
    "chars": 3537,
    "preview": "import DeleteSweepIcon from \"@mui/icons-material/DeleteSweep\";\nimport {\n  Dialog,\n  DialogActions,\n  DialogContent,\n  Di"
  },
  {
    "path": "src/components/media/ProtectMediaButton.tsx",
    "chars": 3426,
    "preview": "import ClearIcon from \"@mui/icons-material/Clear\";\nimport LockIcon from \"@mui/icons-material/Lock\";\nimport LockOpenIcon "
  },
  {
    "path": "src/components/media/PurgeRemoteMediaButton.tsx",
    "chars": 3402,
    "preview": "import CloudOffIcon from \"@mui/icons-material/CloudOff\";\nimport {\n  Dialog,\n  DialogActions,\n  DialogContent,\n  DialogCo"
  },
  {
    "path": "src/components/media/QuarantineMediaButton.tsx",
    "chars": 3522,
    "preview": "import BlockIcon from \"@mui/icons-material/Block\";\nimport ClearIcon from \"@mui/icons-material/Clear\";\nimport RemoveCircl"
  },
  {
    "path": "src/components/media/ViewMedia.tsx",
    "chars": 4545,
    "preview": "import DownloadIcon from \"@mui/icons-material/Download\";\nimport DownloadingIcon from \"@mui/icons-material/Downloading\";\n"
  },
  {
    "path": "src/components/media/index.ts",
    "chars": 329,
    "preview": "export { DeleteMediaButton } from \"./DeleteMediaButton\";\nexport { PurgeRemoteMediaButton } from \"./PurgeRemoteMediaButto"
  },
  {
    "path": "src/components/rooms/EventLookupDialog.tsx",
    "chars": 6397,
    "preview": "import AlertError from \"@mui/icons-material/ErrorOutline\";\nimport ActionCheck from \"@mui/icons-material/CheckCircle\";\nim"
  },
  {
    "path": "src/components/rooms/RoomHierarchy.test.ts",
    "chars": 4776,
    "preview": "import { describe, expect, it } from \"vitest\";\n\nimport { HierarchyRoom } from \"../../providers/types\";\nimport { buildTre"
  },
  {
    "path": "src/components/rooms/RoomHierarchy.tsx",
    "chars": 12885,
    "preview": "import AccountTreeIcon from \"@mui/icons-material/AccountTree\";\nimport ExpandLessIcon from \"@mui/icons-material/ExpandLes"
  },
  {
    "path": "src/components/rooms/RoomMessages.tsx",
    "chars": 21413,
    "preview": "import ExpandMoreIcon from \"@mui/icons-material/ExpandMore\";\nimport FilterListIcon from \"@mui/icons-material/FilterList\""
  },
  {
    "path": "src/components/user-import/ConflictModeCard.tsx",
    "chars": 1509,
    "preview": "import { NativeSelect, Paper } from \"@mui/material\";\nimport { CardContent, CardHeader, Container } from \"@mui/material\";"
  },
  {
    "path": "src/components/user-import/ErrorsCard.tsx",
    "chars": 875,
    "preview": "import { Container, Paper, CardHeader, CardContent, Stack, Typography } from \"@mui/material\";\nimport { useTranslate } fr"
  },
  {
    "path": "src/components/user-import/ResultsCard.tsx",
    "chars": 2900,
    "preview": "import ArrowBackIcon from \"@mui/icons-material/ArrowBack\";\nimport DownloadIcon from \"@mui/icons-material/Download\";\nimpo"
  },
  {
    "path": "src/components/user-import/StartImportCard.tsx",
    "chars": 1437,
    "preview": "import { Button, Checkbox, Paper, Container } from \"@mui/material\";\nimport { CardActions, FormControlLabel } from \"@mui/"
  },
  {
    "path": "src/components/user-import/StatsCard.tsx",
    "chars": 3625,
    "preview": "import { Card, Paper, Stack, CardContent, CardHeader, Container, Typography } from \"@mui/material\";\nimport { NativeSelec"
  },
  {
    "path": "src/components/user-import/UploadCard.tsx",
    "chars": 1214,
    "preview": "import { CardHeader, CardContent, Container, Link, Stack, Typography, Paper } from \"@mui/material\";\nimport { useTranslat"
  },
  {
    "path": "src/components/user-import/UserImport.tsx",
    "chars": 2046,
    "preview": "import { Stack } from \"@mui/material\";\nimport { useTranslate } from \"ra-core\";\nimport { Title } from \"react-admin\";\n\nimp"
  },
  {
    "path": "src/components/user-import/types.ts",
    "chars": 1060,
    "preview": "import { RaRecord } from \"react-admin\";\n\nexport interface ImportLine {\n  id: string;\n  displayname: string;\n  user_type?"
  },
  {
    "path": "src/components/user-import/useImportFile.test.ts",
    "chars": 3277,
    "preview": "import { parse as parseCsv } from \"papaparse\";\n\nimport { ImportLine } from \"./types\";\nimport { anyToBoolean, validateCsv"
  },
  {
    "path": "src/components/user-import/useImportFile.tsx",
    "chars": 14801,
    "preview": "import { parse as parseCsv, unparse as unparseCsv, ParseResult } from \"papaparse\";\nimport { ChangeEvent, useState } from"
  },
  {
    "path": "src/components/users/AdminClientConfigItems.tsx",
    "chars": 2215,
    "preview": "import { Divider, ListItemText, MenuItem, Switch } from \"@mui/material\";\nimport { useEffect, useRef, useState } from \"re"
  },
  {
    "path": "src/components/users/DeviceDisplayNameInput.tsx",
    "chars": 2024,
    "preview": "import SaveIcon from \"@mui/icons-material/Save\";\nimport { IconButton, InputAdornment, TextField } from \"@mui/material\";\n"
  },
  {
    "path": "src/components/users/ExperimentalFeatures.tsx",
    "chars": 2937,
    "preview": "import { Stack, Switch, Typography } from \"@mui/material\";\nimport { useState, useEffect } from \"react\";\nimport { useReco"
  },
  {
    "path": "src/components/users/ServerNotices.tsx",
    "chars": 4090,
    "preview": "import IconCancel from \"@mui/icons-material/Cancel\";\nimport MessageIcon from \"@mui/icons-material/Message\";\nimport { Dia"
  },
  {
    "path": "src/components/users/UserAccountData.tsx",
    "chars": 3362,
    "preview": "import ArrowDownwardIcon from \"@mui/icons-material/ArrowDownward\";\nimport { Typography, Box, Stack, Accordion, Accordion"
  },
  {
    "path": "src/components/users/UserCounts.tsx",
    "chars": 3306,
    "preview": "import CalendarTodayIcon from \"@mui/icons-material/CalendarToday\";\nimport GavelIcon from \"@mui/icons-material/Gavel\";\nim"
  },
  {
    "path": "src/components/users/UserRateLimits.tsx",
    "chars": 2548,
    "preview": "import { Stack, Typography } from \"@mui/material\";\nimport { TextField } from \"@mui/material\";\nimport { useEffect, useSta"
  },
  {
    "path": "src/components/users/buttons/AllowCrossSigningButton.tsx",
    "chars": 3106,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/BlockRoomButton.tsx",
    "chars": 10376,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/DeleteAllMediaButton.tsx",
    "chars": 14632,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport DeleteForeverIcon from \"@mui/icons-material/DeleteFore"
  },
  {
    "path": "src/components/users/buttons/DeleteRoomButton.tsx",
    "chars": 6921,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport ActionDelete from \"@mui/icons-material/Delete\";\nimport"
  },
  {
    "path": "src/components/users/buttons/DeleteUserButton.tsx",
    "chars": 8438,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport ActionDelete from \"@mui/icons-material/Delete\";\nimport"
  },
  {
    "path": "src/components/users/buttons/DeviceCreateButton.tsx",
    "chars": 3231,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AddIcon from \"@mui/icons-material/Add\";\nimport AlertEr"
  },
  {
    "path": "src/components/users/buttons/DeviceRemoveButton.tsx",
    "chars": 2862,
    "preview": "import DeleteIcon from \"@mui/icons-material/Delete\";\nimport { useState } from \"react\";\nimport {\n  Button,\n  Confirm,\n  D"
  },
  {
    "path": "src/components/users/buttons/FindUserButton.tsx",
    "chars": 5684,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/LoginAsUserButton.tsx",
    "chars": 5365,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/PurgeHistoryButton.tsx",
    "chars": 5812,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/QuarantineAllMediaButton.tsx",
    "chars": 5381,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/RenewAccountValidityButton.tsx",
    "chars": 4071,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/buttons/ResetPasswordButton.tsx",
    "chars": 4031,
    "preview": "import ActionCheck from \"@mui/icons-material/CheckCircle\";\nimport AlertError from \"@mui/icons-material/ErrorOutline\";\nim"
  },
  {
    "path": "src/components/users/fields/AvatarField.test.tsx",
    "chars": 1176,
    "preview": "import { render, screen, waitFor } from \"@testing-library/react\";\nimport { act } from \"react\";\nimport { RecordContextPro"
  },
  {
    "path": "src/components/users/fields/AvatarField.tsx",
    "chars": 4239,
    "preview": "import { Avatar, AvatarProps, Badge, Tooltip } from \"@mui/material\";\nimport { get } from \"lodash\";\nimport { useState, us"
  },
  {
    "path": "src/components/users/fields/EditableAvatarField.tsx",
    "chars": 3308,
    "preview": "import CloudUploadIcon from \"@mui/icons-material/CloudUpload\";\nimport DeleteIcon from \"@mui/icons-material/Delete\";\nimpo"
  },
  {
    "path": "src/entrypoints/auth-callback.html",
    "chars": 10765,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "src/entrypoints/index.html",
    "chars": 9813,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "src/i18n/README.md",
    "chars": 949,
    "preview": "# i18n/\n\nInternationalization. 10 languages, each in its own directory.\n\n## Structure\n\n- `index.ts` — i18n provider setu"
  },
  {
    "path": "src/i18n/de/base.ts",
    "chars": 8929,
    "preview": "import type { TranslationMessages } from \"ra-core\";\n\nconst germanMessages: TranslationMessages = {\n  ra: {\n    action: {"
  },
  {
    "path": "src/i18n/de/common.ts",
    "chars": 22188,
    "preview": "import germanMessages from \"./base\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst common: Recor"
  },
  {
    "path": "src/i18n/de/index.ts",
    "chars": 1627,
    "preview": "import { SynapseTranslationMessages } from \"../types\";\n\nimport common from \"./common\";\nimport mas from \"./mas\";\nimport m"
  },
  {
    "path": "src/i18n/de/mas.ts",
    "chars": 6173,
    "preview": "const mas = {\n  mas_users: {\n    name: \"MAS-Benutzer |||| MAS-Benutzer\",\n    fields: {\n      id: \"MAS-ID\",\n      usernam"
  },
  {
    "path": "src/i18n/de/misc_resources.ts",
    "chars": 6863,
    "preview": "const misc_resources = {\n  scheduled_tasks: {\n    name: \"Geplante Aufgabe |||| Geplante Aufgaben\",\n    fields: {\n      i"
  },
  {
    "path": "src/i18n/de/reports.ts",
    "chars": 719,
    "preview": "const reports = {\n  name: \"Gemeldetes Ereignis |||| Gemeldete Ereignisse\",\n  fields: {\n    id: \"ID\",\n    received_ts: \"M"
  },
  {
    "path": "src/i18n/de/rooms.ts",
    "chars": 8890,
    "preview": "const rooms = {\n  name: \"Raum |||| Räume\",\n  fields: {\n    room_id: \"Raum-ID\",\n    name: \"Name\",\n    canonical_alias: \"A"
  },
  {
    "path": "src/i18n/de/users.ts",
    "chars": 9834,
    "preview": "const users = {\n  name: \"Benutzer\",\n  email: \"E-Mail\",\n  msisdn: \"Telefon\",\n  threepid: \"E-Mail / Telefon\",\n  membership"
  },
  {
    "path": "src/i18n/en/common.ts",
    "chars": 20275,
    "preview": "import englishMessages from \"ra-language-english\";\n\n// Top-level non-resource keys: ra built-ins + ketesa + import_users"
  },
  {
    "path": "src/i18n/en/index.ts",
    "chars": 1627,
    "preview": "import { SynapseTranslationMessages } from \"../types\";\n\nimport common from \"./common\";\nimport mas from \"./mas\";\nimport m"
  },
  {
    "path": "src/i18n/en/mas.ts",
    "chars": 5850,
    "preview": "const mas = {\n  mas_users: {\n    name: \"MAS User |||| MAS Users\",\n    fields: {\n      id: \"MAS ID\",\n      username: \"Use"
  },
  {
    "path": "src/i18n/en/misc_resources.ts",
    "chars": 6770,
    "preview": "// Miscellaneous resources: scheduled_tasks, connections, devices, users_media,\n// protect_media, quarantine_media, push"
  },
  {
    "path": "src/i18n/en/reports.ts",
    "chars": 627,
    "preview": "const reports = {\n  name: \"Reported event |||| Reported events\",\n  fields: {\n    id: \"ID\",\n    received_ts: \"Reported at"
  },
  {
    "path": "src/i18n/en/rooms.ts",
    "chars": 7843,
    "preview": "const rooms = {\n  name: \"Room |||| Rooms\",\n  fields: {\n    room_id: \"Room ID\",\n    name: \"Name\",\n    canonical_alias: \"A"
  },
  {
    "path": "src/i18n/en/users.ts",
    "chars": 8443,
    "preview": "const users = {\n  name: \"User |||| Users\",\n  email: \"Email\",\n  msisdn: \"Phone\",\n  threepid: \"Email / Phone\",\n  membershi"
  },
  {
    "path": "src/i18n/fa/base.ts",
    "chars": 8209,
    "preview": "import type { TranslationMessages } from \"ra-core\";\n\nconst farsiMessages: TranslationMessages = {\n  ra: {\n    action: {\n"
  },
  {
    "path": "src/i18n/fa/common.ts",
    "chars": 19981,
    "preview": "import farsiMessages from \"./base\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst common: Record"
  },
  {
    "path": "src/i18n/fa/index.ts",
    "chars": 1627,
    "preview": "import { SynapseTranslationMessages } from \"../types\";\n\nimport common from \"./common\";\nimport mas from \"./mas\";\nimport m"
  },
  {
    "path": "src/i18n/fa/mas.ts",
    "chars": 5880,
    "preview": "const mas = {\n  mas_users: {\n    name: \"کاربر MAS |||| کاربران MAS\",\n    fields: {\n      id: \"شناسه MAS\",\n      username"
  },
  {
    "path": "src/i18n/fa/misc_resources.ts",
    "chars": 6444,
    "preview": "const misc_resources = {\n  scheduled_tasks: {\n    name: \"وظیفه زمان‌بندی‌شده |||| وظایف زمان‌بندی‌شده\",\n    fields: {\n  "
  },
  {
    "path": "src/i18n/fa/reports.ts",
    "chars": 664,
    "preview": "const reports = {\n  name: \"رویداد گزارش شده |||| رویدادهای گزارش شده\",\n  fields: {\n    id: \"شناسه\",\n    received_ts: \"زم"
  },
  {
    "path": "src/i18n/fa/rooms.ts",
    "chars": 7734,
    "preview": "const rooms = {\n  name: \"اتاق |||| اتاق ها\",\n  fields: {\n    room_id: \"شناسه اتاق\",\n    name: \"نام\",\n    canonical_alias"
  },
  {
    "path": "src/i18n/fa/users.ts",
    "chars": 8381,
    "preview": "const users = {\n  name: \"کاربر |||| کاربران\",\n  email: \"ایمیل\",\n  msisdn: \"شماره تلفن\",\n  threepid: \"ایمیل / شماره تلفن\""
  },
  {
    "path": "src/i18n/fr/common.ts",
    "chars": 22825,
    "preview": "import frenchMessages from \"ra-language-french\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst c"
  },
  {
    "path": "src/i18n/fr/index.ts",
    "chars": 1627,
    "preview": "import { SynapseTranslationMessages } from \"../types\";\n\nimport common from \"./common\";\nimport mas from \"./mas\";\nimport m"
  },
  {
    "path": "src/i18n/fr/mas.ts",
    "chars": 6276,
    "preview": "const mas = {\n  mas_users: {\n    name: \"Utilisateur MAS |||| Utilisateurs MAS\",\n    fields: {\n      id: \"ID MAS\",\n      "
  },
  {
    "path": "src/i18n/fr/misc_resources.ts",
    "chars": 7277,
    "preview": "const misc_resources = {\n  scheduled_tasks: {\n    name: \"Tâche planifiée |||| Tâches planifiées\",\n    fields: {\n      id"
  },
  {
    "path": "src/i18n/fr/reports.ts",
    "chars": 724,
    "preview": "const reports = {\n  name: \"Événement signalé |||| Événements signalés\",\n  fields: {\n    id: \"Identifiant\",\n    received_"
  },
  {
    "path": "src/i18n/fr/rooms.ts",
    "chars": 9136,
    "preview": "const rooms = {\n  name: \"Salon |||| Salons\",\n  fields: {\n    room_id: \"Identifiant du salon\",\n    name: \"Nom\",\n    canon"
  },
  {
    "path": "src/i18n/fr/users.ts",
    "chars": 10156,
    "preview": "const users = {\n  name: \"Utilisateur |||| Utilisateurs\",\n  email: \"Adresse électronique\",\n  msisdn: \"Numéro de téléphone"
  },
  {
    "path": "src/i18n/i18n-keys.test.ts",
    "chars": 1865,
    "preview": "import de from \"./de\";\nimport en from \"./en\";\nimport fa from \"./fa\";\nimport fr from \"./fr\";\nimport itMessages from \"./it"
  },
  {
    "path": "src/i18n/index.test.ts",
    "chars": 5554,
    "preview": "import { MockedFunction } from \"vitest\";\nimport { resolveBrowserLocale } from \"react-admin\";\n\nimport { createI18nProvide"
  }
]

// ... and 145 more files (download for full content)

About this extraction

This page contains the full source code of the etkecc/synapse-admin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 345 files (1.9 MB), approximately 517.4k tokens, and a symbol index with 226 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!