feat: update Docker configuration and add deployment documentation for Coolify

This commit is contained in:
hamed
2026-06-27 21:27:58 +03:30
parent 29badf8d68
commit e966e11807
8 changed files with 233 additions and 21 deletions
+4
View File
@@ -23,6 +23,10 @@
/phpstan.neon /phpstan.neon
# Local env & secrets # Local env & secrets
# NOTE: .env IS kept in the image — Symfony's Dotenv::bootEnv requires it to
# exist. Its APP_ENV=dev default is harmless in prod: compose sets the real
# APP_ENV=prod env var, and Dotenv never overwrites an already-set var
# (provided FPM clear_env=no, see docker/php/zz-pool.conf).
.env.local .env.local
.env.*.local .env.*.local
/config/jwt/*.pem /config/jwt/*.pem
+2 -1
View File
@@ -15,7 +15,7 @@ RUN composer install \
# ============================================================ # ============================================================
# Stage 2 — Frontend assets (Webpack Encore / React 19) # Stage 2 — Frontend assets (Webpack Encore / React 19)
# ============================================================ # ============================================================
FROM node:20-alpine AS assets FROM node:22-alpine AS assets
WORKDIR /app WORKDIR /app
# vendor is required: package.json references "@symfony/ux-react": "file:vendor/symfony/ux-react/assets" # vendor is required: package.json references "@symfony/ux-react": "file:vendor/symfony/ux-react/assets"
COPY --from=vendor /app/vendor ./vendor COPY --from=vendor /app/vendor ./vendor
@@ -50,6 +50,7 @@ COPY --from=assets /app/public/build ./public/build
# Container configuration. # Container configuration.
COPY docker/php/php.ini /usr/local/etc/php/conf.d/zz-app.ini COPY docker/php/php.ini /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/php/zz-pool.conf /usr/local/etc/php-fpm.d/zz-pool.conf
COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
+7
View File
@@ -0,0 +1,7 @@
; Production FPM pool overrides for ClinicPro.
; Default www.conf ships clear_env = yes, which strips the container
; environment (APP_ENV=prod, DATABASE_URL, ...) from PHP worker processes.
; Without this, PHP falls back to the baked .env (APP_ENV=dev) and boots the
; wrong environment. Keep the container env so compose-provided vars reach PHP.
[www]
clear_env = no
+167
View File
@@ -0,0 +1,167 @@
# راهنمای دیپلوی ClinicPro (Coolify + Docker Compose)
این راهنما برای دیپلوی بک‌اند ClinicPro روی **Coolify** با استفاده از `docker-compose.yml` نوشته شده.
> توجه: این `docker-compose.yml` فقط برای **production** است. محیط لوکال از `compose.yaml` خودِ ddev استفاده می‌کند — این دو را با هم اشتباه نگیر.
---
## معماری استک
`docker-compose.yml` پنج سرویس بالا می‌آورد:
| سرویس | نقش | نکته |
|---|---|---|
| `app` | PHP-FPM + Nginx (وب) | تنها سرویسی که `RUN_INIT=1` دارد؛ مهاجرت DB و تولید کلید JWT را اجرا می‌کند. دامنه‌ها را به این سرویس (پورت 80) وصل کن. |
| `worker-async` | مصرف‌کننده صف async (SMS و کارهای async) | `messenger:consume async` |
| `worker-scheduler` | زمان‌بند | هر ۱ دقیقه نوبت‌های پرداخت‌نشده را منقضی می‌کند |
| `mariadb` | پایگاه‌داده MariaDB 11.8 | healthcheck دارد؛ بقیه منتظرش می‌مانند |
| `redis` | صف Messenger + کش | `appendonly yes` (ماندگار) |
**ولوم‌های ماندگار** (داده‌ها در ری‌دیپلوی حفظ می‌شوند):
- `jwt_keys` → کلیدهای JWT
- `uploads_public` و `uploads_var` → فایل‌های آپلودی
- `mariadb_data` → داده DB
- `redis_data` → داده Redis
---
## پیش‌نیازها
- نمونه‌ی Coolify در حال اجرا با Traefik (پیش‌فرض Coolify).
- ریپوی Git متصل به Coolify.
- رکوردهای DNS برای دامنه‌ی API و همه‌ی دامنه‌های فرانت‌اند که به سرور اشاره کنند.
---
## مرحله ۱ — ساخت منبع (Resource) در Coolify
1. **New Resource → Docker Compose** (Build Pack: `Docker Compose`).
2. ریپو و برنچ را انتخاب کن.
3. فیلد **Compose file** را روی `docker-compose.yml` بگذار.
4. `networks:` سفارشی تعریف **نکن** — شبکه را Coolify مدیریت می‌کند؛ شبکه‌ی سفارشی روتینگ Traefik را می‌شکند.
---
## مرحله ۲ — دامنه‌ها
همه‌ی دامنه‌های سرو شونده را به سرویس **`app`** (پورت 80) اختصاص بده — هم دامنه‌ی API و هم همه‌ی دامنه‌های فرانت‌اند. Coolify لیست دامنه‌ی جدا‌شده با کاما را روی یک سرویس قبول می‌کند و TLS را خودش صادر می‌کند.
> اجازه‌دادن CORS و host فرانت‌اندها از طریق متغیرهای `CORS_ALLOW_ORIGIN` / `ALLOWED_FRONTEND_HOSTS` کنترل می‌شود، نه دامنه‌ی Coolify.
---
## مرحله ۳ — متغیرهای محیطی
از `.env.coolify.example` کپی کن و در تب **Environment Variables** منبع Coolify بگذار.
فقط متغیرهایی که در `docker-compose.yml` به‌صورت `${...}` ارجاع شده‌اند لازم‌اند. `DATABASE_URL` / `MESSENGER_TRANSPORT_DSN` / `REDIS_URL` داخل خود compose از روی نام سرویس‌ها ساخته می‌شوند.
### اسرار (الزامی — قبل از اولین دیپلوی)
```bash
APP_SECRET= # php -r "echo bin2hex(random_bytes(32));"
JWT_PASSPHRASE= # openssl rand -hex 32 (باید قبل از اولین استارت موجود باشد؛ کلید JWT با همین ساخته می‌شود)
DB_PASSWORD= # رمز کاربر DB اپلیکیشن
DB_ROOT_PASSWORD= # رمز root مریادی‌بی
```
> ⚠️ `JWT_PASSPHRASE` را بعد از اولین دیپلوی عوض نکن — کلید JWT یک‌بار با همین passphrase تولید و روی ولوم `jwt_keys` ماندگار می‌شود. تغییرش همه‌ی توکن‌ها را می‌شکند.
در Coolify می‌توانی به‌جای هاردکد از magic var استفاده کنی:
```bash
DB_PASSWORD=${SERVICE_PASSWORD_DB}
APP_SECRET=${SERVICE_HEX_APPSECRET}
```
### دامنه‌ها و CORS
```bash
APP_BASE_URL=https://api.nobat724.com # دامنه‌ی خودِ بک‌اند (برای callback پرداخت و URLهای مطلق)
```
`ALLOWED_FRONTEND_HOSTS` و `CORS_ALLOW_ORIGIN` از `docker/frontend-domains.json` **تولید** می‌شوند. برای اضافه/حذف دامنه‌ی شهر:
```bash
# آن فایل را ویرایش کن، سپس:
php docker/gen-cors-env.php # روی سرور
# یا لوکال:
ddev exec php docker/gen-cors-env.php
```
خروجی را در Coolify جایگزین کن.
### ریورس‌پراکسی
```bash
TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1
```
تا Symfony به هدرهای `X-Forwarded-*` ترافیک اعتماد کند.
### api.ir (استعلام هویت — Shahkar / IbanMatch)
```bash
API_IR_BASE_URL=https://s.api.ir
API_IR_TOKEN= # خالی => fail-closed (تأیید نماینده رد می‌شود)
```
### چیزهایی که env لازم ندارند
- کلیدهای SMS (kavenegar/rangineh) و درگاه پرداخت (mellat/sep) از **DB ("تنظیمات سایت")** خوانده می‌شوند، نه env.
- `REFRESH_TOKEN_TTL` / `OTP_TTL` / `MAX_FILE_SIZE_BYTES` در خود compose ثابت‌اند.
---
## مرحله ۴ — دیپلوی
روی **Deploy** بزن. در اولین استارت به‌صورت خودکار این‌ها اتفاق می‌افتد (`entrypoint.sh` + `RUN_INIT=1` روی سرویس `app`):
1. مالکیت `var`, `public/uploads`, `config/jwt` به `www-data` داده می‌شود.
2. کلید JWT اگر روی ولوم نباشد ساخته می‌شود (`--skip-if-exists`).
3. کش prod پاک و warmup می‌شود.
4. مهاجرت‌های DB با `--all-or-nothing` اعمال می‌شوند (ترنزکشن).
> ورکرها `RUN_INIT=0` دارند تا مهاجرت/تولید کلید با هم تداخل نکنند.
---
## مرحله ۵ — پس از اولین دیپلوی
### ساخت اولین ادمین
```bash
# داخل کانتینر سرویس app
php bin/console app:create-admin
```
### بررسی سلامت
- healthcheck سرویس `app`: `php fsockopen 127.0.0.1:80`.
- Swagger: `https://<APP_BASE_URL>/api/doc`
- پنل ادمین: `https://<APP_BASE_URL>/admin`
---
## دیپلوی‌های بعدی
push روی برنچ متصل (یا Deploy دستی). در هر ری‌دیپلوی:
- ایمیج دوباره build می‌شود (vendor + اسمبل فرانت‌اند multi-stage).
- مهاجرت‌های جدید روی استارت `app` اعمال می‌شوند.
- ولوم‌ها حفظ می‌شوند (DB، آپلودها، کلیدهای JWT، Redis سالم می‌مانند).
---
## عیب‌یابی
| نشانه | علت محتمل |
|---|---|
| ارورهای CORS در فرانت | `CORS_ALLOW_ORIGIN` با دامنه نمی‌خواند؛ از `gen-cors-env.php` بازتولید کن |
| `app` بالا نمی‌آید، منتظر DB می‌ماند | healthcheck `mariadb` رد نشده؛ لاگ mariadb را ببین |
| ۴۰۱/توکن نامعتبر بعد از ری‌دیپلوی | `JWT_PASSPHRASE` تغییر کرده یا ولوم `jwt_keys` پاک شده |
| IPها/HTTPS اشتباه پشت پراکسی | `TRUSTED_PROXIES` ست نشده |
| مهاجرت اجرا نشد | فقط `app` با `RUN_INIT=1` اجرا می‌کند؛ مطمئن شو override نشده |
| تأیید نماینده رد می‌شود | `API_IR_TOKEN` خالی است (fail-closed) |
+4 -6
View File
@@ -49,12 +49,6 @@
"typescript": "^5.0.0", "typescript": "^5.0.0",
"webpack": "^5.72", "webpack": "^5.72",
"webpack-cli": "^6.0.0" "webpack-cli": "^6.0.0"
},
"optionalDependencies": {
"@tailwindcss/oxide-darwin-arm64": "^4.3.0",
"@tailwindcss/oxide-linux-arm64-gnu": "^4.3.0",
"lightningcss-darwin-arm64": "^1.32.0",
"lightningcss-linux-arm64-gnu": "^1.32.0"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@@ -15902,6 +15896,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -15969,6 +15964,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -20035,6 +20031,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -20118,6 +20115,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
-6
View File
@@ -44,12 +44,6 @@
"zod": "^3.0.0", "zod": "^3.0.0",
"zustand": "^5.0.0" "zustand": "^5.0.0"
}, },
"optionalDependencies": {
"@tailwindcss/oxide-darwin-arm64": "^4.3.0",
"@tailwindcss/oxide-linux-arm64-gnu": "^4.3.0",
"lightningcss-darwin-arm64": "^1.32.0",
"lightningcss-linux-arm64-gnu": "^1.32.0"
},
"license": "UNLICENSED", "license": "UNLICENSED",
"private": true, "private": true,
"scripts": { "scripts": {
+45 -4
View File
@@ -59,7 +59,7 @@
<svg viewbox="0 0 24 24" fill="none"><path d="M12 21s-7-4.3-7-9.5A4 4 0 0 1 12 8a4 4 0 0 1 7 3.5C19 16.7 12 21 12 21Z" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg> <svg viewbox="0 0 24 24" fill="none"><path d="M12 21s-7-4.3-7-9.5A4 4 0 0 1 12 8a4 4 0 0 1 7 3.5C19 16.7 12 21 12 21Z" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg>
</span> </span>
<div> <div>
<b class="mono">۹۵٪</b> <b style="font-variant-numeric:tabular-nums">۹۵٪</b>
<span>رضایت بیماران</span> <span>رضایت بیماران</span>
</div> </div>
</div> </div>
@@ -68,7 +68,7 @@
<svg viewbox="0 0 24 24" fill="none"><path d="M8 7V5m8 2V5M4 9h16M5 7h14v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7Z" stroke="currentColor" stroke-width="1.9"/></svg> <svg viewbox="0 0 24 24" fill="none"><path d="M8 7V5m8 2V5M4 9h16M5 7h14v12a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7Z" stroke="currentColor" stroke-width="1.9"/></svg>
</span> </span>
<div> <div>
<b class="mono">۳۸</b> <b style="font-variant-numeric:tabular-nums">۳۸</b>
<span>نوبت امروز</span> <span>نوبت امروز</span>
</div> </div>
</div> </div>
@@ -553,10 +553,12 @@ var btn2 = document.getElementById('openRegModal2');
if (btn1) if (btn1)
btn1.addEventListener('click', openRegModal); btn1.addEventListener('click', openRegModal);
if (btn2) if (btn2)
btn2.addEventListener('click', openRegModal); btn2.addEventListener('click', openRegModal);
document.querySelectorAll('.reg-card').forEach(function (card) { document.querySelectorAll('.reg-card').forEach(function (card) {
card.addEventListener('click', function () { card.addEventListener('click', function () {
document.querySelectorAll('.reg-card').forEach(function (c) { document.querySelectorAll('.reg-card').forEach(function (c) {
@@ -567,6 +569,7 @@ var radio = card.querySelector('input[type=radio]');
if (radio) if (radio)
radio.checked = true; radio.checked = true;
selectedType = card.dataset.type; selectedType = card.dataset.type;
submitBtn.disabled = false; submitBtn.disabled = false;
submitBtn.style.opacity = '1'; submitBtn.style.opacity = '1';
@@ -582,6 +585,7 @@ var msg = document.getElementById('regMsg');
if (! selectedType) if (! selectedType)
return; return;
if (! name) { if (! name) {
showMsg('نام را وارد کنید', 'error'); showMsg('نام را وارد کنید', 'error');
return; return;
@@ -613,7 +617,9 @@ return r.json();
if (data.success) { if (data.success) {
showMsg('درخواست شما ثبت شد. پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود.', 'success'); showMsg('درخواست شما ثبت شد. پس از بررسی، اطلاعات ورود از طریق SMS ارسال می‌شود.', 'success');
submitBtn.textContent = 'ارسال شد ✓'; submitBtn.textContent = 'ارسال شد ✓';
setTimeout(function() { closeRegModal(); }, 2200); setTimeout(function () {
closeRegModal();
}, 2200);
} else { } else {
var errMsg = (data.errors && data.errors[0]) ? data.errors[0].message : 'خطایی رخ داد'; var errMsg = (data.errors && data.errors[0]) ? data.errors[0].message : 'خطایی رخ داد';
showMsg(errMsg, 'error'); showMsg(errMsg, 'error');
@@ -663,6 +669,8 @@ msg.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca
<svg viewbox="0 0 24 24" fill="none"><path d="M4 20l1.4-4A8 8 0 1 1 9 18.6L4 20Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg> <svg viewbox="0 0 24 24" fill="none"><path d="M4 20l1.4-4A8 8 0 1 1 9 18.6L4 20Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
</a> </a>
</div> </div>
<!-- نماد اعتماد الکترونیکی — بعد از لود کامل صفحه تزریق می‌شود (footer JS) -->
<div id="enamad-seal" style="margin-top:18px"></div>
</div> </div>
<div class="footer-col"> <div class="footer-col">
@@ -684,7 +692,7 @@ msg.style.border = type === 'success' ? '1px solid #bbf7d0' : '1px solid #fecaca
</div> </div>
<div class="fl"> <div class="fl">
<svg viewbox="0 0 24 24" fill="none"><path d="M4 6.5C4 5 5 4 6.5 4h2c.6 0 1.1.4 1.3 1l1 3c.2.6 0 1.2-.5 1.6L9 11c1 2 2.5 3.5 4.5 4.5l1.4-1.3c.4-.4 1-.6 1.6-.4l3 1c.6.2 1 .7 1 1.3v2c0 1.5-1 2.5-2.5 2.4C10.5 20 4 13.5 4 6.5Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg> <svg viewbox="0 0 24 24" fill="none"><path d="M4 6.5C4 5 5 4 6.5 4h2c.6 0 1.1.4 1.3 1l1 3c.2.6 0 1.2-.5 1.6L9 11c1 2 2.5 3.5 4.5 4.5l1.4-1.3c.4-.4 1-.6 1.6-.4l3 1c.6.2 1 .7 1 1.3v2c0 1.5-1 2.5-2.5 2.4C10.5 20 4 13.5 4 6.5Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<a href="tel:07491012178" class="mono" dir="ltr">۰۷۴۹۱۰۱۲۱۷۸</a> <a href="tel:07491012178" style="font-variant-numeric:tabular-nums" dir="ltr">۰۷۴۹۱۰۱۲۱۷۸</a>
</div> </div>
<div class="fl"> <div class="fl">
<svg viewbox="0 0 24 24" fill="none"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="m4 7 8 5 8-5" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg> <svg viewbox="0 0 24 24" fill="none"><rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="m4 7 8 5 8-5" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
@@ -764,6 +772,7 @@ navLinks.style.boxShadow = '0 16px 30px oklch(0.4 0.08 285 / 0.1)';
}); });
var sections = [ var sections = [
'hero', 'hero',
'features', 'features',
@@ -780,6 +789,7 @@ var el = document.getElementById(id);
if (el && el.offsetTop <= y) if (el && el.offsetTop <= y)
cur = id; cur = id;
}); });
links.forEach(function (a) { links.forEach(function (a) {
a.classList.toggle('active', a.getAttribute('href') === '#' + cur); a.classList.toggle('active', a.getAttribute('href') === '#' + cur);
@@ -805,6 +815,7 @@ reveals.forEach(function (el) {
if (inView(el)) if (inView(el))
reveal(el); reveal(el);
}); });
if ('IntersectionObserver' in window) { if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (es) { var io = new IntersectionObserver(function (es) {
@@ -822,6 +833,7 @@ reveals.forEach(function (el) {
if (! el.classList.contains('in')) if (! el.classList.contains('in'))
io.observe(el); io.observe(el);
}); });
} }
var ticking = false; var ticking = false;
@@ -829,12 +841,14 @@ function sweep() {
if (ticking) if (ticking)
return; return;
ticking = true; ticking = true;
requestAnimationFrame(function () { requestAnimationFrame(function () {
reveals.forEach(function (el) { reveals.forEach(function (el) {
if (inView(el)) if (inView(el))
reveal(el); reveal(el);
}); });
ticking = false; ticking = false;
}); });
@@ -845,6 +859,33 @@ setTimeout(function () {
reveals.forEach(reveal); reveals.forEach(reveal);
}, 2800); }, 2800);
} }
// نماد اعتماد الکترونیکی (Enamad) — فقط بعد از لود کامل صفحه تزریق می‌شود تا
// درخواست تصویرِ دامنه‌ی بیرونی با منابع خود سایت رقابت نکند و سرعت لود پایین نیاید.
function mountEnamad() {
var slot = document.getElementById('enamad-seal');
if (! slot)
return;
var a = document.createElement('a');
a.setAttribute('referrerpolicy', 'origin');
a.target = '_blank';
a.href = 'https://trustseal.enamad.ir/?id=648891&Code=tudyxbILEtrmS2VZHeGr1LfmkrEerqeF';
var img = document.createElement('img');
img.setAttribute('referrerpolicy', 'origin');
img.src = 'https://trustseal.enamad.ir/logo.aspx?id=648891&Code=tudyxbILEtrmS2VZHeGr1LfmkrEerqeF';
img.alt = 'نماد اعتماد الکترونیکی';
img.style.cursor = 'pointer';
img.setAttribute('code', 'tudyxbILEtrmS2VZHeGr1LfmkrEerqeF');
img.loading = 'lazy';
a.appendChild(img);
slot.appendChild(a);
}
if (document.readyState === 'complete') {
mountEnamad();
} else {
window.addEventListener('load', mountEnamad);
}
})(); })();
</script> </script>
+4 -4
View File
@@ -2744,12 +2744,12 @@
source-map-js "^1.2.1" source-map-js "^1.2.1"
tailwindcss "4.3.0" tailwindcss "4.3.0"
"@tailwindcss/oxide-darwin-arm64@^4.3.0", "@tailwindcss/oxide-darwin-arm64@4.3.0": "@tailwindcss/oxide-darwin-arm64@4.3.0":
version "4.3.0" version "4.3.0"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz" resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz"
integrity sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ== integrity sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==
"@tailwindcss/oxide-linux-arm64-gnu@^4.3.0", "@tailwindcss/oxide-linux-arm64-gnu@4.3.0": "@tailwindcss/oxide-linux-arm64-gnu@4.3.0":
version "4.3.0" version "4.3.0"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz" resolved "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz"
integrity sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg== integrity sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==
@@ -4849,12 +4849,12 @@ leaflet@^1.9.0, leaflet@^1.9.4:
resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz" resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz"
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
lightningcss-darwin-arm64@^1.32.0, lightningcss-darwin-arm64@1.32.0: lightningcss-darwin-arm64@1.32.0:
version "1.32.0" version "1.32.0"
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz" resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz"
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
lightningcss-linux-arm64-gnu@^1.32.0, lightningcss-linux-arm64-gnu@1.32.0: lightningcss-linux-arm64-gnu@1.32.0:
version "1.32.0" version "1.32.0"
resolved "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz" resolved "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz"
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==