Compare commits

..

3 Commits

Author SHA1 Message Date
lebaudantoine 3d7f2957f9 wip support experimental calendar api 2026-05-01 18:43:37 +02:00
lebaudantoine d83d3ccc18 insert link in mail 2026-05-01 17:43:18 +02:00
lebaudantoine 52a9a583e8 wip initiate dev environment 2026-05-01 17:12:53 +02:00
33 changed files with 4505 additions and 488 deletions
-11
View File
@@ -8,17 +8,6 @@ and this project adheres to
## [Unreleased]
### Added
- 🔒️(backend) add validation of Room.configuration
- ✨(helm) add support multiple transcribe worker / endpoint #1247
### Fixed
- ♻(frontend) standardize role terminology across localizations
- 🐛(backend) make start-recording atomic and fault-tolerant
- 🔒️(frontend) room ids are generated with non-cryptographic rand
## [1.15.0] - 2026-04-30
### Added
+1 -1
View File
@@ -109,7 +109,7 @@ k8s_resource('meet-backend', resource_deps=['postgresql', 'minio', 'redis', 'liv
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('meet-celery-transcribe', resource_deps=['redis'])
k8s_resource('meet-backend-migrate', resource_deps=['meet-backend'])
k8s_resource('livekit-livekit-server', resource_deps=['redis'])
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
-7
View File
@@ -101,12 +101,6 @@ update_npm_version "mail"
# Update backend pyproject.toml
update_python_version "backend"
# Run uv lock in backend
print_info "Running uv lock in backend..."
cd "src/backend"
uv lock
cd -
# Update summary pyproject.toml
update_python_version "summary"
@@ -155,7 +149,6 @@ echo " - src/frontend/package.json"
echo " - src/sdk/package.json"
echo " - src/mail/package.json"
echo " - src/backend/pyproject.toml"
echo " - src/backend/uv.lock"
echo " - src/summary/pyproject.toml"
echo " - src/agents/pyproject.toml"
echo " - CHANGELOG.md"
+3 -4
View File
@@ -44,15 +44,14 @@ server {
add_header Pragma "no-cache" always;
add_header Expires 0 always;
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com https://*.cloud.microsoft";
set $ministries_domains "https://courrieljf.ccomptes.fr";
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
set $nonce $request_id;
set $csp "upgrade-insecure-requests; ";
set $csp "${csp}frame-ancestors ${ms_domains} ${ministries_domains}; ";
set $csp "${csp}frame-ancestors ${ms_domains}; ";
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
set $csp "${csp}connect-src 'self' ${ms_domains} ${ministries_domains}; ";
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
set $csp "${csp}frame-src 'none'; ";
set $csp "${csp}object-src 'none'; ";
set $csp "${csp}base-uri 'none'; ";
+27 -23
View File
@@ -36,7 +36,7 @@
"source-map-loader": "^5.0.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "5.2.1"
"webpack-dev-server": "5.1.0"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
@@ -8996,6 +8996,23 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/html-escaper": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -15654,16 +15671,15 @@
}
},
"node_modules/webpack-dev-server": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz",
"integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz",
"integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/bonjour": "^3.5.13",
"@types/connect-history-api-fallback": "^1.5.4",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.17.21",
"@types/serve-index": "^1.9.4",
"@types/serve-static": "^1.15.5",
"@types/sockjs": "^0.3.36",
@@ -15674,9 +15690,10 @@
"colorette": "^2.0.10",
"compression": "^1.7.4",
"connect-history-api-fallback": "^2.0.0",
"express": "^4.21.2",
"express": "^4.19.2",
"graceful-fs": "^4.2.6",
"http-proxy-middleware": "^2.0.7",
"html-entities": "^2.4.0",
"http-proxy-middleware": "^2.0.3",
"ipaddr.js": "^2.1.0",
"launch-editor": "^2.6.1",
"open": "^10.0.3",
@@ -15711,19 +15728,6 @@
}
}
},
"node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": {
"version": "4.19.8",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
"integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/webpack-dev-server/node_modules/define-lazy-prop": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@@ -15767,9 +15771,9 @@
}
},
"node_modules/webpack-dev-server/node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true,
"license": "MIT",
"engines": {
+1 -1
View File
@@ -53,7 +53,7 @@
"source-map-loader": "^5.0.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "5.2.1"
"webpack-dev-server": "5.1.0"
},
"prettier": "office-addin-prettier-config",
"browserslist": [
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "thunderbird",
"version": "1.0.0",
"main": "index.js",
"private": true,
"scripts": {
"dev": "web-ext run",
"lint": "web-ext lint --source-dir=./src"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"web-ext": "^10.1.0"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { buildMeetingMessage } from "./lib/meeting-message.js";
console.log("[meeting-link] background loaded at", new Date().toISOString());
(async () => {
try {
console.log("[meeting-link] browser.calendar exists?", !!browser.calendar);
if (browser.calendar) {
console.log("[meeting-link] calendar namespace keys:",
Object.keys(browser.calendar));
}
// Try the most basic read call — list configured calendars.
// The exact method name varies between experiment versions; we'll
// try the common one first.
if (browser.calendar.calendars?.query) {
const calendars = await browser.calendar.calendars.query({});
console.log("[meeting-link] calendars found:", calendars.length, calendars);
} else {
console.warn("[meeting-link] calendars.query not present — namespace shape:",
browser.calendar);
}
} catch (err) {
console.error("[meeting-link] calendar probe failed:", err);
}
})();
// Hardcoded for Spike 1. Spike 2 replaces this with a fetch() to your API.
const STUB_MEETING_DATA = {
url: "https://meet.example.com/m/abc-123-xyz",
telephony: {
phone_number: "+33123456789",
pin_code: "1234567890",
},
};
browser.runtime.onMessage.addListener(async (msg) => {
if (msg?.type === "GET_MEETING_MESSAGE") {
try {
const built = buildMeetingMessage(STUB_MEETING_DATA);
return { ok: true, ...built };
} catch (err) {
console.error("[meeting-link] build failed", err);
return { ok: false, error: String(err.message || err) };
}
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

+41
View File
@@ -0,0 +1,41 @@
{
"manifest_version": 3,
"name": "Meeting Link (dev)",
"version": "0.1.0",
"description": "Spike 0 — verifying the dev loop",
"browser_specific_settings": {
"gecko": {
"id": "meeting-link-dev@yourcompany.example",
"strict_min_version": "128.0"
}
},
"background": {
"scripts": ["background.js"],
"type": "module"
},
"compose_action": {
"default_title": "Insert meeting link",
"default_popup": "popup/popup.html",
"default_icon": "icons/icon-32.png"
},
"permissions": ["compose"],
"experiment_apis": {
"calendar_provider": {
"schema": "experiments/calendar/schema/calendar-provider.json",
"parent": {
"scopes": ["addon_parent"],
"paths": [["calendar", "provider"]],
"script": "experiments/calendar/parent/ext-calendar-provider.js",
"events": ["startup"]
}
},
"calendar_calendars": {
"schema": "experiments/calendar/schema/calendar-calendars.json",
"parent": {
"scopes": ["addon_parent"],
"paths": [["calendar", "calendars"]],
"script": "experiments/calendar/parent/ext-calendar-calendars.js"
}
}
}
}
@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Meeting link</title>
<style>
body { font: 13px system-ui; padding: 12px; min-width: 240px; margin: 0; }
button { font: inherit; padding: 6px 12px; cursor: pointer; }
#status { margin-top: 8px; color: #555; min-height: 1.2em; white-space: pre-wrap; }
#status.error { color: #b00020; }
</style>
</head>
<body>
<button id="insert">Insert meeting link</button>
<div id="status"></div>
<script src="popup.js"></script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
const insertBtn = document.getElementById("insert");
const statusEl = document.getElementById("status");
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.classList.toggle("error", isError);
}
insertBtn.addEventListener("click", async () => {
insertBtn.disabled = true;
setStatus("Generating link…");
try {
// 1. Find the compose tab this popup belongs to.
// A compose_action popup is anchored to a compose window, so the
// "active tab in the current window" is the compose tab itself.
const [composeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!composeTab) throw new Error("No compose tab found");
// 2. Read the current compose state — we need to know if we're
// in HTML mode or plain-text mode, and we need the existing body
// so we can append rather than overwrite.
const details = await browser.compose.getComposeDetails(composeTab.id);
// 3. Ask the background to produce the meeting message.
const reply = await browser.runtime.sendMessage({
type: "GET_MEETING_MESSAGE",
});
if (!reply?.ok) throw new Error(reply?.error || "Background error");
// 4. Append to the existing body in the right format.
if (details.isPlainText) {
const newBody = (details.plainTextBody || "") + "\n\n" + reply.text;
await browser.compose.setComposeDetails(composeTab.id, {
plainTextBody: newBody,
});
} else {
const newBody = appendHtmlBeforeBodyEnd(details.body || "", reply.html);
await browser.compose.setComposeDetails(composeTab.id, {
body: newBody,
});
}
setStatus("Inserted ✓");
setTimeout(() => window.close(), 600);
} catch (err) {
console.error("[meeting-link] popup insert failed", err);
setStatus("Failed: " + (err.message || err), true);
insertBtn.disabled = false;
}
});
/**
* Append HTML right before </body>, or fall back to concatenation if no
* </body> tag is present (Thunderbird's compose body is usually a full
* HTML document, but be defensive).
*/
function appendHtmlBeforeBodyEnd(currentHtml, fragment) {
const idx = currentHtml.toLowerCase().lastIndexOf("</body>");
if (idx === -1) return currentHtml + fragment;
return currentHtml.slice(0, idx) + fragment + currentHtml.slice(idx);
}
+30
View File
@@ -0,0 +1,30 @@
// web-ext-config.cjs
const os = require("os");
const path = require("path");
function thunderbirdBinary() {
if (process.env.WEB_EXT_FIREFOX) return process.env.WEB_EXT_FIREFOX;
switch (process.platform) {
case "darwin":
return "/Applications/Thunderbird Beta.app/Contents/MacOS/thunderbird";
case "linux":
return "/usr/bin/thunderbird"; // adjust to your install
case "win32":
return "C:\\Program Files\\Mozilla Thunderbird\\thunderbird.exe";
default:
throw new Error("Unsupported platform: " + process.platform);
}
}
module.exports = {
sourceDir: "./src",
artifactsDir: "./web-ext-artifacts",
run: {
firefox: thunderbirdBinary(),
firefoxProfile: path.join(os.homedir(), ".thunderbird-dev-profile"),
profileCreateIfMissing: true,
keepProfileChanges: true,
browserConsole: true,
},
ignoreFiles: ["package-lock.json", "web-ext-config.cjs", "*.md"],
};
-27
View File
@@ -14,7 +14,6 @@ from django.utils.translation import gettext_lazy as _
from django_pydantic_field.rest_framework import SchemaField
from pydantic import BaseModel, Field
from pydantic import ValidationError as PydanticValidationError
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
from timezone_field.rest_framework import TimeZoneSerializerField
@@ -132,16 +131,6 @@ class RoomSerializer(serializers.ModelSerializer):
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
read_only_fields = ["id", "slug", "pin_code"]
def validate_configuration(self, value):
"""Validate room configuration against the RoomConfiguration schema."""
if value is None or value == {}:
return value
try:
RoomConfiguration.model_validate(value)
except PydanticValidationError as e:
raise serializers.ValidationError(e.errors()) from e
return value
def to_representation(self, instance):
"""
Add users only for administrator users.
@@ -317,22 +306,6 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
)
RoomConfigurationTrackSource = Literal[
"camera", "microphone", "screen_share", "screen_share_audio"
]
class RoomConfiguration(BaseModel):
"""Validate room configuration structure.
Unknown fields are rejected.
"""
can_publish_sources: list[RoomConfigurationTrackSource] | None = None
model_config = {"extra": "forbid"}
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
+10 -26
View File
@@ -6,9 +6,7 @@ from logging import getLogger
from urllib.parse import unquote, urlparse
from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.files.storage import default_storage
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.http import Http404
from django.shortcuts import get_object_or_404
@@ -322,27 +320,16 @@ class RoomViewSet(
options = serializer.validated_data.get("options")
room = self.get_object()
try:
with transaction.atomic():
recording = models.Recording.objects.create(
room=room,
mode=mode,
options=options.model_dump(exclude_none=True) if options else {},
)
models.RecordingAccess.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
recording=recording,
)
# May raise exception if an active or initiated recording already exist for the room
recording = models.Recording.objects.create(
room=room,
mode=mode,
options=options.model_dump(exclude_none=True) if options else {},
)
except (DjangoValidationError, IntegrityError):
# DjangoValidationError covers the Python-level check (full_clean);
# IntegrityError covers the race where two concurrent requests both
# pass that check and the DB-level UNIQUE constraint catches the loser.
return drf_response.Response(
{"error": f"A recording is already in progress for room {room.slug}"},
status=drf_status.HTTP_409_CONFLICT,
)
models.RecordingAccess.objects.create(
user=self.request.user, role=models.RoleChoices.OWNER, recording=recording
)
worker_service = get_worker_service(mode=recording.mode)
worker_manager = WorkerServiceMediator(worker_service=worker_service)
@@ -350,12 +337,9 @@ class RoomViewSet(
try:
worker_manager.start(recording)
except RecordingStartError:
models.Recording.objects.filter(pk=recording.pk).update(
status=models.RecordingStatusChoices.FAILED_TO_START
)
return drf_response.Response(
{"error": f"Recording failed to start for room {room.slug}"},
status=drf_status.HTTP_502_BAD_GATEWAY,
status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
)
if settings.METADATA_COLLECTOR_ENABLED and (
+1 -1
View File
@@ -123,7 +123,7 @@ class LobbyService:
def request_entry(
self,
room: models.Room,
room,
request,
username: str,
) -> Tuple[LobbyParticipant, Optional[Dict]]:
@@ -117,7 +117,7 @@ def test_api_files_create_file_authenticated_success():
policy_parsed = urlparse(policy)
assert policy_parsed.scheme == "http"
assert policy_parsed.netloc in ["minio:9000", "localhost:9000"]
assert policy_parsed.netloc == "localhost:9000"
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png"
query_params = parse_qs(policy_parsed.query)
@@ -232,7 +232,7 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
"""
room = RoomFactory(
access_level=RoomAccessLevel.PUBLIC,
configuration={"can_publish_sources": ["camera"]},
configuration={"can_publish_sources": ["mock-source"]},
)
user = UserFactory()
@@ -264,7 +264,7 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
user=user,
username=None,
color=None,
sources=["camera"],
sources=["mock-source"],
is_admin_or_owner=False,
participant_id=None,
)
@@ -363,7 +363,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
other_user = UserFactory()
room = RoomFactory(
configuration={"can_publish_sources": ["camera"]},
configuration={"can_publish_sources": ["mock-source"]},
)
UserResourceAccessFactory(resource=room, user=user, role="member")
UserResourceAccessFactory(resource=room, user=other_user, role="member")
@@ -401,7 +401,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
user=user,
username=None,
color=None,
sources=["camera"],
sources=["mock-source"],
is_admin_or_owner=False,
participant_id=None,
)
@@ -140,18 +140,16 @@ def test_start_recording_worker_error(
mock_worker_service_factory.assert_called_once_with(mode="screen_recording")
assert response.status_code == 502
assert response.status_code == 500
assert response.json() == {
"error": f"Recording failed to start for room {room.slug}"
}
# Recording object should be created even if worker fails, and moved out
# of the unique-constraint window so the room is not locked.
# Recording object should be created even if worker fails
assert Recording.objects.count() == 1
recording = Recording.objects.first()
assert recording.room == room
assert recording.mode == "screen_recording"
assert recording.status == "failed_to_start"
# Verify recording access details
assert recording.accesses.count() == 1
@@ -160,72 +158,6 @@ def test_start_recording_worker_error(
assert access.role == "owner"
@pytest.mark.parametrize(
"status",
["active", "initiated"],
)
def test_start_recording_conflict_when_already_in_progress(
status, mock_worker_service_factory, mock_worker_manager, settings
):
"""Should return 409 when a second start is attempted while a recording is already active."""
settings.RECORDING_ENABLE = True
room = RoomFactory()
user = UserFactory()
room.accesses.create(user=user, role="owner")
# Pre-existing active recording for the same room.
Recording.objects.create(room=room, mode="screen_recording", status="active")
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/rooms/{room.id}/start-recording/",
{"mode": "screen_recording"},
)
assert response.status_code == 409
assert response.json() == {
"error": f"A recording is already in progress for room {room.slug}"
}
# No new recording row, no access row leaked from the rolled-back transaction.
assert Recording.objects.count() == 1
assert Recording.objects.first().accesses.count() == 0
mock_worker_manager.start.assert_not_called()
def test_start_recording_after_worker_failure_unblocks_room(
mock_worker_service_factory, mock_worker_manager, settings
):
"""Should allow a new recording when the previous recording failed."""
settings.RECORDING_ENABLE = True
room = RoomFactory()
user = UserFactory()
room.accesses.create(user=user, role="owner")
mock_worker_manager.start = mock.Mock(
side_effect=[RecordingStartError("boom"), None]
)
client = APIClient()
client.force_login(user)
first = client.post(
f"/api/v1.0/rooms/{room.id}/start-recording/",
{"mode": "screen_recording"},
)
assert first.status_code == 502
second = client.post(
f"/api/v1.0/rooms/{room.id}/start-recording/",
{"mode": "screen_recording"},
)
assert second.status_code == 201
assert Recording.objects.count() == 2
def test_start_recording_success(
mock_worker_service_factory, mock_worker_manager, settings
):
@@ -67,7 +67,7 @@ def test_api_rooms_update_members():
"name": "New name",
"slug": "should-be-ignored",
"access_level": RoomAccessLevel.RESTRICTED,
"configuration": {"can_publish_sources": ["camera", "microphone"]},
"configuration": {"the_key": "the_value"},
},
format="json",
)
@@ -95,7 +95,7 @@ def test_api_rooms_update_administrators():
"name": "New name",
"slug": "should-be-ignored",
"access_level": RoomAccessLevel.PUBLIC,
"configuration": {"can_publish_sources": ["camera", "microphone"]},
"configuration": {"the_key": "the_value"},
},
format="json",
)
@@ -104,98 +104,7 @@ def test_api_rooms_update_administrators():
assert room.name == "New name"
assert room.slug == "new-name"
assert room.access_level == RoomAccessLevel.PUBLIC
assert room.configuration == {"can_publish_sources": ["camera", "microphone"]}
@pytest.mark.parametrize(
"configuration",
[
{},
{"can_publish_sources": ["camera", "microphone"]},
{
"can_publish_sources": [
"camera",
"microphone",
"screen_share",
"screen_share_audio",
]
},
{"can_publish_sources": []},
{"can_publish_sources": None},
],
)
def test_api_rooms_update_configuration_valid(configuration):
"""Administrators should be allowed to set valid configurations."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{"configuration": configuration},
format="json",
)
assert response.status_code == 200
room.refresh_from_db()
assert room.configuration == configuration
def test_api_rooms_update_configuration_extra_keys_rejected():
"""Extra keys in configuration should be rejected."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{
"configuration": {
"can_publish_sources": ["camera"],
"arbitrary_key": "value",
}
},
format="json",
)
assert response.status_code == 400
room.refresh_from_db()
assert room.configuration == {}
@pytest.mark.parametrize("invalid_source", ["invalid_source", "CAMERA"])
def test_api_rooms_update_configuration_invalid_source_value(invalid_source):
"""Invalid source values should be rejected."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{"configuration": {"can_publish_sources": [invalid_source]}},
format="json",
)
assert response.status_code == 400
room.refresh_from_db()
assert room.configuration == {}
def test_api_rooms_update_configuration_wrong_type():
"""Configuration values with wrong types should be rejected."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{"configuration": {"can_publish_sources": "camera"}},
format="json",
)
assert response.status_code == 400
room.refresh_from_db()
assert room.configuration == {}
assert room.configuration == {"the_key": "the_value"}
def test_api_rooms_update_administrators_of_another():
+4 -120
View File
@@ -54,7 +54,7 @@
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.10",
"postcss": "8.5.6",
"prettier": "3.8.1",
"typescript": "5.8.3",
"vite": "7.3.2",
@@ -1759,35 +1759,6 @@
"ts-pattern": "5.9.0"
}
},
"node_modules/@pandacss/core/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/dev": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@pandacss/dev/-/dev-1.8.2.tgz",
@@ -1845,35 +1816,6 @@
"ts-pattern": "5.9.0"
}
},
"node_modules/@pandacss/generator/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/is-valid-prop": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.8.2.tgz",
@@ -1975,35 +1917,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@pandacss/node/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/node/node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
@@ -2063,35 +1976,6 @@
"postcss": "8.5.6"
}
},
"node_modules/@pandacss/postcss/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/preset-base": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/@pandacss/preset-base/-/preset-base-1.8.2.tgz",
@@ -9548,9 +9432,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
+1 -1
View File
@@ -59,7 +59,7 @@
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.10",
"postcss": "8.5.6",
"prettier": "3.8.1",
"typescript": "5.8.3",
"vite": "7.3.2",
@@ -1,20 +1,10 @@
// Google Meet uses only letters in a room identifier
const ROOM_ID_ALLOWED_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz'
const getRandomChar = () => {
const maxValue =
Math.floor(0x100000000 / ROOM_ID_ALLOWED_CHARACTERS.length) *
ROOM_ID_ALLOWED_CHARACTERS.length
const randomValue = new Uint32Array(1)
do {
crypto.getRandomValues(randomValue)
} while (randomValue[0] >= maxValue)
return ROOM_ID_ALLOWED_CHARACTERS[
randomValue[0] % ROOM_ID_ALLOWED_CHARACTERS.length
const getRandomChar = () =>
ROOM_ID_ALLOWED_CHARACTERS[
Math.floor(Math.random() * ROOM_ID_ALLOWED_CHARACTERS.length)
]
}
const generateSegment = (length: number): string =>
Array.from(Array(length), getRandomChar).join('')
+2 -2
View File
@@ -1,7 +1,7 @@
{
"error": {
"title": "Aufzeichnung nicht verfügbar",
"body": "Die Aufzeichnung ist nicht verfügbar oder wurde möglicherweise gelöscht. Nur der Host der Besprechung hat Zugriff darauf. Wende dich bei Bedarf gerne an die Person."
"body": "Die Aufzeichnung ist nicht verfügbar oder wurde möglicherweise gelöscht. Nur die*der Organisator:in der Besprechung hat Zugriff darauf. Wende dich bei Bedarf gerne an die Person."
},
"expired": {
"title": "Aufzeichnung abgelaufen",
@@ -22,7 +22,7 @@
"button": "Herunterladen",
"warning": {
"title": "Linkfreigabe deaktiviert",
"body": "Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Hosts können sie herunterladen."
"body": "Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisator:innen können sie herunterladen."
}
}
}
+4 -4
View File
@@ -3,7 +3,7 @@
"heading": {
"normal": "Du hast das Meeting verlassen",
"duplicateIdentity": "Du bist dem Meeting von einem anderen Gerät aus beigetreten",
"participantRemoved": "Du wurdest vom Host aus dem Meeting entfernt"
"participantRemoved": "Du wurdest von der*dem Organisator:in aus dem Meeting entfernt"
},
"home": "Zur Startseite zurückkehren",
"back": "Dem Meeting erneut beitreten"
@@ -383,7 +383,7 @@
"body": "Transkribiere bis zu {{max_duration}} Meetingdauer.",
"bodyWithoutMaxDuration": "Transkribieren dein Meeting ohne Zeitbegrenzung.",
"details": {
"receiver": "Das Transkript wird an den Host und die Co-Hosts gesendet.",
"receiver": "Das Transkript wird an die*den Organisator:in und die Mitorganisierenden gesendet.",
"destination": "Ein neues Dokument wird erstellt auf",
"destinationUnknown": "Ein neues Dokument wird erstellt",
"language": "Meeting-Sprache:",
@@ -438,7 +438,7 @@
"linkMore": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zur Aufzeichnung öffnen öffnet in neuem Tab",
"details": {
"receiver": "Die Aufzeichnung wird an den Host und die Co-Hosts gesendet.",
"receiver": "Die Aufzeichnung wird an die*den Organisator:in und die Mitorganisierenden gesendet.",
"destination": "Diese Aufzeichnung wird vorübergehend auf unseren Servern gespeichert",
"loading": "Aufzeichnung wird gestartet",
"linkMore": "Mehr erfahren",
@@ -494,7 +494,7 @@
"button": "OK"
},
"admin": {
"description": "Diese Host-Einstellungen ermöglichen dir die Kontrolle über dein Meeting. Nur Hosts haben Zugriff auf diese Optionen.",
"description": "Diese Einstellungen für Organisierende ermöglichen dir die Kontrolle über dein Meeting. Nur Organisierende haben Zugriff auf diese Optionen.",
"access": {
"title": "Raumzugang",
"description": "Diese Einstellungen gelten auch für zukünftige Meetings in diesem Raum.",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"error": {
"title": "Recording unavailable",
"body": "The recording is unavailable or may have been deleted. Only the meeting host can access it. Feel free to contact them if needed."
"body": "The recording is unavailable or may have been deleted. Only the meeting organizer can access it. Feel free to contact them if needed."
},
"expired": {
"title": "Recording expired",
@@ -22,7 +22,7 @@
"button": "Download",
"warning": {
"title": "Link sharing disabled",
"body": "Sharing the recording via link is not yet available. Only hosts can download it."
"body": "Sharing the recording via link is not yet available. Only organizers can download it."
}
}
}
+4 -4
View File
@@ -3,7 +3,7 @@
"heading": {
"normal": "You have left the meeting",
"duplicateIdentity": "You have joined the meeting from another device",
"participantRemoved": "You have been removed from the meeting by a host"
"participantRemoved": "You have been removed from the meeting by an administrator"
},
"home": "Return to home",
"back": "Rejoin the meeting"
@@ -328,7 +328,7 @@
"chat": "Messages in the chat",
"transcript": "Transcribe",
"screenRecording": "Record",
"admin": "Host settings",
"admin": "Admin settings",
"tools": "More tools",
"info": "Meeting information"
},
@@ -338,7 +338,7 @@
"chat": "messages",
"transcript": "transcribe",
"screenRecording": "record",
"admin": "host settings",
"admin": "admin settings",
"tools": "more tools",
"info": "meeting information"
},
@@ -493,7 +493,7 @@
"button": "OK"
},
"admin": {
"description": "These host settings allow you to maintain control of your meeting. Only hosts can access these controls.",
"description": "These organizer settings allow you to maintain control of your meeting. Only organizers can access these controls.",
"access": {
"title": "Room access",
"description": "These settings will also apply to future occurrences of this meeting.",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"error": {
"title": "Opname niet beschikbaar",
"body": "De opname is niet beschikbaar of is mogelijk verwijderd. Alleen de host van de vergadering heeft toegang. Neem gerust contact met hem of haar op als dat nodig is."
"body": "De opname is niet beschikbaar of is mogelijk verwijderd. Alleen de organisator van de vergadering heeft toegang. Neem gerust contact met hem of haar op als dat nodig is."
},
"expired": {
"title": "Opname verlopen",
@@ -22,7 +22,7 @@
"button": "Downloaden",
"warning": {
"title": "Delen via link uitgeschakeld",
"body": "Het delen van de opname via een link is nog niet beschikbaar. Alleen hosts kunnen deze downloaden."
"body": "Het delen van de opname via een link is nog niet beschikbaar. Alleen organisatoren kunnen deze downloaden."
}
}
}
+2 -2
View File
@@ -437,7 +437,7 @@
"linkMore": "Documentatie openen",
"linkAriaLabel": "Documentatie over opname openen - opent in nieuw venster",
"details": {
"receiver": "De opname wordt verzonden naar de host en co-hosts.",
"receiver": "De opname wordt verzonden naar de organisator en co-organisatoren.",
"destination": "Deze opname wordt tijdelijk op onze servers bewaard",
"loading": "Opname wordt gestart",
"linkMore": "Meer informatie",
@@ -493,7 +493,7 @@
"button": "Opnieuw proberen"
},
"admin": {
"description": "Deze hostinstellingen geven u controle over uw vergadering. Alleen hosts hebben toegang tot deze bedieningselementen.",
"description": "Deze organisatorinstellingen geven u controle over uw vergadering. Alleen organisatoren hebben toegang tot deze bedieningselementen.",
"access": {
"title": "Toegang tot vergadering",
"description": "Deze instellingen zijn ook van toepassing op toekomstige sessies van deze vergadering.",
@@ -1,62 +1,52 @@
{{- $sharedEnvVars := default (dict) .Values.celeryTranscribe.envVars -}}
{{- $envVars := include "meet.common.env" (list . .Values.celeryTranscribe) -}}
{{- $fullName := include "meet.celeryTranscribe.fullname" . -}}
{{- $component := "celery-transcribe" -}}
{{- range $idx, $instance := .Values.celeryTranscribe.instances }}
{{- $instanceValues := merge $instance $.Values.celeryTranscribe }}
{{- $fullName := printf "%s-%s" (include "meet.celeryTranscribe.fullname" $) $instance.name }}
{{- $extraInstanceEnvVars := default (dict) $instance.extraEnvVars -}}
{{- $mergedInstanceEnvVars := merge $extraInstanceEnvVars $sharedEnvVars -}}
{{- $fakeInstanceObjectForEnvHelper := dict "envVars" $mergedInstanceEnvVars -}}
{{- $envVars := include "meet.common.env" (list . $fakeInstanceObjectForEnvHelper) -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $fullName }}
annotations:
{{- with $instanceValues.dpAnnotations }}
{{- with .Values.celeryTranscribe.dpAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
namespace: {{ $.Release.Namespace | quote }}
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "meet.common.labels" (list $ $component) | nindent 4 }}
instance: {{ $instance.name }}
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
spec:
replicas: {{ $instanceValues.replicas }}
replicas: {{ .Values.celeryTranscribe.replicas }}
selector:
matchLabels:
{{- include "meet.common.selectorLabels" (list $ $component) | nindent 6 }}
instance: {{ $instance.name }}
{{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
template:
metadata:
annotations:
{{- with $instanceValues.podAnnotations }}
{{- with .Values.celeryTranscribe.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "meet.common.selectorLabels" (list $ $component) | nindent 8 }}
instance: {{ $instance.name }}
{{- include "meet.common.selectorLabels" (list . $component) | nindent 8 }}
spec:
{{- if $.Values.image.credentials }}
imagePullSecrets:
- name: {{ include "meet.secret.dockerconfigjson.name" (dict "fullname" (include "meet.fullname" $) "imageCredentials" $.Values.image.credentials) }}
- name: {{ include "meet.secret.dockerconfigjson.name" (dict "fullname" (include "meet.fullname" .) "imageCredentials" $.Values.image.credentials) }}
{{- end }}
shareProcessNamespace: {{ $instanceValues.shareProcessNamespace }}
{{- with $instanceValues.podSecurityContext }}
shareProcessNamespace: {{ .Values.celeryTranscribe.shareProcessNamespace }}
{{- with .Values.celeryTranscribe.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
{{- with $instanceValues.sidecars }}
{{- with .Values.celeryTranscribe.sidecars }}
{{- toYaml . | nindent 8 }}
{{- end }}
- name: {{ $.Chart.Name }}
image: "{{ ($instanceValues.image | default dict).repository | default $.Values.image.repository }}:{{ ($instanceValues.image | default dict).tag | default $.Values.image.tag }}"
imagePullPolicy: {{ ($instanceValues.image | default dict).pullPolicy | default $.Values.image.pullPolicy }}
{{- with $instanceValues.command }}
- name: {{ .Chart.Name }}
image: "{{ (.Values.celeryTranscribe.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.celeryTranscribe.image | default dict).tag | default .Values.image.tag }}"
imagePullPolicy: {{ (.Values.celeryTranscribe.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
{{- with .Values.celeryTranscribe.command }}
command:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $instanceValues.args }}
{{- with .Values.celeryTranscribe.args }}
args:
{{- toYaml . | nindent 12 }}
{{- end }}
@@ -64,65 +54,65 @@ spec:
{{- if $envVars }}
{{- $envVars | indent 12 }}
{{- end }}
{{- with $instanceValues.securityContext }}
{{- with .Values.celeryTranscribe.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ $instanceValues.service.targetPort }}
containerPort: {{ .Values.celeryTranscribe.service.targetPort }}
protocol: TCP
{{- if $instanceValues.probes.liveness }}
{{- if .Values.celeryTranscribe.probes.liveness }}
livenessProbe:
{{- include "meet.probes.abstract" (merge $instanceValues.probes.liveness (dict "targetPort" $instanceValues.service.targetPort)) | nindent 12 }}
{{- include "meet.probes.abstract" (merge .Values.celeryTranscribe.probes.liveness (dict "targetPort" .Values.celeryTranscribe.service.targetPort )) | nindent 12 }}
{{- end }}
{{- if $instanceValues.probes.readiness }}
{{- if .Values.celeryTranscribe.probes.readiness }}
readinessProbe:
{{- include "meet.probes.abstract" (merge $instanceValues.probes.readiness (dict "targetPort" $instanceValues.service.targetPort)) | nindent 12 }}
{{- include "meet.probes.abstract" (merge .Values.celeryTranscribe.probes.readiness (dict "targetPort" .Values.celeryTranscribe.service.targetPort )) | nindent 12 }}
{{- end }}
{{- if $instanceValues.probes.startup }}
{{- if .Values.celeryTranscribe.probes.startup }}
startupProbe:
{{- include "meet.probes.abstract" (merge $instanceValues.probes.startup (dict "targetPort" $instanceValues.service.targetPort)) | nindent 12 }}
{{- include "meet.probes.abstract" (merge .Values.celeryTranscribe.probes.startup (dict "targetPort" .Values.celeryTranscribe.service.targetPort )) | nindent 12 }}
{{- end }}
{{- with $instanceValues.resources }}
{{- with .Values.celeryTranscribe.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
{{- range $index, $value := $.Values.mountFiles }}
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
mountPath: {{ $value.path }}
subPath: content
{{- end }}
{{- range $name, $volume := $instanceValues.persistence }}
{{- range $name, $volume := .Values.celeryTranscribe.persistence }}
- name: "{{ $name }}"
mountPath: "{{ $volume.mountPath }}"
{{- end }}
{{- range $instanceValues.extraVolumeMounts }}
{{- range .Values.celeryTranscribe.extraVolumeMounts }}
- name: {{ .name }}
mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }}
{{- end }}
{{- with $instanceValues.nodeSelector }}
{{- with .Values.celeryTranscribe.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $instanceValues.affinity }}
{{- with .Values.celeryTranscribe.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with $instanceValues.tolerations }}
{{- with .Values.celeryTranscribe.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- range $index, $value := $.Values.mountFiles }}
{{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}"
configMap:
name: "{{ include "meet.fullname" $ }}-files-{{ $index }}"
{{- end }}
{{- range $name, $volume := $instanceValues.persistence }}
{{- range $name, $volume := .Values.celeryTranscribe.persistence }}
- name: "{{ $name }}"
{{- if eq $volume.type "emptyDir" }}
emptyDir: {}
@@ -131,7 +121,7 @@ spec:
claimName: "{{ $fullName }}-{{ $name }}"
{{- end }}
{{- end }}
{{- range $instanceValues.extraVolumes }}
{{- range .Values.celeryTranscribe.extraVolumes }}
- name: {{ .name }}
{{- if .existingClaim }}
persistentVolumeClaim:
@@ -153,18 +143,15 @@ spec:
{{- end }}
{{- end }}
---
{{ if $instanceValues.pdb.enabled }}
{{ if .Values.celeryTranscribe.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ $fullName }}
namespace: {{ $.Release.Namespace | quote }}
namespace: {{ .Release.Namespace | quote }}
spec:
maxUnavailable: 1
selector:
matchLabels:
{{- include "meet.common.selectorLabels" (list $ $component) | nindent 6 }}
instance: {{ $instance.name }}
---
{{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
{{ end }}
{{- end }}
-15
View File
@@ -848,21 +848,6 @@ celeryTranscribe:
pdb:
enabled: false
## @param celeryTranscribe.instances List of celeryTranscribe instances to deploy. Each entry creates a dedicated Deployment. Useful when wanting to use multiple instances of WhisperX (configure each endpoint in the extraEnv value specific to that instance).
## @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[].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
## @extra celeryTranscribe.instances[].nodeSelector Override node selector for this specific instance
## @extra celeryTranscribe.instances[].affinity Override affinity for this specific instance
## @extra celeryTranscribe.instances[].tolerations Override tolerations for this specific instance
## @extra celeryTranscribe.instances[].pdb.enabled Enable pdb for this specific instance
instances:
- name: default
extraEnvVars: {}
## @section celerySummarize
celerySummarize:
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies = [
[project.optional-dependencies]
dev = [
"ruff==0.15.6",
"pytest==9.0.3",
"pytest==9.0.2",
"responses>=0.25.8",
]