mirror of
https://github.com/suitenumerique/meet.git
synced 2026-11-01 17:09:49 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cae6c19da8 | |||
| 455daf1e8f | |||
| 73b182b34a | |||
| 6a1622cd50 | |||
| 56b5a34ec5 | |||
| 815baa73e8 |
+13
-13
@@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -117,11 +117,11 @@ jobs:
|
||||
timeout 30 sh -c 'until docker exec garage /garage health; do sleep 1; done'
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.15"
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -170,12 +170,12 @@ jobs:
|
||||
sudo apt-get install -y ffmpeg
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
with:
|
||||
python-version: "3.15"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -208,12 +208,12 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: "24.22.1"
|
||||
|
||||
@@ -232,7 +232,7 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+11
-1
@@ -8,6 +8,16 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.37.0] - 2026-11-01
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(summary) update ffmpeg to 8.1.3-r0
|
||||
- ✨(frontend) replace the Crisp chat with the La Suite support form
|
||||
- 🔒️(frontend) keep rooms out of search engine and AI indexes
|
||||
- ⬆️(backend) upgrade django-lasuite to v1.0.0
|
||||
- ⚡️(docker) build frontend assets natively for arm64 images
|
||||
|
||||
## [1.36.3] - 2026-10-23
|
||||
|
||||
### Fixed
|
||||
@@ -178,7 +188,7 @@ and this project adheres to
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(helm) probe liveness on __lbheartbeat__ and readiness on __heartbeat__
|
||||
- 🐛(helm) probe liveness on `__lbheartbeat__` and readiness on `__heartbeat__`
|
||||
- 🐛(helm) render periodSeconds and failureThreshold on probes
|
||||
- 🐛(backend) report the app release to Sentry instead of "NA"
|
||||
- 🐛(frontend) play the waiting room notification sound on every arrival
|
||||
|
||||
+14
@@ -16,6 +16,20 @@ the following command inside your docker container:
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## v1.37.0
|
||||
|
||||
### Crisp support chat replaced by the La Suite support form
|
||||
|
||||
The Crisp chat is gone, the support button, now open the La Suite support form. They are hidden unless these `FRONTEND_SUPPORT` keys are all set.
|
||||
The former help links for recording/transcription are now redirecting to the adhoc page in the official documentation
|
||||
|
||||
### Pages hidden from search engines
|
||||
|
||||
Every page except `/` now carries a `noindex` tag and, with the provided nginx configurations, an `X-Robots-Tag` header to stay out of search indexes.
|
||||
A new `robots.txt` also blocks the main AI crawlers. If you maintain your own nginx configuration, copy the `location = /` and `location = /home.html` blocks from `src/frontend/default.conf`, and the `X-Robots-Tag` header from its `location /`.
|
||||
|
||||
## [v1.35.0]
|
||||
|
||||
### Purging deleted rooms
|
||||
|
||||
Deleting a room now only soft deletes it: it is hidden but keeps its slug and PIN code. The `purge_inactive_rooms` command also permanently deletes soft-deleted rooms. See [the room purge documentation](docs/features/room-purge.md).
|
||||
|
||||
@@ -38,6 +38,17 @@ server {
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /home.html /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
location = /home.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
@@ -45,6 +56,7 @@ server {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
|
||||
add_header Expires 0;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
}
|
||||
|
||||
# Optionally, handle 404 errors by redirecting to index.html
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ---- Front-end image ----
|
||||
FROM node:22.23.4-alpine AS frontend-deps
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.4-alpine AS frontend-deps
|
||||
|
||||
RUN apk upgrade --no-cache ca-certificates
|
||||
|
||||
@@ -41,7 +41,7 @@ COPY ./docker/dinum-frontend/fonts/ \
|
||||
./dist/assets/fonts/
|
||||
|
||||
# ---- Addons builder image ----
|
||||
FROM node:22.23.4-alpine AS addons-builder
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.4-alpine AS addons-builder
|
||||
|
||||
RUN apk upgrade --no-cache ca-certificates
|
||||
|
||||
@@ -65,7 +65,8 @@ RUN apk upgrade --no-cache libexpat && \
|
||||
apk add --no-cache --upgrade 'pcre2>=10.49-r0' 'tiff>=4.7.2-r0' \
|
||||
'libx11>=1.8.14-r0' 'ca-certificates>=20261020-r0' \
|
||||
'brotli-libs>=1.2.1-r0' && \
|
||||
apk del curl
|
||||
apk del curl && \
|
||||
rm -f /usr/share/nginx/html/50x.html
|
||||
USER nginx
|
||||
|
||||
USER nginx
|
||||
|
||||
@@ -43,6 +43,7 @@ server {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
|
||||
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
|
||||
|
||||
@@ -76,6 +77,17 @@ server {
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /home.html /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
location = /home.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
@@ -83,6 +95,7 @@ server {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
|
||||
add_header Expires 0;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
}
|
||||
|
||||
# Optionally, handle 404 errors by redirecting to index.html
|
||||
|
||||
@@ -37,7 +37,7 @@ $ make help
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
If you need any assistance or have questions while getting started, feel free to reach out to @lebaudantoine anytime! Antoine is available to help you onboard and guide you through the process. Chat with him @antoine.lebaud:matrix.org, or from the [support hotline](https://go.crisp.chat/chat/embed/?website_id=58ea6697-8eba-4492-bc59-ad6562585041).
|
||||
If you need any assistance or have questions while getting started, feel free to reach out to @lebaudantoine anytime! Antoine is available to help you onboard and guide you through the process. Chat with him @antoine.lebaud:matrix.org, or by email at [support-visio@numerique.gouv.fr](mailto:support-visio@numerique.gouv.fr).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ These are the environmental options available on meet backend.
|
||||
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
|
||||
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
|
||||
| FRONTEND_ANALYTICS | Analytics information | {} |
|
||||
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
|
||||
| FRONTEND_SUPPORT | La Suite support form (`widget_script_url`, `widget_api_url`, `widget_channel`) and help articles (`help_article_*`) | {} |
|
||||
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
|
||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title data-app-name></title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title data-app-name></title>
|
||||
<link rel="stylesheet" href="../styles/spinner.css" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title data-app-name></title>
|
||||
<link rel="stylesheet" href="taskpane.css" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title data-app-name></title>
|
||||
<link rel="stylesheet" href="../styles/spinner.css" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.36.3"
|
||||
version = "1.37.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.7.0",
|
||||
|
||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
|
||||
[[package]]
|
||||
name = "agents"
|
||||
version = "1.36.3"
|
||||
version = "1.37.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "boto3" },
|
||||
|
||||
@@ -184,7 +184,7 @@ def test_api_rooms_list_with_application_disabled(mock_rs_authenticate, settings
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_list_with_invalid_rs_token(settings):
|
||||
"""Listing rooms with invalid resource server token should return 400."""
|
||||
"""Listing rooms with invalid resource server token should return 401."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
@@ -202,9 +202,8 @@ def test_api_rooms_list_with_invalid_rs_token(settings):
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
# Return 400 instead of 401 because ResourceServerAuthentication raises
|
||||
# SuspiciousOperation when the introspected user is not active
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 401
|
||||
assert "introspected user is not active" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_list_missing_scope():
|
||||
@@ -422,7 +421,7 @@ def test_api_rooms_retrieve_with_expired_token(settings):
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_retrieve_with_invalid_rs_token(settings):
|
||||
"""Retrieving rooms with invalid resource server token should return 400."""
|
||||
"""Retrieving rooms with invalid resource server token should return 401."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
@@ -443,9 +442,8 @@ def test_api_rooms_retrieve_with_invalid_rs_token(settings):
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.get(f"/external-api/v1.0/rooms/{room.id}/")
|
||||
|
||||
# Return 400 instead of 401 because ResourceServerAuthentication raises
|
||||
# SuspiciousOperation when the introspected user is not active
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 401
|
||||
assert "introspected user is not active" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_retrieve_requires_scope():
|
||||
@@ -647,7 +645,7 @@ def test_api_rooms_create_with_expired_token(settings):
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_create_with_invalid_rs_token(settings):
|
||||
"""Creating rooms with invalid resource server token should return 400."""
|
||||
"""Creating rooms with invalid resource server token should return 401."""
|
||||
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
@@ -665,9 +663,8 @@ def test_api_rooms_create_with_invalid_rs_token(settings):
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.post("/external-api/v1.0/rooms/")
|
||||
|
||||
# Return 400 instead of 401 because ResourceServerAuthentication raises
|
||||
# SuspiciousOperation when the introspected user is not active
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 401
|
||||
assert "introspected user is not active" in str(response.data).lower()
|
||||
|
||||
|
||||
def test_api_rooms_create_inactive_user():
|
||||
@@ -2971,6 +2968,36 @@ def test_api_rooms_missing_token_is_audited_as_denial(audit_events):
|
||||
assert event["url"]["path"] == "/external-api/v1.0/rooms/"
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_api_rooms_invalid_rs_token_is_audited_as_denial(audit_events, settings):
|
||||
"""A token refused by the identity provider is recorded as a denial."""
|
||||
settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect"
|
||||
settings.OIDC_OP_URL = "https://oidc.example.com"
|
||||
|
||||
responses.add(
|
||||
responses.POST,
|
||||
"https://oidc.example.com/introspect",
|
||||
json={"iss": "https://oidc.example.com", "active": False},
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
client.credentials(HTTP_AUTHORIZATION="Bearer invalid-token-123")
|
||||
response = client.get("/external-api/v1.0/rooms/")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.headers["WWW-Authenticate"] == "Bearer"
|
||||
responses.assert_call_count("https://oidc.example.com/introspect", 1)
|
||||
|
||||
[event] = find_events(audit_events, "room.list")
|
||||
|
||||
assert event["event"]["category"] == ["api", "authentication"]
|
||||
assert event["event"]["reason"] == "authentication_failed"
|
||||
assert event["lasuite"]["outcome"] == "denied"
|
||||
assert event["lasuite"]["actor"] == {"type": "anonymous"}
|
||||
assert event["http"]["response"] == {"status_code": 401}
|
||||
assert event["error"]["message"] == response.json()["detail"]
|
||||
|
||||
|
||||
def test_api_rooms_missing_scope_is_audited_as_denial(audit_events):
|
||||
"""A token without the required scope is a permission denial by the application."""
|
||||
user = UserFactory()
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.36.3"
|
||||
version = "1.37.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -33,7 +33,7 @@ dependencies = [
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==9.0.0",
|
||||
"django-filter==26.1",
|
||||
"django-lasuite[all]==0.0.29",
|
||||
"django-lasuite[all]==1.0.0",
|
||||
"django-parler==2.4",
|
||||
"redis==5.2.1",
|
||||
"django-redis==7.0.0",
|
||||
|
||||
Generated
+22
-11
@@ -837,26 +837,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-lasuite"
|
||||
version = "0.0.29"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "djangorestframework" },
|
||||
{ name = "joserfc" },
|
||||
{ name = "mozilla-django-oidc" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "requests" },
|
||||
{ name = "requests-toolbelt" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/ab/72a79b7133fb2d17835bef40f45f7e6d2b4343469c37fd00386122e9068d/django_lasuite-0.0.29.tar.gz", hash = "sha256:831cb859e45e33963c9d89168c2de14f9844e805000b0f0d1512ffd0b9731a39", size = 35248, upload-time = "2026-08-25T13:03:43.597Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/cc/35335b826b039c43da68a0e624e154f60c3ac6c83f01f9b4ad6ce91faf03/django_lasuite-1.0.0.tar.gz", hash = "sha256:8921aaba5a78f1f9728cee2cc4596755478108f17146d4a2fccb393c043a05a3", size = 45815, upload-time = "2026-10-30T14:57:31.764Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/cf/86648643221816f18ebd5a3e59b6d2a85ee85b38b19c7abe939f798a2845/django_lasuite-0.0.29-py3-none-any.whl", hash = "sha256:33bcff90ced2709190ef01241badb17ceed08a32725b9cdd3105650b36da303b", size = 54899, upload-time = "2026-08-25T13:03:42.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/6f/d1df3f2881cda711e569b8e685ad3a846ca9c71226340597cee1c09beae0/django_lasuite-1.0.0-py3-none-any.whl", hash = "sha256:23d589be9496b1f484772b49f3c4c4a81e70affd537e3c50abf15303c84d7cb0", size = 68529, upload-time = "2026-10-30T14:57:30.416Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
all = [
|
||||
{ name = "celery" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "django-configurations" },
|
||||
{ name = "djangorestframework" },
|
||||
{ name = "joserfc" },
|
||||
{ name = "mozilla-django-oidc" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "python-icap" },
|
||||
{ name = "requests" },
|
||||
{ name = "requests-toolbelt" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1358,7 +1360,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.36.3"
|
||||
version = "1.37.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1440,7 +1442,7 @@ requires-dist = [
|
||||
{ name = "django-cors-headers", specifier = "==4.9.0" },
|
||||
{ name = "django-countries", specifier = "==9.0.0" },
|
||||
{ name = "django-filter", specifier = "==26.1" },
|
||||
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.29" },
|
||||
{ name = "django-lasuite", extras = ["all"], specifier = "==1.0.0" },
|
||||
{ name = "django-parler", specifier = "==2.4" },
|
||||
{ name = "django-pydantic-field", specifier = "==0.5.4" },
|
||||
{ name = "django-redis", specifier = "==7.0.0" },
|
||||
@@ -2258,6 +2260,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/a3/17c284b4f4d8ad50f0f9ba70ad8fcc35c777aeafcdbbffdd91bbdc5ab379/python_frontmatter-1.3.0-py3-none-any.whl", hash = "sha256:9f7dd9260bec99044219159a329f64f039087f9d1a2124c9442556f2fe6f82ec", size = 10562, upload-time = "2026-05-20T19:21:43.323Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-icap"
|
||||
version = "0.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/0a/123c30f4041eabb94c587d35fb64aa9498435d028a8577340a4e5adc5224/python_icap-0.2.0.tar.gz", hash = "sha256:627869ac29119074542de6ae95370914854fdb35ee0a13c8ae4c55847cfcb80e", size = 684516, upload-time = "2026-07-08T22:21:58.842Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/f2/b437a5e08b94dfc4cd8505cfc3e63922247e9ce3957707550ab08c514560/python_icap-0.2.0-py3-none-any.whl", hash = "sha256:b648d55b68abe387849a124d1b4a443801342a566afd2141f4515953ed4bd738", size = 70680, upload-time = "2026-07-08T22:21:57.307Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-magic"
|
||||
version = "0.4.27"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.23.4-alpine AS frontend-deps
|
||||
FROM --platform=$BUILDPLATFORM node:22.23.4-alpine AS frontend-deps
|
||||
|
||||
RUN apk upgrade --no-cache ca-certificates
|
||||
|
||||
@@ -54,7 +54,8 @@ RUN apk upgrade --no-cache libexpat && \
|
||||
apk add --no-cache --upgrade 'pcre2>=10.49-r0' 'tiff>=4.7.2-r0' \
|
||||
'libx11>=1.8.14-r0' 'ca-certificates>=20261020-r0' \
|
||||
'brotli-libs>=1.2.1-r0' && \
|
||||
apk del curl
|
||||
apk del curl && \
|
||||
rm -f /usr/share/nginx/html/50x.html
|
||||
USER nginx
|
||||
|
||||
# Un-privileged user running the application
|
||||
|
||||
@@ -16,6 +16,17 @@ server {
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /home.html /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
location = /home.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
@@ -23,6 +34,7 @@ server {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
|
||||
add_header Expires 0;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
}
|
||||
|
||||
# Optionally, handle 404 errors by redirecting to index.html
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Generated
+2
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.3.0",
|
||||
"@fontsource-variable/lexend": "5.3.0",
|
||||
@@ -21,7 +21,6 @@
|
||||
"@remixicon/react": "4.9.0",
|
||||
"@tanstack/react-query": "5.101.4",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.34.1",
|
||||
"i18next": "26.3.6",
|
||||
@@ -4563,12 +4562,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/crisp-sdk-web": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/crisp-sdk-web/-/crisp-sdk-web-1.1.2.tgz",
|
||||
"integrity": "sha512-qxSpiT4EGHnVEO6J/t1UURY6fg1ojgquLU0IIr7RNjZxXZs1R2e6AhAiFqxiGZ13Vb4o5VoUpATFU2TzX/ppOQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -29,7 +29,6 @@
|
||||
"@remixicon/react": "4.9.0",
|
||||
"@tanstack/react-query": "5.101.4",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.34.1",
|
||||
"i18next": "26.3.6",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
User-agent: GPTBot
|
||||
User-agent: OAI-SearchBot
|
||||
User-agent: ChatGPT-User
|
||||
User-agent: ClaudeBot
|
||||
User-agent: Claude-SearchBot
|
||||
User-agent: Claude-User
|
||||
User-agent: PerplexityBot
|
||||
User-agent: Perplexity-User
|
||||
User-agent: Google-Extended
|
||||
User-agent: Applebot-Extended
|
||||
User-agent: CCBot
|
||||
User-agent: Meta-ExternalAgent
|
||||
User-agent: Amazonbot
|
||||
User-agent: Bytespider
|
||||
Disallow: /
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@@ -13,7 +13,9 @@ export interface ApiConfig {
|
||||
flags_api_host?: string
|
||||
}
|
||||
support?: {
|
||||
id: string
|
||||
widget_script_url?: string
|
||||
widget_api_url?: string
|
||||
widget_channel?: string
|
||||
help_article_transcript: string
|
||||
help_article_recording: string
|
||||
help_article_more_tools: string
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
|
||||
import { useSupport } from '@/features/support/hooks/useSupport'
|
||||
import { useSyncUserPreferencesWithBackend } from '@/features/auth/api/useSyncUserPreferencesWithBackend'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
@@ -8,10 +7,9 @@ export const AppInitialization = () => {
|
||||
const { data } = useConfig()
|
||||
useSyncUserPreferencesWithBackend()
|
||||
|
||||
const { analytics = {}, support = {}, custom_css_url = '' } = data ?? {}
|
||||
const { analytics = {}, custom_css_url = '' } = data ?? {}
|
||||
|
||||
useAnalytics(analytics)
|
||||
useSupport(support)
|
||||
|
||||
useEffect(() => {
|
||||
if (custom_css_url) {
|
||||
|
||||
@@ -32,6 +32,7 @@ export type LogCode =
|
||||
| 'fullscreen_failure'
|
||||
| 'publish_sources_failure'
|
||||
| 'disconnect_failure'
|
||||
| 'support_widget_failure'
|
||||
| 'generic_failure'
|
||||
|
||||
export const reportError = (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { apiUrl } from '@/api/apiUrl'
|
||||
import { terminateSupportSession } from '@/features/support/hooks/useSupport'
|
||||
import { terminateAnalyticsSession } from '@/features/analytics/hooks/useAnalytics'
|
||||
|
||||
const logoutUrl = () => {
|
||||
@@ -8,6 +7,5 @@ const logoutUrl = () => {
|
||||
|
||||
export const logout = async () => {
|
||||
await terminateAnalyticsSession()
|
||||
terminateSupportSession()
|
||||
window.location.href = logoutUrl()
|
||||
}
|
||||
|
||||
+4
-9
@@ -2,23 +2,18 @@ import { RiQuestionLine } from '@remixicon/react'
|
||||
import { MenuItem } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe'
|
||||
import {
|
||||
useIsSupportEnabled,
|
||||
openSupportChat,
|
||||
} from '@/features/support/hooks/useSupport'
|
||||
import { useSupportWidget } from '@/features/support/hooks/useSupportWidget'
|
||||
|
||||
export const SupportMenuItem = () => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
|
||||
const isSupportEnabled = useIsSupportEnabled()
|
||||
const { isEnabled, open } = useSupportWidget()
|
||||
|
||||
if (!isSupportEnabled) {
|
||||
return
|
||||
}
|
||||
if (!isEnabled) return
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={menuRecipe({ icon: true, variant: 'dark' }).item}
|
||||
onAction={openSupportChat}
|
||||
onAction={open}
|
||||
>
|
||||
<RiQuestionLine size={20} />
|
||||
{t('support')}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiQuestionLine } from '@remixicon/react'
|
||||
import { Button } from '@/primitives'
|
||||
import { Spinner } from '@/primitives/Spinner'
|
||||
import { useSupportWidget } from '../hooks/useSupportWidget'
|
||||
|
||||
export const SupportButton = () => {
|
||||
const { t } = useTranslation('global', { keyPrefix: 'support' })
|
||||
const { isEnabled, isPending, open } = useSupportWidget()
|
||||
|
||||
if (!isEnabled) return null
|
||||
|
||||
return (
|
||||
<Button
|
||||
square
|
||||
variant="secondaryText"
|
||||
aria-label={t('buttonLabel')}
|
||||
tooltip={t('buttonLabel')}
|
||||
isPending={isPending}
|
||||
onPress={open}
|
||||
>
|
||||
{isPending ? <Spinner size={24} /> : <RiQuestionLine />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useLocation } from 'wouter'
|
||||
import { RiCheckboxCircleFill, RiCloseLine } from '@remixicon/react'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Button, Text } from '@/primitives'
|
||||
import { announceToScreenReader } from '@/stores/screenReaderAnnouncer'
|
||||
import { supportStore } from '@/stores/support'
|
||||
import { subscribeToSupportWidget } from '../utils/supportWidget'
|
||||
import { ToastDuration } from '@/features/notifications/NotificationDuration'
|
||||
import { StyledToast } from '@/features/notifications/components/StyledToast'
|
||||
import { StyledToastContainer } from '@/features/notifications/components/StyledToastContainer'
|
||||
|
||||
const hide = () => {
|
||||
supportStore.showSentConfirmation = false
|
||||
}
|
||||
|
||||
export const SupportWidget = () => {
|
||||
const { t } = useTranslation('global', { keyPrefix: 'support.sent' })
|
||||
const { showSentConfirmation } = useSnapshot(supportStore)
|
||||
const [location] = useLocation()
|
||||
|
||||
useEffect(() => subscribeToSupportWidget(), [])
|
||||
|
||||
useEffect(hide, [location])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showSentConfirmation) return
|
||||
announceToScreenReader(`${t('title')}. ${t('body')}`)
|
||||
const timeout = window.setTimeout(hide, ToastDuration.EXTRA_LONG)
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [showSentConfirmation, t])
|
||||
|
||||
if (!showSentConfirmation) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
top: '5rem',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 1000,
|
||||
})}
|
||||
>
|
||||
<StyledToastContainer>
|
||||
<StyledToast>
|
||||
<RiCheckboxCircleFill
|
||||
size={24}
|
||||
aria-hidden="true"
|
||||
className={css({ color: 'success.300', flexShrink: 0 })}
|
||||
/>
|
||||
<div>
|
||||
<Text margin={false} bold>
|
||||
{t('title')}
|
||||
</Text>
|
||||
<Text margin={false}>{t('body')}</Text>
|
||||
</div>
|
||||
<Button
|
||||
square
|
||||
size="sm"
|
||||
invisible
|
||||
aria-label={t('close')}
|
||||
onPress={hide}
|
||||
>
|
||||
<RiCloseLine color="white" />
|
||||
</Button>
|
||||
</StyledToast>
|
||||
</StyledToastContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { type ApiUser } from '@/features/auth/api/ApiUser'
|
||||
import { useUser } from '@/features/auth/api/useUser'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
type CrispSdk = (typeof import('crisp-sdk-web'))['Crisp']
|
||||
|
||||
let crisp: CrispSdk | undefined
|
||||
let crispPromise: Promise<CrispSdk> | undefined
|
||||
|
||||
const loadCrisp = (): Promise<CrispSdk> => {
|
||||
crispPromise ??= import('crisp-sdk-web')
|
||||
.then((module) => {
|
||||
crisp = module.Crisp
|
||||
return module.Crisp
|
||||
})
|
||||
.catch((error) => {
|
||||
crispPromise = undefined
|
||||
throw error
|
||||
})
|
||||
|
||||
return crispPromise
|
||||
}
|
||||
|
||||
export const openSupportChat = () => {
|
||||
if (!crisp?.isCrispInjected()) return
|
||||
crisp.chat.open()
|
||||
}
|
||||
|
||||
export const initializeSupportSession = (user: ApiUser) => {
|
||||
if (!crisp?.isCrispInjected()) return
|
||||
|
||||
const { id, email } = user
|
||||
crisp.setTokenId(`meet-${id}`)
|
||||
if (email) crisp.user.setEmail(email)
|
||||
}
|
||||
|
||||
export const terminateSupportSession = () => {
|
||||
if (!crisp?.isCrispInjected()) return
|
||||
|
||||
crisp.setTokenId()
|
||||
crisp.session.reset()
|
||||
}
|
||||
|
||||
export type useSupportProps = {
|
||||
id?: string
|
||||
isDisabled?: boolean
|
||||
}
|
||||
|
||||
const IDLE_TIMEOUT_MS = 10_000
|
||||
|
||||
const scheduleWhenIdle = (callback: () => void): (() => void) => {
|
||||
if (typeof window.requestIdleCallback === 'function') {
|
||||
const handle = window.requestIdleCallback(callback, {
|
||||
timeout: IDLE_TIMEOUT_MS,
|
||||
})
|
||||
return () => window.cancelIdleCallback(handle)
|
||||
}
|
||||
|
||||
const handle = window.setTimeout(callback, 1)
|
||||
return () => window.clearTimeout(handle)
|
||||
}
|
||||
|
||||
export const useSupport = ({ id, isDisabled }: useSupportProps) => {
|
||||
const { user } = useUser()
|
||||
const [isInjected, setIsInjected] = useState(
|
||||
() => crisp?.isCrispInjected() ?? false
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || isDisabled) return
|
||||
|
||||
if (crisp?.isCrispInjected()) {
|
||||
setIsInjected(true)
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const cancelIdle = scheduleWhenIdle(() => {
|
||||
void loadCrisp()
|
||||
.then((sdk) => {
|
||||
if (cancelled) return
|
||||
|
||||
if (!sdk.isCrispInjected()) {
|
||||
sdk.configure(id)
|
||||
sdk.setHideOnMobile(true)
|
||||
}
|
||||
|
||||
setIsInjected(true)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
console.error('Failed to initialize support chat', error)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
cancelIdle()
|
||||
}
|
||||
}, [id, isDisabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !isInjected || isDisabled) return
|
||||
initializeSupportSession(user)
|
||||
}, [user, isInjected, isDisabled])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Some users block the chat widget, so check its availability safely.
|
||||
const isCrispAvailable = () => {
|
||||
try {
|
||||
return !!window?.$crisp?.is
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const useIsSupportEnabled = () => {
|
||||
const { data } = useConfig()
|
||||
return !!data?.support?.id && isCrispAvailable()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { useUser } from '@/features/auth/api/useUser'
|
||||
import { reportError } from '@/features/analytics/telemetry'
|
||||
import { announceToScreenReader } from '@/stores/screenReaderAnnouncer'
|
||||
import { supportStore } from '@/stores/support'
|
||||
import { openSupportWidget } from '../utils/supportWidget'
|
||||
|
||||
export const useSupportWidget = () => {
|
||||
const { t, i18n } = useTranslation('global', { keyPrefix: 'support' })
|
||||
const { data } = useConfig()
|
||||
const { user } = useUser()
|
||||
const { status } = useSnapshot(supportStore)
|
||||
|
||||
const {
|
||||
widget_script_url: scriptUrl,
|
||||
widget_api_url: api,
|
||||
widget_channel: channel,
|
||||
} = data?.support ?? {}
|
||||
const isEnabled = !!(scriptUrl && api && channel)
|
||||
|
||||
const open = () => {
|
||||
if (!isEnabled) return
|
||||
|
||||
openSupportWidget(scriptUrl, {
|
||||
api,
|
||||
channel,
|
||||
locale: i18n.language,
|
||||
email: user?.email,
|
||||
}).catch((error) => {
|
||||
reportError('support_widget_failure', error)
|
||||
announceToScreenReader(t('openError'), 'assertive')
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isEnabled,
|
||||
isPending: status === 'pending',
|
||||
open,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { supportStore } from '@/stores/support'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_lasuite_widget?: unknown[]
|
||||
}
|
||||
}
|
||||
|
||||
type WidgetOptions = {
|
||||
api: string
|
||||
channel: string
|
||||
locale: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
const ROOT_ID = 'lasuite-widget-support-root'
|
||||
const OPENED_EVENT = 'lasuite-widget-support-opened'
|
||||
const SUCCESS_EVENT = 'lasuite-widget-support-success'
|
||||
const CLOSED_EVENT = 'lasuite-widget-support-closed'
|
||||
const TIMEOUT_MS = 10_000
|
||||
|
||||
const WIDGET_CSS = `@media (min-width: 48em) {
|
||||
[role='dialog'] {
|
||||
top: 50%;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
[role='status'] {
|
||||
display: none;
|
||||
}`
|
||||
|
||||
const styleWidget = () => {
|
||||
const shadowRoot = document.getElementById(ROOT_ID)?.shadowRoot
|
||||
if (!shadowRoot) return
|
||||
const style = document.createElement('style')
|
||||
style.textContent = WIDGET_CSS
|
||||
shadowRoot.appendChild(style)
|
||||
}
|
||||
|
||||
let returnFocusTo: HTMLElement | null = null
|
||||
|
||||
// The widget removes the form without moving focus, which drops it on <body>.
|
||||
const restoreFocus = () => {
|
||||
const activeEl = document.activeElement
|
||||
const isFocusLost = activeEl === document.body || activeEl?.id === ROOT_ID
|
||||
if (isFocusLost && returnFocusTo?.isConnected) {
|
||||
returnFocusTo.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
|
||||
export const subscribeToSupportWidget = () => {
|
||||
const onOpened = () => {
|
||||
styleWidget()
|
||||
supportStore.status = 'open'
|
||||
}
|
||||
const onSuccess = () => {
|
||||
supportStore.showSentConfirmation = true
|
||||
restoreFocus()
|
||||
}
|
||||
const onClosed = () => {
|
||||
supportStore.status = 'closed'
|
||||
restoreFocus()
|
||||
returnFocusTo = null
|
||||
}
|
||||
|
||||
document.addEventListener(OPENED_EVENT, onOpened)
|
||||
document.addEventListener(SUCCESS_EVENT, onSuccess)
|
||||
document.addEventListener(CLOSED_EVENT, onClosed)
|
||||
return () => {
|
||||
document.removeEventListener(OPENED_EVENT, onOpened)
|
||||
document.removeEventListener(SUCCESS_EVENT, onSuccess)
|
||||
document.removeEventListener(CLOSED_EVENT, onClosed)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveTrigger = () => {
|
||||
const activeEl = document.activeElement as HTMLElement | null
|
||||
if (activeEl?.getAttribute('role') === 'menuitem') {
|
||||
return document.querySelector<HTMLElement>('#room-options-trigger')
|
||||
}
|
||||
return activeEl
|
||||
}
|
||||
|
||||
let scriptPromise: Promise<void> | undefined
|
||||
|
||||
const loadScript = (src: string): Promise<void> => {
|
||||
scriptPromise ??= new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
const timeout = window.setTimeout(
|
||||
() => fail(new Error(`Timed out loading ${src}`)),
|
||||
TIMEOUT_MS
|
||||
)
|
||||
const fail = (error: Error) => {
|
||||
window.clearTimeout(timeout)
|
||||
script.remove()
|
||||
scriptPromise = undefined
|
||||
reject(error)
|
||||
}
|
||||
|
||||
script.src = src
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
window.clearTimeout(timeout)
|
||||
resolve()
|
||||
}
|
||||
script.onerror = () => fail(new Error(`Failed to load ${src}`))
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
|
||||
return scriptPromise
|
||||
}
|
||||
|
||||
const initWidget = (options: WidgetOptions): Promise<void> =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const onOpened = () => {
|
||||
window.clearTimeout(timeout)
|
||||
resolve()
|
||||
}
|
||||
const timeout = window.setTimeout(() => {
|
||||
document.removeEventListener(OPENED_EVENT, onOpened)
|
||||
reject(new Error('Support widget did not open'))
|
||||
}, TIMEOUT_MS)
|
||||
|
||||
document.addEventListener(OPENED_EVENT, onOpened, { once: true })
|
||||
window._lasuite_widget?.push(['support', 'init', options])
|
||||
})
|
||||
|
||||
export const openSupportWidget = async (
|
||||
scriptUrl: string,
|
||||
options: WidgetOptions
|
||||
) => {
|
||||
if (supportStore.status !== 'closed') return
|
||||
|
||||
supportStore.status = 'pending'
|
||||
returnFocusTo = resolveTrigger()
|
||||
try {
|
||||
await loadScript(scriptUrl)
|
||||
await initWidget(options)
|
||||
} catch (error) {
|
||||
supportStore.status = 'closed'
|
||||
returnFocusTo = null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { HStack, Stack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Text } from '@/primitives'
|
||||
import { SettingsButton } from '@/features/settings'
|
||||
import { SupportButton } from '@/features/support/components/SupportButton'
|
||||
import { useUser } from '@/features/auth/api/useUser'
|
||||
import { useMatchesRoute } from '@/navigation/useMatchesRoute'
|
||||
import { FeedbackBanner } from '@/components/FeedbackBanner'
|
||||
@@ -153,6 +154,7 @@ export const Header = () => {
|
||||
</header>
|
||||
<nav>
|
||||
<Stack gap={1} direction="row" align="center">
|
||||
<SupportButton />
|
||||
{isLoggedIn === false &&
|
||||
!isHome &&
|
||||
!isLegalTerms &&
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSnapshot } from 'valtio'
|
||||
import { Footer } from '@/layout/Footer'
|
||||
import { ScreenReaderAnnouncer } from '@/primitives'
|
||||
import { SkipLink, MAIN_CONTENT_ID } from './SkipLink'
|
||||
import { SupportWidget } from '@/features/support/components/SupportWidget'
|
||||
|
||||
export type Layout = 'fullpage' | 'centered'
|
||||
|
||||
@@ -46,6 +47,7 @@ export const Layout = ({ children }: { children: ReactNode }) => {
|
||||
})}
|
||||
>
|
||||
<ScreenReaderAnnouncer />
|
||||
<SupportWidget />
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
},
|
||||
"selected": "ausgewählt",
|
||||
"submit": "OK",
|
||||
"support": {
|
||||
"buttonLabel": "Support kontaktieren",
|
||||
"openError": "Das Kontaktformular konnte nicht geöffnet werden. Bitte versuchen Sie es später erneut.",
|
||||
"sent": {
|
||||
"title": "Nachricht gesendet",
|
||||
"body": "Das Support-Team hat Ihre Nachricht erhalten.",
|
||||
"close": "Schließen"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"links": {
|
||||
"legifrance": "legifrance.gouv.fr",
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
},
|
||||
"selected": "selected",
|
||||
"submit": "OK",
|
||||
"support": {
|
||||
"buttonLabel": "Contact support",
|
||||
"openError": "The contact form could not be opened. Please try again later.",
|
||||
"sent": {
|
||||
"title": "Message sent",
|
||||
"body": "The support team has received your message.",
|
||||
"close": "Close"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"links": {
|
||||
"legifrance": "legifrance.gouv.fr",
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
},
|
||||
"selected": "seleccionado",
|
||||
"submit": "Aceptar",
|
||||
"support": {
|
||||
"buttonLabel": "Contactar con el soporte",
|
||||
"openError": "No se ha podido abrir el formulario de contacto. Inténtelo de nuevo más tarde.",
|
||||
"sent": {
|
||||
"title": "Mensaje enviado",
|
||||
"body": "El equipo de soporte ha recibido su mensaje.",
|
||||
"close": "Cerrar"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"links": {
|
||||
"legifrance": "legifrance.gouv.fr",
|
||||
|
||||
@@ -27,6 +27,15 @@
|
||||
},
|
||||
"selected": "sélectionné",
|
||||
"submit": "OK",
|
||||
"support": {
|
||||
"buttonLabel": "Contacter l'aide",
|
||||
"openError": "Impossible d'ouvrir le formulaire de contact. Réessayez plus tard.",
|
||||
"sent": {
|
||||
"title": "Message envoyé",
|
||||
"body": "L'équipe support a bien reçu votre message.",
|
||||
"close": "Fermer"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"links": {
|
||||
"legifrance": "legifrance.gouv.fr",
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
},
|
||||
"selected": "geselecteerd",
|
||||
"submit": "OK",
|
||||
"support": {
|
||||
"buttonLabel": "Contact ondersteuning",
|
||||
"openError": "Het contactformulier kon niet worden geopend. Probeer het later opnieuw.",
|
||||
"sent": {
|
||||
"title": "Bericht verzonden",
|
||||
"body": "Het supportteam heeft je bericht ontvangen.",
|
||||
"close": "Sluiten"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"links": {
|
||||
"legifrance": "legifrance.gouv.fr",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { proxy } from 'valtio'
|
||||
|
||||
export type SupportWidgetStatus = 'closed' | 'pending' | 'open'
|
||||
|
||||
type State = {
|
||||
status: SupportWidgetStatus
|
||||
showSentConfirmation: boolean
|
||||
}
|
||||
|
||||
export const supportStore = proxy<State>({
|
||||
status: 'closed',
|
||||
showSentConfirmation: false,
|
||||
})
|
||||
@@ -72,14 +72,6 @@ main#main-content :is(h1, h2, h3)[tabindex='-1']:focus {
|
||||
}
|
||||
}
|
||||
|
||||
body:has(.lk-video-conference) #crisp-chatbox-button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body:has(.lk-video-conference) #crisp-chatbox > * > div[role='button'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html:has(.lk-video-conference) {
|
||||
scrollbar-gutter: auto !important;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { defineConfig, loadEnv, type Plugin } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
@@ -25,6 +26,23 @@ if (mediapipeVersion !== livekitMediapipeVersion) {
|
||||
)
|
||||
}
|
||||
|
||||
const NOINDEX_TAG = '<meta name="robots" content="noindex, nofollow" />'
|
||||
|
||||
const homePage = (): Plugin => ({
|
||||
name: 'home-page',
|
||||
apply: 'build',
|
||||
writeBundle({ dir = 'dist' }) {
|
||||
const html = readFileSync(join(dir, 'index.html'), 'utf-8')
|
||||
if (!html.includes(NOINDEX_TAG)) {
|
||||
throw new Error(
|
||||
`${dir}/index.html does not contain ${NOINDEX_TAG}, so home.html ` +
|
||||
`cannot be generated from it. The two must stay in sync.`
|
||||
)
|
||||
}
|
||||
writeFileSync(join(dir, 'home.html'), html.replace(NOINDEX_TAG, ''))
|
||||
},
|
||||
})
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
@@ -66,6 +84,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
],
|
||||
}),
|
||||
homePage(),
|
||||
env.VITE_ANALYZE === 'true' &&
|
||||
visualizer({
|
||||
open: true,
|
||||
|
||||
@@ -163,7 +163,7 @@ backend:
|
||||
LIVEKIT_DEFAULT_VIDEO_CODEC: vp9
|
||||
FRONTEND_IDLE_DISCONNECT_WARNING_DELAY: 9000
|
||||
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
|
||||
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
|
||||
FRONTEND_SUPPORT: "{'help_article_transcript': 'https://docs.numerique.gouv.fr/docs/ba213749-2379-421c-b2ea-e4bd0840ea9b', 'help_article_recording': 'https://docs.numerique.gouv.fr/docs/a4e12e41-2974-472c-84a9-c00d12c10769', 'help_article_more_tools': 'https://docs.numerique.gouv.fr/docs/75fa98c7-5af1-433e-a611-e38143db26d0'}"
|
||||
FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}"
|
||||
FRONTEND_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/7c5bd65d-3c21-486f-bce1-26e0a921d642/"
|
||||
FRONTEND_TECHNICAL_DOCUMENTATION_URL: "https://docs.numerique.gouv.fr/docs/f2baa1b9-f29e-4d58-959d-65d4376fc6b8/"
|
||||
|
||||
@@ -51,6 +51,7 @@ data:
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
|
||||
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
|
||||
|
||||
@@ -84,6 +85,17 @@ data:
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /home.html /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
location = /home.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
@@ -91,6 +103,7 @@ data:
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
|
||||
add_header Expires 0;
|
||||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||
}
|
||||
|
||||
# Optionally, handle 404 errors by redirecting to index.html
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.36.3",
|
||||
"version": "1.37.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -5,7 +5,7 @@ RUN apk add --no-cache --upgrade 'ca-certificates>=20261020-r0'
|
||||
|
||||
# Install ffmpeg for audio/video processing (format conversion, extraction, compression)
|
||||
# See summary/core/file_service.py for usage.
|
||||
RUN apk add --no-cache "ffmpeg=8.1.2-r0" 'brotli-libs>=1.2.1-r0'
|
||||
RUN apk add --no-cache "ffmpeg=8.1.3-r0" 'brotli-libs>=1.2.1-r0'
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.36.3"
|
||||
version = "1.37.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"certifi>=2026.10.19",
|
||||
|
||||
Generated
+1
-1
@@ -1484,7 +1484,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "summary"
|
||||
version = "1.36.3"
|
||||
version = "1.37.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "boto3" },
|
||||
|
||||
Reference in New Issue
Block a user