mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-07 14:00:56 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d08a360572 |
@@ -88,8 +88,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
@@ -161,8 +159,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
@@ -190,8 +186,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||
@@ -202,27 +196,6 @@ jobs:
|
||||
- name: Check format
|
||||
run: cd src/frontend/ && npm run check
|
||||
|
||||
test-front:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||
|
||||
- name: Run tests
|
||||
run: cd src/frontend/ && npm test
|
||||
|
||||
lint-sdk:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -233,8 +206,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
@@ -256,8 +227,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
@@ -10,14 +10,11 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(helm) import environment variables from Secrets and ConfigMaps
|
||||
- 🔒(backend) throttle meeting link generation
|
||||
- 🔒️(backend) add a daily cap on room creation
|
||||
- 🔧(summary) add setting to control Sentry traces sampling rate
|
||||
- ✨(frontend) let signed-out visitors start a meeting
|
||||
- ✨(backend) expose `allow_unregistered_rooms` in the frontend configuration
|
||||
- ✅(frontend) add vitest so the frontend can carry unit tests
|
||||
- ♿️(frontend) make participant pagination readable and keyboard reachable #1775
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -35,8 +32,6 @@ and this project adheres to
|
||||
- 🔒️(summary) redact meeting content from Sentry events
|
||||
- 🐛(frontend) hide tooltips until they have a computed placement
|
||||
- 🐛(brevo) use django-lasuite for marketing management
|
||||
- ♿️(frontend) expose loading state to assistive technology
|
||||
- 🐛(frontend) honour Keep hand raised when picture-in-picture is open
|
||||
|
||||
## [1.33.0] - 2026-09-30
|
||||
|
||||
@@ -167,7 +162,6 @@ and this project adheres to
|
||||
### Added
|
||||
|
||||
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
||||
|
||||
### Changed
|
||||
|
||||
- 📱(frontend) collapse mobile control bar items on narrow viewports
|
||||
|
||||
@@ -169,7 +169,7 @@ run-summary: ## start only the summary application and all needed services
|
||||
@$(COMPOSE) up --force-recreate -d celery-summary-summarize
|
||||
.PHONY: run-summary
|
||||
|
||||
run-agents: ## start the LiveKit agents (opt-in, see docs/developping_locally.md)
|
||||
run-agents: ## start the multi-user-transcriber agent
|
||||
@$(MAKE) run-agent-multi-user-transcriber
|
||||
@$(MAKE) run-agent-metadata-collector
|
||||
.PHONY: run-agents
|
||||
@@ -186,6 +186,7 @@ run:
|
||||
run: ## start the wsgi (production) and development server
|
||||
@$(MAKE) run-backend
|
||||
@$(MAKE) run-summary
|
||||
@$(MAKE) run-agents
|
||||
@$(COMPOSE) up --force-recreate -d frontend
|
||||
.PHONY: run
|
||||
|
||||
@@ -259,8 +260,7 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
|
||||
test: ## run project tests; pass extra pytest args via ARGS, e.g. `make test ARGS="-vv"`
|
||||
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(MAKE) test-back-parallel ARGS="$${args}" && \
|
||||
$(MAKE) test-summary ARGS="$${args}" && \
|
||||
$(MAKE) test-frontend
|
||||
$(MAKE) test-summary ARGS="$${args}"
|
||||
.PHONY: test
|
||||
|
||||
test-back: ## run back-end tests (pass extra pytest args via ARGS)
|
||||
@@ -278,10 +278,6 @@ test-summary: ## run summary tests (pass extra pytest args via ARGS)
|
||||
bin/pytest-summary $${args}
|
||||
.PHONY: test-summary
|
||||
|
||||
test-frontend: ## run the frontend unit tests
|
||||
cd $(PATH_FRONT) && npm test
|
||||
.PHONY: test-frontend
|
||||
|
||||
makemigrations: ## run django makemigrations for the Meet project.
|
||||
@echo "$(BOLD)Running makemigrations$(RESET)"
|
||||
@$(COMPOSE) up -d postgresql
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/suitenumerique/meet"/>
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/suitenumerique/meet"/>
|
||||
<a href="https://github.com/suitenumerique/meet/blob/main/LICENSE">
|
||||
<img alt="GitHub license" src="https://img.shields.io/github/license/suitenumerique/meet"/>
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/license/suitenumerique/meet"/>
|
||||
</a>
|
||||
<a href="https://digitalpublicgoods.net/r/la-suite-meet-simple-video-conferencing">
|
||||
<img src="https://img.shields.io/badge/Verified-DPG-3333AB?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iMzEiIGhlaWdodD0iMzMiIHZpZXdCb3g9IjAgMCAzMSAzMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjIwMDggMjEuMzY3OEwxMC4xNzM2IDE4LjAxMjRMMTEuNTIxOSAxNi40MDAzTDEzLjk5MjggMTguNDU5TDE5LjYyNjkgMTIuMjExMUwyMS4xOTA5IDEzLjYxNkwxNC4yMDA4IDIxLjM2NzhaTTI0LjYyNDEgOS4zNTEyN0wyNC44MDcxIDMuMDcyOTdMMTguODgxIDUuMTg2NjJMMTUuMzMxNCAtMi4zMzA4MmUtMDVMMTEuNzgyMSA1LjE4NjYyTDUuODU2MDEgMy4wNzI5N0w2LjAzOTA2IDkuMzUxMjdMMCAxMS4xMTc3TDMuODQ1MjEgMTYuMDg5NUwwIDIxLjA2MTJMNi4wMzkwNiAyMi44Mjc3TDUuODU2MDEgMjkuMTA2TDExLjc4MjEgMjYuOTkyM0wxNS4zMzE0IDMyLjE3OUwxOC44ODEgMjYuOTkyM0wyNC44MDcxIDI5LjEwNkwyNC42MjQxIDIyLjgyNzdMMzAuNjYzMSAyMS4wNjEyTDI2LjgxNzYgMTYuMDg5NUwzMC42NjMxIDExLjExNzdMMjQuNjI0MSA5LjM1MTI3WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==" alt="DPG Badge"/>
|
||||
@@ -49,8 +49,8 @@ Powered by [LiveKit](https://livekit.io/), La Suite Meet offers Zoom-level perfo
|
||||
- Telephony integration
|
||||
- Secure participation with robust authentication and access control
|
||||
- Customizable frontend style
|
||||
- LiveKit advanced features including:
|
||||
- speaker detection
|
||||
- LiveKit Advances features including :
|
||||
- speaker detection
|
||||
- simulcast
|
||||
- end-to-end optimizations
|
||||
- selective subscription
|
||||
|
||||
+25
-76
@@ -16,54 +16,43 @@ the following command inside your docker container:
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Marketing / Brevo integration now uses `django-lasuite`
|
||||
### Purging inactive rooms
|
||||
|
||||
The in-house marketing service (`core.services.marketing`) has been removed and
|
||||
replaced by the shared implementation from `django-lasuite`
|
||||
(`lasuite.marketing`). This fixes a bug where updating a user's contact on
|
||||
Brevo overwrote their list memberships, removing lists set by other
|
||||
La Suite products. Existing lists are now preserved and merged.
|
||||
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md).
|
||||
|
||||
**Celery worker required.** Newsletter signup on login
|
||||
(`SIGNUP_NEW_USER_TO_MARKETING_EMAIL=True`) is now dispatched as an
|
||||
asynchronous Celery task (`lasuite.marketing.tasks.create_or_update_contact`)
|
||||
instead of a synchronous call with a 1s timeout. Make sure a Celery worker is
|
||||
running alongside the backend, otherwise contacts will never be pushed to Brevo.
|
||||
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`.
|
||||
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading.
|
||||
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window.
|
||||
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged.
|
||||
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL.
|
||||
|
||||
**Configuration changes.** The following environment variables / settings are
|
||||
**removed** and no longer read:
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
|
||||
|
||||
- `MARKETING_SERVICE_CLASS`
|
||||
- `BREVO_API_KEY`
|
||||
- `BREVO_API_CONTACT_LIST_IDS`
|
||||
- `BREVO_API_CONTACT_ATTRIBUTES` (previous default: `{"VISIO_USER": True}`)
|
||||
- `BREVO_API_TIMEOUT`
|
||||
### Local development: MinIO replaced by Garage
|
||||
|
||||
They are replaced by a single `LASUITE_MARKETING` setting, configured through:
|
||||
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------ | ------------------------------------------------ | -------------------------------------------- |
|
||||
| `LASUITE_MARKETING_BACKEND` | `lasuite.marketing.backends.dummy.DummyBackend` | Backend class path |
|
||||
| `LASUITE_MARKETING_PARAMETERS` | `{}` | Keyword arguments passed to the backend |
|
||||
To migrate a local environment:
|
||||
|
||||
⚠️ The default backend is now a **dummy** (no-op). If you previously used
|
||||
Brevo, you must explicitly configure it, otherwise signups are silently dropped:
|
||||
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans`
|
||||
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media`
|
||||
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
|
||||
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
|
||||
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
|
||||
|
||||
LASUITE_MARKETING_BACKEND=lasuite.marketing.backends.brevo.BrevoBackend
|
||||
LASUITE_MARKETING_PARAMETERS={"api_key": "<your-brevo-api-key>", "api_contact_list_ids": [1, 2], "api_contact_attributes": {"VISIO_USER": True}}
|
||||
### Summary service and metadata collector: boto3 replaces the minio client
|
||||
|
||||
Migration mapping:
|
||||
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings.
|
||||
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`, then to `us-east-1`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
|
||||
|
||||
- `BREVO_API_KEY` → `api_key`
|
||||
- `BREVO_API_CONTACT_LIST_IDS` → `api_contact_list_ids`
|
||||
- `BREVO_API_CONTACT_ATTRIBUTES` → `api_contact_attributes` (re-add
|
||||
`{"VISIO_USER": True}` if you relied on the old default)
|
||||
- `BREVO_API_TIMEOUT` → no equivalent (the request runs in a background task)
|
||||
Also:
|
||||
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs
|
||||
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`.
|
||||
|
||||
Note: `BREVO_API_KEY` used to support being read from a secret file; the API key
|
||||
now lives inside `LASUITE_MARKETING_PARAMETERS`, so adapt how you inject that
|
||||
secret (e.g. build the whole variable from your secret store).
|
||||
### Helm chart: media services default to Garage
|
||||
|
||||
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
|
||||
### Recording encoding settings replaced by a resolution/profile model
|
||||
|
||||
The `RECORDING_ENCODING_*` settings introduced in v1.16.0 exposed raw encoder
|
||||
@@ -186,46 +175,6 @@ Before enabling it:
|
||||
See [docs/features/recording.md](docs/features/recording.md#tuning-recording-encoding)
|
||||
for the full setting reference, the shipped profile table and the tuning caveats.
|
||||
|
||||
## v1.33.0
|
||||
|
||||
### Purging inactive rooms
|
||||
|
||||
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md).
|
||||
|
||||
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`.
|
||||
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading.
|
||||
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window.
|
||||
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged.
|
||||
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL.
|
||||
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
|
||||
|
||||
### Local development: MinIO replaced by Garage
|
||||
|
||||
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost.
|
||||
|
||||
To migrate a local environment:
|
||||
|
||||
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans`
|
||||
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media`
|
||||
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
|
||||
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
|
||||
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
|
||||
|
||||
### Summary service and metadata collector: boto3 replaces the minio client
|
||||
|
||||
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings.
|
||||
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
|
||||
|
||||
Also:
|
||||
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs
|
||||
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`.
|
||||
|
||||
### Helm chart: media services default to Garage
|
||||
|
||||
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
|
||||
|
||||
## v1.30.0
|
||||
|
||||
### Removing S3 storage-event webhooks for recordings
|
||||
|
||||
+8
-9
@@ -104,16 +104,15 @@ k8s_yaml(secret_yaml_generic(
|
||||
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .'))
|
||||
|
||||
k8s_resource('dev-backend-garage-cors', resource_deps=['dev-backend-garage'])
|
||||
k8s_resource('dev-backend-keycloak', resource_deps=['dev-backend-keycloak-pg'])
|
||||
k8s_resource('meet-backend', resource_deps=['dev-backend-postgres', 'dev-backend-garage-cors', 'dev-backend-redis', 'dev-backend-keycloak', 'livekit-livekit-server'])
|
||||
k8s_resource('meet-celery-backend', resource_deps=['dev-backend-redis'])
|
||||
k8s_resource('meet-celery-summarize', resource_deps=['dev-backend-redis'])
|
||||
k8s_resource('meet-celery-summary-backend', resource_deps=['dev-backend-redis'])
|
||||
k8s_resource('meet-celery-transcribe-default', resource_deps=['dev-backend-redis'])
|
||||
k8s_resource('livekit-livekit-server', resource_deps=['dev-backend-redis'])
|
||||
k8s_resource('garage-cors', resource_deps=['garage'])
|
||||
k8s_resource('meet-backend', resource_deps=['postgresql', 'garage-cors', 'redis', 'livekit-livekit-server'])
|
||||
k8s_resource('meet-celery-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summarize', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summary-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-transcribe-default', resource_deps=['redis'])
|
||||
k8s_resource('livekit-livekit-server', resource_deps=['redis'])
|
||||
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
|
||||
k8s_resource('livekit-egress', resource_deps=['livekit-livekit-server'])
|
||||
k8s_resource('keycloak', resource_deps=['kc-postgresql'])
|
||||
# Trigger once on launch
|
||||
k8s_resource(
|
||||
'meet-backend-createsuperuser',
|
||||
|
||||
+8
-5
@@ -9,8 +9,6 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:5
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
mailcatcher:
|
||||
image: sj26/mailcatcher:latest
|
||||
@@ -273,6 +271,11 @@ services:
|
||||
- ./src/agents:/app
|
||||
- /app/.venv
|
||||
|
||||
redis-summary:
|
||||
image: redis:5
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
app-summary-dev:
|
||||
build:
|
||||
context: src/summary
|
||||
@@ -287,7 +290,7 @@ services:
|
||||
volumes:
|
||||
- ./src/summary:/app
|
||||
depends_on:
|
||||
- redis
|
||||
- redis-summary
|
||||
|
||||
celery-summary-transcribe:
|
||||
container_name: celery-summary-transcribe
|
||||
@@ -301,7 +304,7 @@ services:
|
||||
volumes:
|
||||
- ./src/summary:/app
|
||||
depends_on:
|
||||
- redis
|
||||
- redis-summary
|
||||
- app-summary-dev
|
||||
- garage
|
||||
develop:
|
||||
@@ -321,7 +324,7 @@ services:
|
||||
volumes:
|
||||
- ./src/summary:/app
|
||||
depends_on:
|
||||
- redis
|
||||
- redis-summary
|
||||
- app-summary-dev
|
||||
- garage
|
||||
develop:
|
||||
|
||||
@@ -107,37 +107,6 @@ $ npm i
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
### LiveKit agents (optional)
|
||||
|
||||
The LiveKit agents are not started by `make run`. Each one runs its own
|
||||
container and stays connected to LiveKit, which costs CPU and memory you
|
||||
don't need unless you work on the features they power. Start them only
|
||||
when you need them.
|
||||
|
||||
| Agent | Feature | Make command | Setting in `env.d/development/common` |
|
||||
|---|---|---|---|
|
||||
| `metadata-collector-dev` | Recording metadata (used to identify speakers in transcripts) | `make run-agent-metadata-collector` | `METADATA_COLLECTOR_ENABLED=True` |
|
||||
| `multi-user-transcriber-dev` | Live subtitles | `make run-agent-multi-user-transcriber` | `ROOM_SUBTITLE_ENABLED=True` |
|
||||
|
||||
To start both at once:
|
||||
```shellscript
|
||||
$ make run-agents
|
||||
```
|
||||
|
||||
Then set the matching settings to `True` and restart the backend so it
|
||||
picks them up:
|
||||
```shellscript
|
||||
$ make run-backend
|
||||
```
|
||||
|
||||
The multi-user transcriber also needs a speech-to-text provider. Configure
|
||||
`STT_PROVIDER` and its credentials in
|
||||
`env.d/development/multi_user_transcriber`.
|
||||
|
||||
Keep the settings and the agents in sync: if a setting is `True` while its
|
||||
agent is stopped, the backend still dispatches jobs to it and the feature
|
||||
fails silently.
|
||||
|
||||
---
|
||||
|
||||
## Adding Content
|
||||
|
||||
@@ -104,11 +104,10 @@ ROOM_TELEPHONY_ENABLED=True
|
||||
# ROOMKIT_ENABLED = True
|
||||
# ROOMKIT_SERVER_TO_SERVER_API_TOKEN = ThisIsAnExampleKeyForDevPurposeOnly
|
||||
|
||||
# LiveKit agents (opt-in, start them with `make run-agents`)
|
||||
# Metadata (requires the metadata-collector agent)
|
||||
METADATA_COLLECTOR_ENABLED=False
|
||||
# Metadata
|
||||
METADATA_COLLECTOR_ENABLED=True
|
||||
|
||||
# Subtitle (requires the multi-user-transcriber agent)
|
||||
# Subtitle
|
||||
ROOM_SUBTITLE_ENABLED=False
|
||||
|
||||
FRONTEND_USE_FRENCH_GOV_FOOTER=False
|
||||
|
||||
@@ -9,10 +9,6 @@ AWS_S3_ACCESS_KEY_ID="meet-access-key"
|
||||
AWS_S3_SECRET_ACCESS_KEY="meet-secret-access-key"
|
||||
AWS_S3_REGION_NAME="local"
|
||||
|
||||
CELERY_BROKER_URL="redis://redis:6379/2"
|
||||
CELERY_RESULT_BACKEND="redis://redis:6379/2"
|
||||
TASK_TRACKER_REDIS_URL="redis://redis:6379/2"
|
||||
|
||||
WHISPERX_BASE_URL="https://configure-your-url.com"
|
||||
WHISPERX_ASR_MODEL="large-v2"
|
||||
WHISPERX_API_KEY="your-secret-key"
|
||||
|
||||
Generated
+4
-278
@@ -64,8 +64,7 @@
|
||||
"typescript-eslint": "8.60.1",
|
||||
"vite": "8.0.14",
|
||||
"vite-plugin-static-copy": "4.1.1",
|
||||
"vite-plugin-svgr": "5.2.0",
|
||||
"vitest": "5.0.2"
|
||||
"vite-plugin-svgr": "5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/react-spectrum": {
|
||||
@@ -943,9 +942,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2507,24 +2506,6 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/dom-mediacapture-record": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
|
||||
@@ -2805,64 +2786,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz",
|
||||
"integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@vitest/spy": "5.0.2",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^1.2.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker/node_modules/magic-string": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz",
|
||||
"integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.25",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
|
||||
@@ -3228,16 +3151,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types-flow": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
|
||||
@@ -4217,16 +4130,6 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -5127,13 +5030,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
|
||||
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
@@ -5636,16 +5532,6 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
|
||||
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
@@ -8761,20 +8647,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
|
||||
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -10348,13 +10220,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
|
||||
"integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@@ -10661,26 +10526,6 @@
|
||||
"xtend": "~4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz",
|
||||
"integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
|
||||
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -11765,112 +11610,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz",
|
||||
"integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/mocker": "5.0.2",
|
||||
"chai": "^6.2.2",
|
||||
"es-module-lexer": "^2.3.2",
|
||||
"expect-type": "^1.4.0",
|
||||
"magic-string": "^1.2.3",
|
||||
"obug": "^2.1.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"std-env": "^4.2.0",
|
||||
"tinybench": "^6.1.4",
|
||||
"tinyexec": "^1.3.0",
|
||||
"tinyglobby": "^0.2.17",
|
||||
"why-is-node-running": "^3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "5.0.2",
|
||||
"@vitest/browser-preview": "5.0.2",
|
||||
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.2",
|
||||
"@vitest/coverage-v8": "5.0.2",
|
||||
"@vitest/ui": "5.0.2",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/magic-string": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitest/node_modules/picomatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/walk-sync": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz",
|
||||
@@ -12055,19 +11794,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz",
|
||||
"integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.11"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"dev": "panda codegen && vite",
|
||||
"build": "panda codegen && tsc -b && vite build",
|
||||
"build:debug": "VITE_ANALYZE=true npm run build -- --debug",
|
||||
"test": "panda codegen && vitest run",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"preview": "vite preview",
|
||||
@@ -72,7 +71,6 @@
|
||||
"typescript-eslint": "8.60.1",
|
||||
"vite": "8.0.14",
|
||||
"vite-plugin-static-copy": "4.1.1",
|
||||
"vite-plugin-svgr": "5.2.0",
|
||||
"vitest": "5.0.2"
|
||||
"vite-plugin-svgr": "5.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@ export const LoadingScreen = ({
|
||||
<Screen layout={layout} header={header} footer={footer}>
|
||||
<CenteredContent>
|
||||
<Center>
|
||||
<p role="status" aria-live="polite">
|
||||
{t('loading')}
|
||||
</p>
|
||||
<p>{t('loading')}</p>
|
||||
</Center>
|
||||
</CenteredContent>
|
||||
</Screen>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useSwipe,
|
||||
} from '@livekit/components-react'
|
||||
import { mergeProps } from '@/utils/mergeProps'
|
||||
import { PaginationIndicator } from './PaginationIndicator'
|
||||
import { useGridLayout } from '../hooks/useGridLayout'
|
||||
import { PaginationControl } from './PaginationControl'
|
||||
import { useSpeakerPromotionTrigger } from '../hooks/useSpeakerPromotionTrigger'
|
||||
@@ -61,7 +62,13 @@ export function GridLayout({ tracks, ...props }: GridLayoutProps) {
|
||||
>
|
||||
<TrackLoop tracks={pagination.tracks}>{props.children}</TrackLoop>
|
||||
{tracks.length > layout.maxTiles && (
|
||||
<PaginationControl {...pagination} focusShortcut />
|
||||
<>
|
||||
<PaginationIndicator
|
||||
totalPageCount={pagination.totalPageCount}
|
||||
currentPage={pagination.currentPage}
|
||||
/>
|
||||
<PaginationControl pagesContainer={gridEl} {...pagination} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,147 +1,99 @@
|
||||
import * as React from 'react'
|
||||
import { createInteractingObservable } from '@livekit/components-core'
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { Button } from '@/primitives'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { css, cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { css } from '@/styled-system/css'
|
||||
|
||||
const paginationToolbar = cva({
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.125rem',
|
||||
backgroundColor: 'primaryDark.50',
|
||||
borderRadius: '2rem',
|
||||
border: '1px solid',
|
||||
borderColor: 'primaryDark.200',
|
||||
padding: '0.375rem',
|
||||
},
|
||||
variants: {
|
||||
placement: {
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
bottom: '1rem',
|
||||
left: '50%',
|
||||
zIndex: 2,
|
||||
transform: 'translateX(-50%)',
|
||||
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.45)',
|
||||
},
|
||||
inline: {
|
||||
alignSelf: 'center',
|
||||
flexShrink: 0,
|
||||
marginTop: '1rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
placement: 'overlay',
|
||||
},
|
||||
})
|
||||
|
||||
export type PaginationControlProps = RecipeVariantProps<
|
||||
typeof paginationToolbar
|
||||
> & {
|
||||
export interface PaginationControlProps {
|
||||
totalPageCount: number
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
currentPage: number
|
||||
// The shortcut listens on the main window only, so a single instance may own it.
|
||||
focusShortcut?: boolean
|
||||
pagesContainer?: React.RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
const arrowButtonClass = css({
|
||||
_disabled: {
|
||||
cursor: 'default',
|
||||
backgroundColor: 'transparent !important',
|
||||
'& svg': {
|
||||
opacity: 0.35,
|
||||
},
|
||||
_focusVisible: {
|
||||
outline: '2px solid',
|
||||
outlineColor: 'focusRing',
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function PaginationControl({
|
||||
totalPageCount,
|
||||
nextPage,
|
||||
prevPage,
|
||||
currentPage,
|
||||
placement,
|
||||
focusShortcut = false,
|
||||
pagesContainer: connectedElement,
|
||||
}: PaginationControlProps) {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
|
||||
const prevButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const nextButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const isSinglePage = totalPageCount <= 1
|
||||
const isFirstPage = currentPage <= 1
|
||||
const isLastPage = currentPage >= totalPageCount
|
||||
const [interactive, setInteractive] = useState(false)
|
||||
|
||||
const focusPagination = useCallback(() => {
|
||||
const target = isLastPage ? prevButtonRef.current : nextButtonRef.current
|
||||
target?.focus()
|
||||
}, [isLastPage])
|
||||
useEffect(() => {
|
||||
let subscription:
|
||||
| ReturnType<ReturnType<typeof createInteractingObservable>['subscribe']>
|
||||
| undefined
|
||||
if (connectedElement) {
|
||||
subscription = createInteractingObservable(
|
||||
connectedElement.current,
|
||||
2000
|
||||
).subscribe(setInteractive)
|
||||
}
|
||||
return () => {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
}
|
||||
}, [connectedElement])
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: focusShortcut ? 'focus-pagination' : undefined,
|
||||
handler: focusPagination,
|
||||
isDisabled: isSinglePage,
|
||||
unregisterOnUnmount: true,
|
||||
})
|
||||
|
||||
if (isSinglePage) return null
|
||||
|
||||
const pageCount = t('count', { currentPage, totalPageCount })
|
||||
if (totalPageCount <= 1) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-label={`${t('label')}, ${pageCount}`}
|
||||
className={paginationToolbar({ placement })}
|
||||
<nav
|
||||
aria-label={t('label')}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
bottom: '1rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
alignItems: 'stretch',
|
||||
backgroundColor: 'var(--lk-control-bg)',
|
||||
borderRadius: 'var(--lk-border-radius)',
|
||||
transition: 'opacity ease-in-out .15s',
|
||||
display: 'none',
|
||||
border: '1px solid',
|
||||
borderColor: 'primaryDark.100',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
style={{
|
||||
display: interactive ? 'flex' : 'none',
|
||||
}}
|
||||
data-lk-user-interaction={interactive}
|
||||
>
|
||||
<Button
|
||||
ref={prevButtonRef}
|
||||
aria-disabled={isFirstPage}
|
||||
onPress={isFirstPage ? undefined : prevPage}
|
||||
size="sm"
|
||||
square
|
||||
variant="primaryTextDark"
|
||||
className={arrowButtonClass}
|
||||
isDisabled={currentPage == 1}
|
||||
onPress={prevPage}
|
||||
size="xs"
|
||||
variant="quaternaryText"
|
||||
aria-label={t('previous')}
|
||||
tooltip={t('previous')}
|
||||
>
|
||||
<RiArrowLeftSLine size={20} />
|
||||
<RiArrowLeftSLine />
|
||||
</Button>
|
||||
<span
|
||||
role="status"
|
||||
className={css({
|
||||
color: 'white',
|
||||
fontSize: '0.8125rem',
|
||||
fontWeight: 500,
|
||||
minWidth: '3.5rem',
|
||||
textAlign: 'center',
|
||||
userSelect: 'none',
|
||||
padding: '0 0.35rem',
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '0.25rem 0.5rem',
|
||||
})}
|
||||
>
|
||||
{pageCount}
|
||||
{t('count', {
|
||||
currentPage,
|
||||
totalPageCount,
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
ref={nextButtonRef}
|
||||
aria-disabled={isLastPage}
|
||||
onPress={isLastPage ? undefined : nextPage}
|
||||
size="sm"
|
||||
square
|
||||
variant="primaryTextDark"
|
||||
className={arrowButtonClass}
|
||||
isDisabled={currentPage == totalPageCount}
|
||||
onPress={nextPage}
|
||||
size="xs"
|
||||
variant="quaternaryText"
|
||||
aria-label={t('next')}
|
||||
tooltip={t('next')}
|
||||
>
|
||||
<RiArrowRightSLine size={20} />
|
||||
<RiArrowRightSLine />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react'
|
||||
|
||||
export interface PaginationIndicatorProps {
|
||||
totalPageCount: number
|
||||
currentPage: number
|
||||
}
|
||||
|
||||
export const PaginationIndicator: (
|
||||
props: PaginationIndicatorProps & React.RefAttributes<HTMLDivElement>
|
||||
) => React.ReactNode = /* @__PURE__ */ React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PaginationIndicatorProps
|
||||
>(function PaginationIndicator(
|
||||
{ totalPageCount, currentPage }: PaginationIndicatorProps,
|
||||
ref
|
||||
) {
|
||||
const bubbles = new Array(totalPageCount).fill('').map((_, index) => {
|
||||
if (index + 1 === currentPage) {
|
||||
return <span data-lk-active key={index} />
|
||||
} else {
|
||||
return <span key={index} />
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={ref} className="lk-pagination-indicator" aria-hidden="true">
|
||||
{bubbles}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,96 @@
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
interface PipPaginationProps {
|
||||
totalPageCount: number
|
||||
currentPage: number
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
}
|
||||
|
||||
export const PipPagination = ({
|
||||
totalPageCount,
|
||||
currentPage,
|
||||
nextPage,
|
||||
prevPage,
|
||||
}: PipPaginationProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
|
||||
|
||||
if (totalPageCount <= 1) return null
|
||||
|
||||
return (
|
||||
<Nav aria-label={t('label')}>
|
||||
<ArrowButton
|
||||
type="button"
|
||||
onClick={prevPage}
|
||||
disabled={currentPage === 1}
|
||||
aria-label={t('previous')}
|
||||
>
|
||||
<RiArrowLeftSLine size={18} />
|
||||
</ArrowButton>
|
||||
<Counter role="status">
|
||||
{t('count', { currentPage, totalPageCount })}
|
||||
</Counter>
|
||||
<ArrowButton
|
||||
type="button"
|
||||
onClick={nextPage}
|
||||
disabled={currentPage === totalPageCount}
|
||||
aria-label={t('next')}
|
||||
>
|
||||
<RiArrowRightSLine size={18} />
|
||||
</ArrowButton>
|
||||
</Nav>
|
||||
)
|
||||
}
|
||||
|
||||
const Nav = styled('nav', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.25rem',
|
||||
marginTop: '1rem',
|
||||
flexShrink: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const ArrowButton = styled('button', {
|
||||
base: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '1.75rem',
|
||||
height: '1.75rem',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'white',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
transition: 'opacity 0.15s, background-color 0.15s',
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: 'primaryDark.75',
|
||||
},
|
||||
'&:focus-visible': {
|
||||
outline: '2px solid',
|
||||
outlineColor: 'white',
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.3,
|
||||
cursor: 'default',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Counter = styled('span', {
|
||||
base: {
|
||||
fontSize: '0.75rem',
|
||||
color: 'white',
|
||||
opacity: 0.8,
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '0 0.25rem',
|
||||
minWidth: '3rem',
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { RoomEvent, Track } from 'livekit-client'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { PipFocusLayout } from './PipFocusLayout'
|
||||
import { PipGridLayout } from './PipGridLayout'
|
||||
import { PaginationControl } from '@/features/layout/components/PaginationControl'
|
||||
import { PipPagination } from './PipPagination'
|
||||
import { PipScreenShareLayout } from './PipScreenShareLayout'
|
||||
import { StageFrame } from './StageFrame'
|
||||
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
||||
@@ -120,7 +120,12 @@ const PaginatedStage = ({
|
||||
}) => (
|
||||
<StageWrapper>
|
||||
<StageFrame>{children}</StageFrame>
|
||||
<PaginationControl {...pagination} placement="inline" />
|
||||
<PipPagination
|
||||
totalPageCount={pagination.totalPageCount}
|
||||
currentPage={pagination.currentPage}
|
||||
nextPage={pagination.nextPage}
|
||||
prevPage={pagination.prevPage}
|
||||
/>
|
||||
</StageWrapper>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useIsSpeaking, useRoomContext } from '@livekit/components-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||
import {
|
||||
closeLowerHandToasts,
|
||||
showLowerHandToast,
|
||||
} from '@/features/notifications/utils'
|
||||
|
||||
const SPEAKING_DETECTION_DELAY = 3000
|
||||
|
||||
/**
|
||||
* Offers to lower the local participant's raised hand after
|
||||
* SPEAKING_DETECTION_DELAY of speaking. Mount it once: each copy runs its own
|
||||
* timer and shows its own toast.
|
||||
*/
|
||||
export const LowerHandOnSpeaking = () => {
|
||||
const room = useRoomContext()
|
||||
const { isHandRaised, lowerHand } = useRaisedHand({
|
||||
participant: room.localParticipant,
|
||||
})
|
||||
const isSpeaking = useIsSpeaking(room.localParticipant)
|
||||
const [hasOffered, setHasOffered] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isHandRaised) return
|
||||
setHasOffered(false)
|
||||
closeLowerHandToasts()
|
||||
}, [isHandRaised])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSpeaking || !isHandRaised || hasOffered) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setHasOffered(true)
|
||||
showLowerHandToast(room.localParticipant, lowerHand)
|
||||
}, SPEAKING_DETECTION_DELAY)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSpeaking, isHandRaised, hasOffered])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -2,12 +2,19 @@ import { useTranslation } from 'react-i18next'
|
||||
import { RiHand } from '@remixicon/react'
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { useIsSpeaking, useRoomContext } from '@livekit/components-react'
|
||||
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
closeLowerHandToasts,
|
||||
showLowerHandToast,
|
||||
} from '@/features/notifications/utils'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { type ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
const SPEAKING_DETECTION_DELAY = 3000
|
||||
|
||||
type Props = Pick<NonNullable<ButtonRecipeProps>, 'variant'> & ToggleButtonProps
|
||||
|
||||
export const HandToggle = ({
|
||||
@@ -18,15 +25,64 @@ export const HandToggle = ({
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'controls.hand' })
|
||||
|
||||
const room = useRoomContext()
|
||||
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||
const { isHandRaised, toggleRaisedHand, lowerHand } = useRaisedHand({
|
||||
participant: room.localParticipant,
|
||||
})
|
||||
|
||||
const isSpeaking = useIsSpeaking(room.localParticipant)
|
||||
const speakingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [hasShownToast, setHasShownToast] = useState(false)
|
||||
|
||||
const resetToastState = () => {
|
||||
setHasShownToast(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isHandRaised) return
|
||||
closeLowerHandToasts()
|
||||
}, [isHandRaised])
|
||||
|
||||
const handleToggle = () => {
|
||||
toggleRaisedHand()
|
||||
resetToastState()
|
||||
}
|
||||
|
||||
useRegisterKeyboardShortcut({
|
||||
id: 'raise-hand',
|
||||
handler: toggleRaisedHand,
|
||||
handler: handleToggle,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const shouldShowToast = isSpeaking && isHandRaised && !hasShownToast
|
||||
|
||||
if (shouldShowToast && !speakingTimerRef.current) {
|
||||
speakingTimerRef.current = setTimeout(() => {
|
||||
speakingTimerRef.current = null
|
||||
setHasShownToast(true)
|
||||
const onClose = () => {
|
||||
lowerHand()
|
||||
resetToastState()
|
||||
}
|
||||
showLowerHandToast(room.localParticipant, onClose)
|
||||
}, SPEAKING_DETECTION_DELAY)
|
||||
}
|
||||
if ((!isSpeaking || !isHandRaised) && speakingTimerRef.current) {
|
||||
clearTimeout(speakingTimerRef.current)
|
||||
speakingTimerRef.current = null
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSpeaking, isHandRaised, hasShownToast, lowerHand])
|
||||
|
||||
// Clear any pending timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (speakingTimerRef.current) {
|
||||
clearTimeout(speakingTimerRef.current)
|
||||
speakingTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const tooltipLabel = isHandRaised ? 'lower' : 'raise'
|
||||
|
||||
return (
|
||||
@@ -44,7 +100,7 @@ export const HandToggle = ({
|
||||
tooltip={t(tooltipLabel)}
|
||||
isSelected={isHandRaised}
|
||||
onPress={(e) => {
|
||||
toggleRaisedHand()
|
||||
handleToggle()
|
||||
onPress?.(e)
|
||||
}}
|
||||
data-attr={`controls-hand-${tooltipLabel}`}
|
||||
|
||||
@@ -31,20 +31,35 @@ export const useSidePanel = () => {
|
||||
const isSidePanelOpen = !!activePanelId
|
||||
const isSubPanelOpen = !!activeSubPanelId
|
||||
|
||||
// Reads the live store, not the render snapshot: shortcut handlers outlive
|
||||
// toggles that unmount (e.g. inside a closed overflow menu).
|
||||
const togglePanel = (panelId: PanelId) => {
|
||||
layoutStore.activePanelId =
|
||||
layoutStore.activePanelId === panelId ? null : panelId
|
||||
if (layoutStore.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
const toggleAdmin = () => {
|
||||
layoutStore.activePanelId = isAdminOpen ? null : PanelId.ADMIN
|
||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
}
|
||||
|
||||
const toggleAdmin = () => togglePanel(PanelId.ADMIN)
|
||||
const toggleParticipants = () => togglePanel(PanelId.PARTICIPANTS)
|
||||
const toggleChat = () => togglePanel(PanelId.CHAT)
|
||||
const toggleEffects = () => togglePanel(PanelId.EFFECTS)
|
||||
const toggleTools = () => togglePanel(PanelId.TOOLS)
|
||||
const toggleInfo = () => togglePanel(PanelId.INFO)
|
||||
const toggleParticipants = () => {
|
||||
layoutStore.activePanelId = isParticipantsOpen ? null : PanelId.PARTICIPANTS
|
||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
}
|
||||
|
||||
const toggleChat = () => {
|
||||
layoutStore.activePanelId = isChatOpen ? null : PanelId.CHAT
|
||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
}
|
||||
|
||||
const toggleEffects = () => {
|
||||
layoutStore.activePanelId = isEffectsOpen ? null : PanelId.EFFECTS
|
||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
}
|
||||
|
||||
const toggleTools = () => {
|
||||
layoutStore.activePanelId = isToolsOpen ? null : PanelId.TOOLS
|
||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
}
|
||||
|
||||
const toggleInfo = () => {
|
||||
layoutStore.activePanelId = isInfoOpen ? null : PanelId.INFO
|
||||
if (layoutSnap.activeSubPanelId) layoutStore.activeSubPanelId = null
|
||||
}
|
||||
|
||||
const openTranscript = () => {
|
||||
layoutStore.activeSubPanelId = SubPanelId.TRANSCRIPT
|
||||
|
||||
@@ -31,7 +31,6 @@ import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer'
|
||||
import { ChatProvider } from '@/features/chat/components/ChatProvider'
|
||||
import { SyncDevicePreferences } from '@/features/rooms/livekit/components/SyncDevicePreferences'
|
||||
import { RoomSilentMicDetector } from '@/features/rooms/components/SilentMicDetector'
|
||||
import { LowerHandOnSpeaking } from '@/features/rooms/livekit/components/LowerHandOnSpeaking'
|
||||
import { LobbyProvider } from '@/features/rooms/components/LobbyProvider'
|
||||
|
||||
/**
|
||||
@@ -120,7 +119,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
<ConnectionObserver />
|
||||
<SyncDevicePreferences />
|
||||
<RoomSilentMicDetector />
|
||||
<LowerHandOnSpeaking />
|
||||
<MediaStateObserver />
|
||||
<ChatProvider />
|
||||
<LobbyProvider />
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeRoomId } from './isRoomValid'
|
||||
|
||||
describe('normalizeRoomId', () => {
|
||||
it('lowercases and re-inserts the hyphens of a ten-letter id', () => {
|
||||
expect(normalizeRoomId('ABCDEFGHIJ')).toBe('abc-defg-hij')
|
||||
expect(normalizeRoomId('abc-defghij')).toBe('abc-defg-hij')
|
||||
})
|
||||
|
||||
it('returns any other input unchanged', () => {
|
||||
expect(normalizeRoomId('abc-def')).toBe('abc-def')
|
||||
expect(normalizeRoomId('Not-A-Room')).toBe('Not-A-Room')
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,6 @@ export type ShortcutCategory = 'navigation' | 'media' | 'interaction'
|
||||
export type ShortcutId =
|
||||
| 'open-shortcuts'
|
||||
| 'focus-toolbar'
|
||||
| 'focus-pagination'
|
||||
| 'toggle-microphone'
|
||||
| 'toggle-camera'
|
||||
| 'push-to-talk'
|
||||
@@ -45,11 +44,6 @@ export const shortcutCatalog: ShortcutDescriptor[] = [
|
||||
category: 'navigation',
|
||||
shortcut: { key: 'F2' },
|
||||
},
|
||||
{
|
||||
id: 'focus-pagination',
|
||||
category: 'navigation',
|
||||
shortcut: { key: 'G', ctrlKey: true, shiftKey: true },
|
||||
},
|
||||
{
|
||||
id: 'toggle-microphone',
|
||||
category: 'media',
|
||||
|
||||
@@ -7,16 +7,12 @@ export type useRegisterKeyboardShortcutProps = {
|
||||
id?: ShortcutId
|
||||
handler: () => Promise<void | boolean | undefined> | void
|
||||
isDisabled?: boolean
|
||||
// Opt-in: controls rendered inside menus unmount when the menu closes, and
|
||||
// their shortcuts must keep working.
|
||||
unregisterOnUnmount?: boolean
|
||||
}
|
||||
|
||||
export const useRegisterKeyboardShortcut = ({
|
||||
id,
|
||||
handler,
|
||||
isDisabled = false,
|
||||
unregisterOnUnmount = false,
|
||||
}: useRegisterKeyboardShortcutProps) => {
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
@@ -25,14 +21,8 @@ export const useRegisterKeyboardShortcut = ({
|
||||
const formattedKey = formatShortcutKey(descriptor.shortcut)
|
||||
if (isDisabled) {
|
||||
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
||||
return
|
||||
} else {
|
||||
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
||||
}
|
||||
keyboardShortcutsStore.shortcuts.set(formattedKey, handler)
|
||||
if (!unregisterOnUnmount) return
|
||||
return () => {
|
||||
if (keyboardShortcutsStore.shortcuts.get(formattedKey) === handler) {
|
||||
keyboardShortcutsStore.shortcuts.delete(formattedKey)
|
||||
}
|
||||
}
|
||||
}, [handler, id, isDisabled, unregisterOnUnmount])
|
||||
}, [handler, id, isDisabled])
|
||||
}
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"actions": {
|
||||
"open-shortcuts": "Tastenkürzel-Hilfe öffnen",
|
||||
"focus-toolbar": "Fokus auf die untere Symbolleiste",
|
||||
"focus-pagination": "Fokus auf die Teilnehmerseiten",
|
||||
"toggle-microphone": "Mikrofon umschalten",
|
||||
"toggle-camera": "Kamera umschalten",
|
||||
"push-to-talk": "Push-to-talk",
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"actions": {
|
||||
"open-shortcuts": "Open shortcuts help",
|
||||
"focus-toolbar": "Focus bottom toolbar",
|
||||
"focus-pagination": "Focus participant pagination",
|
||||
"toggle-microphone": "Toggle microphone",
|
||||
"toggle-camera": "Toggle camera",
|
||||
"push-to-talk": "Push-to-talk (hold to unmute)",
|
||||
|
||||
@@ -783,7 +783,6 @@
|
||||
"actions": {
|
||||
"open-shortcuts": "Abrir la ayuda de atajos",
|
||||
"focus-toolbar": "Poner el foco en la barra de herramientas inferior",
|
||||
"focus-pagination": "Poner el foco en la paginación de participantes",
|
||||
"toggle-microphone": "Activar o desactivar el micrófono",
|
||||
"toggle-camera": "Activar o desactivar la cámara",
|
||||
"push-to-talk": "Pulsar para hablar (mantener para reactivar)",
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"actions": {
|
||||
"open-shortcuts": "Ouvrir l’aide des raccourcis",
|
||||
"focus-toolbar": "Mettre le focus sur la barre d’outils du bas",
|
||||
"focus-pagination": "Mettre le focus sur la pagination des participants",
|
||||
"toggle-microphone": "Activer ou désactiver le micro",
|
||||
"toggle-camera": "Activer ou désactiver la caméra",
|
||||
"push-to-talk": "Appuyer pour parler (maintenir pour réactiver)",
|
||||
|
||||
@@ -784,7 +784,6 @@
|
||||
"actions": {
|
||||
"open-shortcuts": "Sneltoetsenhulp openen",
|
||||
"focus-toolbar": "Focus op de onderste werkbalk",
|
||||
"focus-pagination": "Focus op de paginering van deelnemers",
|
||||
"toggle-microphone": "Microfoon aan/uit",
|
||||
"toggle-camera": "Camera aan/uit",
|
||||
"push-to-talk": "Push-to-talk (ingedrukt houden om te activeren)",
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["vite.config.ts", "vitest.config.ts"]
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
// Kept apart from vite.config.ts so the tests load none of the build plugins.
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
},
|
||||
})
|
||||
@@ -24,7 +24,7 @@ _summaryEnvVars: &summaryEnvVars
|
||||
APP_NAME: summary-microservice
|
||||
APP_API_TOKEN: password
|
||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
||||
AWS_S3_ENDPOINT_URL: http://dev-backend-garage.meet.svc.cluster.local:9000/
|
||||
AWS_S3_ENDPOINT_URL: http://garage.meet.svc.cluster.local:9000/
|
||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||
AWS_S3_SECURE_ACCESS: False
|
||||
@@ -69,9 +69,9 @@ _summaryEnvVars: &summaryEnvVars
|
||||
LLM_MODEL: Qwen/Qwen3-Coder-30B-A3B-Instruct
|
||||
WEBHOOK_API_TOKEN: password
|
||||
WEBHOOK_URL: https://www.mock-impress.com/webhook/
|
||||
CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://user:pass@dev-backend-redis:6379/1
|
||||
TASK_TRACKER_REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
|
||||
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED: true
|
||||
RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP: 0.5
|
||||
RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS: true
|
||||
@@ -127,21 +127,12 @@ backend:
|
||||
LOGIN_REDIRECT_URL_FAILURE: https://meet.127.0.0.1.nip.io
|
||||
LOGOUT_REDIRECT_URL: https://meet.127.0.0.1.nip.io
|
||||
# Databases
|
||||
DB_HOST: dev-backend-postgres
|
||||
DB_NAME:
|
||||
secretKeyRef:
|
||||
name: dev-backend-postgres
|
||||
key: database
|
||||
DB_USER:
|
||||
secretKeyRef:
|
||||
name: dev-backend-postgres
|
||||
key: username
|
||||
DB_PASSWORD:
|
||||
secretKeyRef:
|
||||
name: dev-backend-postgres
|
||||
key: password
|
||||
DB_HOST: postgres
|
||||
DB_NAME: meet
|
||||
DB_USER: dinum
|
||||
DB_PASSWORD: pass
|
||||
DB_PORT: 5432
|
||||
REDIS_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||
REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
# Static files
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
# Permissions
|
||||
@@ -172,7 +163,7 @@ backend:
|
||||
FRONTEND_TRANSCRIPTION_DESTINATION: "https://docs.numerique.gouv.fr"
|
||||
FRONTEND_IS_SILENT_LOGIN_ENABLED: False
|
||||
# S3 Storage
|
||||
AWS_S3_ENDPOINT_URL: http://dev-backend-garage.meet.svc.cluster.local:9000
|
||||
AWS_S3_ENDPOINT_URL: http://garage.meet.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||
AWS_STORAGE_BUCKET_NAME: meet-media-storage
|
||||
@@ -195,7 +186,7 @@ backend:
|
||||
MEDIA_BASE_URL: https://meet.127.0.0.1.nip.io
|
||||
FILE_UPLOAD_ENABLED: True
|
||||
CELERY_ENABLED: True
|
||||
CELERY_BROKER_URL: redis://user:pass@dev-backend-redis:6379/1
|
||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
||||
# Recording & Transcription
|
||||
RECORDING_ENABLE: True
|
||||
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v2/async-jobs/transcribe/
|
||||
@@ -275,11 +266,11 @@ ingressMedia:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/recordings/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-garage.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: garage.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/$1
|
||||
|
||||
serviceMedia:
|
||||
host: dev-backend-garage.meet.svc.cluster.local
|
||||
host: garage.meet.svc.cluster.local
|
||||
port: 9000
|
||||
|
||||
# ---- Extra ingress/service for background file uploads ------------
|
||||
@@ -291,11 +282,11 @@ ingressMediaFiles:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-url: https://meet.127.0.0.1.nip.io/api/v1.0/files/media-auth/
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "Authorization, X-Amz-Date, X-Amz-Content-SHA256"
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: dev-backend-garage.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: garage.meet.svc.cluster.local:9000
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /meet-media-storage/files/$1
|
||||
|
||||
serviceMediaFiles:
|
||||
host: dev-backend-garage.meet.svc.cluster.local
|
||||
host: garage.meet.svc.cluster.local
|
||||
port: 9000
|
||||
|
||||
# ---- STT Orchestration Microservice Components --------------------
|
||||
@@ -373,7 +364,7 @@ agentMetadata:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ENABLE_SILERO_VAD: "false"
|
||||
AWS_S3_ENDPOINT_URL: dev-backend-garage.meet.svc.cluster.local:9000
|
||||
AWS_S3_ENDPOINT_URL: garage.meet.svc.cluster.local:9000
|
||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||
AWS_S3_SECURE_ACCESS: False
|
||||
|
||||
@@ -13,15 +13,14 @@ egress:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
redis:
|
||||
address: dev-backend-redis:6379
|
||||
username: user
|
||||
address: redis-master:6379
|
||||
password: pass
|
||||
s3:
|
||||
access_key: meet-access-key
|
||||
secret: meet-secret-access-key
|
||||
region: local
|
||||
bucket: meet-media-storage
|
||||
endpoint: http://dev-backend-garage:9000
|
||||
endpoint: http://garage:9000
|
||||
force_path_style: true
|
||||
|
||||
loadBalancer:
|
||||
|
||||
@@ -14,8 +14,7 @@ livekit:
|
||||
port_range_end: 60000
|
||||
tcp_port: 7881
|
||||
redis:
|
||||
address: dev-backend-redis:6379
|
||||
username: user
|
||||
address: redis-master:6379
|
||||
password: pass
|
||||
keys:
|
||||
turn:
|
||||
|
||||
@@ -16,15 +16,14 @@ egress:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
redis:
|
||||
address: dev-backend-redis:6379
|
||||
username: user
|
||||
address: redis-master:6379
|
||||
password: pass
|
||||
s3:
|
||||
access_key: meet-access-key
|
||||
secret: meet-secret-access-key
|
||||
region: local
|
||||
bucket: meet-media-storage
|
||||
endpoint: http://dev-backend-garage:9000
|
||||
endpoint: http://garage:9000
|
||||
force_path_style: true
|
||||
|
||||
loadBalancer:
|
||||
|
||||
@@ -14,8 +14,7 @@ livekit:
|
||||
port_range_end: 60000
|
||||
tcp_port: 7881
|
||||
redis:
|
||||
address: dev-backend-redis:6379
|
||||
username: user
|
||||
address: redis-master:6379
|
||||
password: pass
|
||||
keys:
|
||||
turn:
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: garage
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||
spec:
|
||||
rules:
|
||||
- host: "garage.127.0.0.1.nip.io"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: garage
|
||||
port:
|
||||
number: 9000
|
||||
tls:
|
||||
- hosts:
|
||||
- garage.127.0.0.1.nip.io
|
||||
secretName: meet-tls
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: garage
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
ports:
|
||||
- name: client
|
||||
port: 9000
|
||||
protocol: TCP
|
||||
targetPort: 9000
|
||||
selector:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: garage
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: garage-config
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
data:
|
||||
garage.toml: |
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
db_engine = "lmdb"
|
||||
|
||||
replication_factor = 1
|
||||
|
||||
rpc_bind_addr = "127.0.0.1:3901"
|
||||
rpc_public_addr = "127.0.0.1:3901"
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "[::]:9000"
|
||||
# Clients must sign their requests for this region (AWS_S3_REGION_NAME)
|
||||
s3_region = "local"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: garage-dev
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
type: Opaque
|
||||
data:
|
||||
GARAGE_RPC_SECRET: {{ printf "%s/garage-rpc-secret" .Release.Namespace | sha256sum | b64enc }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: garage
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: garage
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: garage
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: garage
|
||||
spec:
|
||||
containers:
|
||||
- name: garage
|
||||
command:
|
||||
- /garage
|
||||
- server
|
||||
- --single-node
|
||||
- --default-bucket
|
||||
env:
|
||||
- name: GARAGE_RPC_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: garage-dev
|
||||
key: GARAGE_RPC_SECRET
|
||||
- name: GARAGE_DEFAULT_ACCESS_KEY
|
||||
value: meet-access-key
|
||||
- name: GARAGE_DEFAULT_SECRET_KEY
|
||||
value: meet-secret-access-key
|
||||
- name: GARAGE_DEFAULT_BUCKET
|
||||
value: meet-media-storage
|
||||
image: "dxflrs/garage:v2.4.1"
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 9000
|
||||
name: client
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /garage
|
||||
- health
|
||||
volumeMounts:
|
||||
- mountPath: /etc/garage.toml
|
||||
name: config
|
||||
subPath: garage.toml
|
||||
- mountPath: /var/lib/garage
|
||||
name: data
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: garage-config
|
||||
- name: data
|
||||
emptyDir:
|
||||
---
|
||||
# Garage denies cross-origin requests by default: allow the frontend to upload
|
||||
# files straight to the bucket
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: garage-cors
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: aws-cli
|
||||
image: amazon/aws-cli:2.37.1
|
||||
env:
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
value: meet-access-key
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
value: meet-secret-access-key
|
||||
- name: AWS_DEFAULT_REGION
|
||||
value: local
|
||||
- name: AWS_ENDPOINT_URL
|
||||
value: http://garage:9000
|
||||
- name: BUCKET
|
||||
value: meet-media-storage
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
deadline=$(($(date +%s) + 300))
|
||||
until aws s3api head-bucket --bucket="$BUCKET" --cli-connect-timeout=5; do
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
echo "Bucket $BUCKET still unavailable on $AWS_ENDPOINT_URL" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for bucket $BUCKET on $AWS_ENDPOINT_URL"
|
||||
sleep 5
|
||||
done
|
||||
exec aws s3api put-bucket-cors --bucket="$BUCKET" \
|
||||
--cors-configuration='{"CORSRules": [{"AllowedOrigins": ["https://meet.127.0.0.1.nip.io"], "AllowedMethods": ["GET", "HEAD", "PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"]}]}'
|
||||
restartPolicy: Never
|
||||
backoffLimit: 3
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: kc-postgres
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
ports:
|
||||
- name: tcp-postgresql
|
||||
port: 5432
|
||||
protocol: TCP
|
||||
targetPort: tcp-postgresql
|
||||
selector:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: kc-postgresql
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: kc-postgresql
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: kc-postgresql
|
||||
serviceName: "kc-postgres"
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: kc-postgresql
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: pg
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp-postgresql
|
||||
env:
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: pass
|
||||
- name: POSTGRES_USER
|
||||
value: dinum
|
||||
- name: POSTGRES_DB
|
||||
value: keycloak
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: keycloak
|
||||
spec:
|
||||
rules:
|
||||
- host: "keycloak.127.0.0.1.nip.io"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: keycloak
|
||||
port:
|
||||
number: 8080
|
||||
tls:
|
||||
- hosts:
|
||||
- keycloak.127.0.0.1.nip.io
|
||||
secretName: meet-tls
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
ports:
|
||||
- name: tcp-keycloak
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: tcp-keycloak
|
||||
selector:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: keycloak
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: realm
|
||||
data:
|
||||
meet.json: |
|
||||
{{ .Values.realm | indent 4 }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: keycloak
|
||||
serviceName: "keycloak"
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: keycloak
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: keycloak
|
||||
image: quay.io/keycloak/keycloak:20.0.1
|
||||
args:
|
||||
- start-dev
|
||||
- --features=preview
|
||||
- --import-realm
|
||||
- --proxy=edge
|
||||
- --hostname=keycloak.127.0.0.1.nip.io
|
||||
- --hostname-strict=false
|
||||
- --hostname-strict-https=false
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: tcp-keycloak
|
||||
env:
|
||||
- name: KEYCLOAK_ADMIN
|
||||
value: admin
|
||||
- name: KEYCLOAK_ADMIN_PASSWORD
|
||||
value: admin
|
||||
- name: PROXY_ADDRESS_FORWARDING
|
||||
value: 'true'
|
||||
- name: KC_DB_URL_HOST
|
||||
value: kc_postgresql
|
||||
- name: KC_DB_URL_DATABASE
|
||||
value: keycloak
|
||||
- name: KC_DB_PASSWORD
|
||||
value: pass
|
||||
- name: KC_DB_USERNAME
|
||||
value: dinum
|
||||
- name: KC_DB_SCHEMA
|
||||
value: public
|
||||
volumeMounts:
|
||||
- name: realm
|
||||
mountPath: "/opt/keycloak/data/import"
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: realm
|
||||
configMap:
|
||||
name: realm
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
ports:
|
||||
- name: tcp-postgresql
|
||||
port: 5432
|
||||
protocol: TCP
|
||||
targetPort: tcp-postgresql
|
||||
selector:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: postgresql
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgresql
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: postgresql
|
||||
serviceName: "postgres"
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: postgresql
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 10
|
||||
containers:
|
||||
- name: pg
|
||||
image: postgres:16-alpine
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: [ "pg_isready", "-U", "dinum", "-d", "meet", "-h", "127.0.0.1" ]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: [ "pg_isready", "-U", "dinum", "-d", "meet", "-h", "127.0.0.1" ]
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp-postgresql
|
||||
env:
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: pass
|
||||
- name: POSTGRES_USER
|
||||
value: dinum
|
||||
- name: POSTGRES_DB
|
||||
value: meet
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis-master
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
spec:
|
||||
ports:
|
||||
- name: tcp-redis
|
||||
port: 6379
|
||||
protocol: TCP
|
||||
targetPort: tcp-redis
|
||||
selector:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: redis
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: redis
|
||||
data:
|
||||
redis.conf: |
|
||||
bind 0.0.0.0
|
||||
port 6379
|
||||
user default on >pass ~* &* +@all
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: redis
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: redis
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: extra
|
||||
app.kubernetes.io/name: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
args:
|
||||
- redis-server
|
||||
- /usr/local/etc/redis/redis.conf
|
||||
image: "redis:8.2-alpine"
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: tcp-redis
|
||||
volumeMounts:
|
||||
- name: redis
|
||||
mountPath: "/usr/local/etc/redis"
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: redis
|
||||
configMap:
|
||||
name: redis
|
||||
@@ -13,9 +13,6 @@ repositories:
|
||||
- name: livekit
|
||||
url: https://helm.livekit.io
|
||||
|
||||
- name: dev-backends
|
||||
url: https://suitenumerique.github.io/helm-dev-backend
|
||||
|
||||
releases:
|
||||
- name: extra
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||
@@ -30,59 +27,8 @@ releases:
|
||||
- enablePermanentRedirect: {{ .Values | get "enablePermanentRedirect" "False"}}
|
||||
- oldDomain: {{ .Values | get "oldDomain" "demo.com" }}
|
||||
- newDomain: {{ .Values | get "newDomain" "demo.com" }}
|
||||
|
||||
- name: dev-backend
|
||||
installed: {{ regexMatch "^dev.*" .Environment.Name }}
|
||||
namespace: {{ .Namespace }}
|
||||
chart: dev-backends/dev-backend
|
||||
version: 0.0.14
|
||||
values:
|
||||
- postgres:
|
||||
enabled: true
|
||||
username: dinum
|
||||
password: pass
|
||||
database: meet
|
||||
size: 1Gi
|
||||
- redis:
|
||||
enabled: true
|
||||
username: user
|
||||
password: pass
|
||||
- garage:
|
||||
enabled: true
|
||||
accessKey: meet-access-key
|
||||
secretKey: meet-secret-access-key
|
||||
bucket: meet-media-storage
|
||||
region: local
|
||||
persistence: false
|
||||
ingress:
|
||||
enabled: true
|
||||
hostname: garage.127.0.0.1.nip.io
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 10m
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: meet-tls
|
||||
cors:
|
||||
allowedOrigins:
|
||||
- https://meet.127.0.0.1.nip.io
|
||||
- keycloak:
|
||||
enabled: true
|
||||
hostname: keycloak.127.0.0.1.nip.io
|
||||
username: admin
|
||||
password: admin
|
||||
tls:
|
||||
enabled: true
|
||||
secretName: meet-tls
|
||||
db:
|
||||
username: dinum
|
||||
password: pass
|
||||
database: keycloak
|
||||
size: 1Gi
|
||||
realm:
|
||||
name: meet
|
||||
username: meet
|
||||
password: meet
|
||||
email: meet@example.com
|
||||
- realm: |
|
||||
{{ readFile "../../docker/auth/realm.json" | replace "http://localhost:3200" "https://meet.127.0.0.1.nip.io" | indent 8 }}
|
||||
|
||||
|
||||
- name: meet
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.agentMetadata.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.agentSubtitles.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -41,10 +41,6 @@ items:
|
||||
imagePullPolicy: {{ ($.Values.backend.image | default dict).pullPolicy | default $.Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml .command | nindent 22 }}
|
||||
{{- with $.Values.backend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 20 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 22 }}
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -49,10 +49,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -49,10 +49,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.backend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $envVars := include "meet.env.transformDict" (mergeOverwrite (default dict .Values.backend.envVars) (default dict .Values.celeryBackend.envVars)) -}}
|
||||
{{- $envFrom := concat (default (list) .Values.backend.envFrom) (default (list) .Values.celeryBackend.envFrom) -}}
|
||||
{{- $fullName := include "meet.celeryBackend.fullname" . -}}
|
||||
{{- $component := "celery-backend" -}}
|
||||
apiVersion: apps/v1
|
||||
@@ -51,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.celerySummarize.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -46,10 +46,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.celerySummaryBackend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $sharedEnvVars := default (dict) .Values.celeryTranscribe.envVars -}}
|
||||
{{- $sharedEnvFrom := default (list) .Values.celeryTranscribe.envFrom -}}
|
||||
{{- $component := "celery-transcribe" -}}
|
||||
|
||||
{{- range $idx, $instance := .Values.celeryTranscribe.instances }}
|
||||
@@ -7,8 +6,6 @@
|
||||
{{- $fullName := printf "%s-%s" (include "meet.celeryTranscribe.fullname" $) $instance.name }}
|
||||
{{- $extraInstanceEnvVars := default (dict) $instance.extraEnvVars -}}
|
||||
{{- $mergedInstanceEnvVars := merge $extraInstanceEnvVars $sharedEnvVars -}}
|
||||
{{- $extraInstanceEnvFrom := default (list) $instance.extraEnvFrom -}}
|
||||
{{- $envFrom := concat $sharedEnvFrom $extraInstanceEnvFrom -}}
|
||||
{{- $fakeInstanceObjectForEnvHelper := dict "envVars" $mergedInstanceEnvVars -}}
|
||||
{{- $envVars := include "meet.common.env" (list . $fakeInstanceObjectForEnvHelper) -}}
|
||||
apiVersion: apps/v1
|
||||
@@ -63,10 +60,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with $envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.frontend.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -50,10 +50,6 @@ spec:
|
||||
args:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.summary.envFrom }}
|
||||
envFrom:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if $envVars }}
|
||||
{{- $envVars | indent 12 }}
|
||||
|
||||
@@ -232,9 +232,6 @@ backend:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param backend.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param backend.podAnnotations Annotations to add to the backend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -436,9 +433,6 @@ frontend:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param frontend.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param frontend.podAnnotations Annotations to add to the frontend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -630,9 +624,6 @@ summary:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param summary.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param summary.podAnnotations Annotations to add to the summary Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -760,9 +751,6 @@ celeryBackend:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param celeryBackend.envFrom Import additional environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param celeryBackend.podAnnotations Annotations to add to the celeryBackend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -875,9 +863,6 @@ celeryTranscribe:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param celeryTranscribe.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param celeryTranscribe.podAnnotations Annotations to add to the celeryTranscribe Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -938,7 +923,6 @@ celeryTranscribe:
|
||||
## @extra celeryTranscribe.instances[].name Unique name suffix for the instance (used in the Deployment name and pod labels)
|
||||
## @extra celeryTranscribe.instances[].replicas Override the number of replicas for this specific instance
|
||||
## @extra celeryTranscribe.instances[].extraEnvVars Additional environment variables for this specific instance (same structure as envVars)
|
||||
## @extra celeryTranscribe.instances[].extraEnvFrom Additional ConfigMap or Secret environment sources for this specific instance
|
||||
## @extra celeryTranscribe.instances[].command Override the container command for this specific instance
|
||||
## @extra celeryTranscribe.instances[].args Override the container args for this specific instance
|
||||
## @extra celeryTranscribe.instances[].resources Override resource requirements for this specific instance
|
||||
@@ -949,7 +933,6 @@ celeryTranscribe:
|
||||
instances:
|
||||
- name: default
|
||||
extraEnvVars: {}
|
||||
extraEnvFrom: []
|
||||
|
||||
## @section celerySummarize
|
||||
|
||||
@@ -1007,9 +990,6 @@ celerySummarize:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param celerySummarize.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param celerySummarize.podAnnotations Annotations to add to the celerySummarize Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -1119,9 +1099,6 @@ celerySummaryBackend:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param celerySummaryBackend.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param celerySummaryBackend.podAnnotations Annotations to add to the celerySummaryBackend Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -1229,9 +1206,6 @@ agentMetadata:
|
||||
envVars:
|
||||
<<: *commonEnvVars
|
||||
|
||||
## @param agentMetadata.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param agentMetadata.podAnnotations Annotations to add to the agentMetadata Pod
|
||||
podAnnotations: {}
|
||||
|
||||
@@ -1323,9 +1297,6 @@ agentSubtitles:
|
||||
# are NOT supported by the LiveKit Deepgram plugin in streaming mode.
|
||||
# Use language="multi" for automatic multilingual support (10 languages).
|
||||
|
||||
## @param agentSubtitles.envFrom Import environment variables from ConfigMaps or Secrets
|
||||
envFrom: []
|
||||
|
||||
## @param agentSubtitles.podAnnotations Annotations to add to the agentSubtitles Pod
|
||||
podAnnotations: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user