From d6956395c8c05bfd394f2a3c8322a190cae9d615 Mon Sep 17 00:00:00 2001 From: tajniak81 <13187254+tajniak81@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:53:14 +0200 Subject: [PATCH] Rebrand CommGate to gsmnode and adopt new design system Rename the whole project from CommGate to gsmnode and re-skin every surface onto the new signal-green + ink design system (Space Grotesk / IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark, lowercase gsm+node wordmark). - Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme, new logo/favicon assets; both builds verified. - Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive launcher icons; rename Android applicationId/package to app.gsmnode.phone; bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified. - API Server (Go): panel routes, User-Agent, health service id, branding. - Home Assistant Plugin: rename integration domain sms_gateway to gsmnode (folder, DOMAIN, services, entity ids, classes); py_compile verified. - Update all READMEs; add .gitignore (excludes Design/, build artifacts). Co-Authored-By: Claude Opus 4.8 --- .claude/launch.json | 19 + .claude/settings.local.json | 49 + .gitignore | 31 + API Server/.env.example | 28 + API Server/.gitignore | 5 + API Server/README.md | 154 ++ API Server/cmd/server/main.go | 64 + API Server/go.mod | 5 + API Server/go.sum | 2 + API Server/internal/api/auth.go | 101 + API Server/internal/api/calls.go | 54 + API Server/internal/api/devices.go | 191 ++ .../api/dist/assets/index-FjfYnLdU.css | 1 + .../api/dist/assets/index-Hipq9mYq.js | 72 + API Server/internal/api/dist/favicon-32.png | Bin 0 -> 759 bytes API Server/internal/api/dist/favicon.svg | 9 + .../api/dist/gsmnode-horizontal-white.png | Bin 0 -> 11104 bytes .../internal/api/dist/gsmnode-horizontal.png | Bin 0 -> 11370 bytes API Server/internal/api/dist/index.html | 16 + API Server/internal/api/health.go | 11 + API Server/internal/api/inbox.go | 96 + API Server/internal/api/messages.go | 314 +++ API Server/internal/api/panel.go | 23 + API Server/internal/api/respond.go | 65 + API Server/internal/api/server.go | 243 ++ API Server/internal/api/sweeper.go | 74 + API Server/internal/api/util.go | 65 + API Server/internal/api/webhooks.go | 170 ++ API Server/internal/auth/jwt.go | 64 + API Server/internal/auth/token.go | 16 + API Server/internal/config/config.go | 95 + API Server/internal/pb/client.go | 265 +++ API Server/panel/index.html | 15 + API Server/panel/package-lock.json | 1974 ++++++++++++++++ API Server/panel/package.json | 21 + API Server/panel/public/favicon-32.png | Bin 0 -> 759 bytes API Server/panel/public/favicon.svg | 9 + .../panel/public/gsmnode-horizontal-white.png | Bin 0 -> 11104 bytes .../panel/public/gsmnode-horizontal.png | Bin 0 -> 11370 bytes API Server/panel/src/App.vue | 111 + .../panel/src/components/EndpointTable.vue | 40 + API Server/panel/src/main.js | 6 + API Server/panel/src/style.css | 336 +++ API Server/panel/src/theme.js | 30 + API Server/panel/vite.config.js | 20 + API Server/scripts/Run-ApiServer.ps1 | 25 + API Server/scripts/create-user.mjs | 50 + API Server/scripts/setup-pocketbase.mjs | 194 ++ Home Assistant Plugin/README.md | 133 ++ .../configuration.example.yaml | 49 + .../custom_components/gsmnode/__init__.py | 105 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 5943 bytes .../__pycache__/binary_sensor.cpython-313.pyc | Bin 0 -> 3206 bytes .../__pycache__/client.cpython-313.pyc | Bin 0 -> 6852 bytes .../__pycache__/config_flow.cpython-313.pyc | Bin 0 -> 3095 bytes .../gsmnode/__pycache__/const.cpython-313.pyc | Bin 0 -> 423 bytes .../__pycache__/notify.cpython-313.pyc | Bin 0 -> 8013 bytes .../gsmnode/binary_sensor.py | 66 + .../custom_components/gsmnode/client.py | 105 + .../custom_components/gsmnode/config_flow.py | 67 + .../custom_components/gsmnode/const.py | 13 + .../custom_components/gsmnode/manifest.json | 13 + .../custom_components/gsmnode/notify.py | 150 ++ .../custom_components/gsmnode/services.yaml | 53 + .../custom_components/gsmnode/strings.json | 43 + .../gsmnode/translations/en.json | 43 + Phone App/.gitignore | 19 + Phone App/.metadata | 30 + Phone App/README.md | 124 + Phone App/analysis_options.yaml | 6 + Phone App/android/.gitignore | 13 + Phone App/android/app/build.gradle | 43 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 76 + .../gsmnode/phone/GatewayForegroundService.kt | 99 + .../kotlin/app/gsmnode/phone/MainActivity.kt | 187 ++ .../kotlin/app/gsmnode/phone/SmsReceiver.kt | 40 + .../app/gsmnode/phone/SmsStatusReceiver.kt | 33 + .../drawable-hdpi/ic_launcher_foreground.png | Bin 0 -> 4107 bytes .../drawable-mdpi/ic_launcher_foreground.png | Bin 0 -> 2682 bytes .../res/drawable-v21/launch_background.xml | 12 + .../drawable-xhdpi/ic_launcher_foreground.png | Bin 0 -> 5258 bytes .../ic_launcher_foreground.png | Bin 0 -> 7632 bytes .../ic_launcher_foreground.png | Bin 0 -> 10616 bytes .../main/res/drawable/launch_background.xml | 12 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 9 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2170 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1235 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2526 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4122 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5131 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/colors.xml | 4 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + Phone App/android/build.gradle | 18 + Phone App/android/gradle.properties | 7 + .../gradle/wrapper/gradle-wrapper.properties | 5 + Phone App/android/settings.gradle | 25 + .../app/src/main/AndroidManifest.xml | 76 + .../gsmnode/phone/GatewayForegroundService.kt | 99 + .../kotlin/app/gsmnode/phone/MainActivity.kt | 187 ++ .../kotlin/app/gsmnode/phone/SmsReceiver.kt | 40 + .../app/gsmnode/phone/SmsStatusReceiver.kt | 33 + Phone App/assets/icon/adaptive-foreground.png | Bin 0 -> 16595 bytes Phone App/assets/icon/app-icon.png | Bin 0 -> 24992 bytes Phone App/lib/config.dart | 15 + Phone App/lib/main.dart | 42 + Phone App/lib/models/message.dart | 33 + Phone App/lib/screens/home_screen.dart | 298 +++ Phone App/lib/screens/login_screen.dart | 152 ++ Phone App/lib/services/api_client.dart | 130 ++ Phone App/lib/services/gateway_service.dart | 165 ++ Phone App/lib/services/sms_service.dart | 76 + Phone App/lib/services/storage.dart | 54 + Phone App/lib/theme.dart | 292 +++ Phone App/lib/widgets/gsmnode_mark.dart | 102 + Phone App/pubspec.yaml | 31 + README.md | 82 + Web App/README.md | 82 + Web App/server/.env.example | 8 + Web App/server/Run-WebApp.ps1 | 29 + Web App/server/dist/apple-touch-icon.png | Bin 0 -> 4109 bytes Web App/server/dist/assets/index-Ab7NGFI_.css | 1 + Web App/server/dist/assets/index-B7DZHD5s.js | 150 ++ Web App/server/dist/favicon-32.png | Bin 0 -> 759 bytes Web App/server/dist/favicon.svg | 9 + .../server/dist/gsmnode-horizontal-white.png | Bin 0 -> 11104 bytes Web App/server/dist/gsmnode-horizontal.png | Bin 0 -> 11370 bytes Web App/server/dist/gsmnode-mark.svg | 6 + Web App/server/dist/icon-192.png | Bin 0 -> 4246 bytes Web App/server/dist/icon-512.png | Bin 0 -> 11170 bytes Web App/server/dist/index.html | 17 + Web App/server/go.mod | 3 + Web App/server/main.go | 113 + Web App/web/.gitignore | 4 + Web App/web/index.html | 16 + Web App/web/package-lock.json | 1996 +++++++++++++++++ Web App/web/package.json | 22 + Web App/web/public/apple-touch-icon.png | Bin 0 -> 4109 bytes Web App/web/public/favicon-32.png | Bin 0 -> 759 bytes Web App/web/public/favicon.svg | 9 + .../web/public/gsmnode-horizontal-white.png | Bin 0 -> 11104 bytes Web App/web/public/gsmnode-horizontal.png | Bin 0 -> 11370 bytes Web App/web/public/gsmnode-mark.svg | 6 + Web App/web/public/icon-192.png | Bin 0 -> 4246 bytes Web App/web/public/icon-512.png | Bin 0 -> 11170 bytes Web App/web/src/App.vue | 3 + Web App/web/src/api.js | 68 + Web App/web/src/components/ApiStatus.vue | 67 + Web App/web/src/components/PageHeader.vue | 13 + Web App/web/src/components/StatusBadge.vue | 25 + Web App/web/src/main.js | 6 + Web App/web/src/router.js | 46 + Web App/web/src/store/auth.js | 33 + Web App/web/src/style.css | 336 +++ Web App/web/src/theme.js | 30 + Web App/web/src/views/Call.vue | 94 + Web App/web/src/views/Devices.vue | 90 + Web App/web/src/views/Inbox.vue | 69 + Web App/web/src/views/Layout.vue | 99 + Web App/web/src/views/Login.vue | 115 + Web App/web/src/views/Messages.vue | 89 + Web App/web/src/views/Send.vue | 117 + Web App/web/src/views/Webhooks.vue | 120 + Web App/web/vite.config.js | 22 + 166 files changed, 12605 insertions(+) create mode 100644 .claude/launch.json create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 API Server/.env.example create mode 100644 API Server/.gitignore create mode 100644 API Server/README.md create mode 100644 API Server/cmd/server/main.go create mode 100644 API Server/go.mod create mode 100644 API Server/go.sum create mode 100644 API Server/internal/api/auth.go create mode 100644 API Server/internal/api/calls.go create mode 100644 API Server/internal/api/devices.go create mode 100644 API Server/internal/api/dist/assets/index-FjfYnLdU.css create mode 100644 API Server/internal/api/dist/assets/index-Hipq9mYq.js create mode 100644 API Server/internal/api/dist/favicon-32.png create mode 100644 API Server/internal/api/dist/favicon.svg create mode 100644 API Server/internal/api/dist/gsmnode-horizontal-white.png create mode 100644 API Server/internal/api/dist/gsmnode-horizontal.png create mode 100644 API Server/internal/api/dist/index.html create mode 100644 API Server/internal/api/health.go create mode 100644 API Server/internal/api/inbox.go create mode 100644 API Server/internal/api/messages.go create mode 100644 API Server/internal/api/panel.go create mode 100644 API Server/internal/api/respond.go create mode 100644 API Server/internal/api/server.go create mode 100644 API Server/internal/api/sweeper.go create mode 100644 API Server/internal/api/util.go create mode 100644 API Server/internal/api/webhooks.go create mode 100644 API Server/internal/auth/jwt.go create mode 100644 API Server/internal/auth/token.go create mode 100644 API Server/internal/config/config.go create mode 100644 API Server/internal/pb/client.go create mode 100644 API Server/panel/index.html create mode 100644 API Server/panel/package-lock.json create mode 100644 API Server/panel/package.json create mode 100644 API Server/panel/public/favicon-32.png create mode 100644 API Server/panel/public/favicon.svg create mode 100644 API Server/panel/public/gsmnode-horizontal-white.png create mode 100644 API Server/panel/public/gsmnode-horizontal.png create mode 100644 API Server/panel/src/App.vue create mode 100644 API Server/panel/src/components/EndpointTable.vue create mode 100644 API Server/panel/src/main.js create mode 100644 API Server/panel/src/style.css create mode 100644 API Server/panel/src/theme.js create mode 100644 API Server/panel/vite.config.js create mode 100644 API Server/scripts/Run-ApiServer.ps1 create mode 100644 API Server/scripts/create-user.mjs create mode 100644 API Server/scripts/setup-pocketbase.mjs create mode 100644 Home Assistant Plugin/README.md create mode 100644 Home Assistant Plugin/configuration.example.yaml create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__init__.py create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__pycache__/__init__.cpython-313.pyc create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__pycache__/binary_sensor.cpython-313.pyc create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__pycache__/client.cpython-313.pyc create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__pycache__/config_flow.cpython-313.pyc create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__pycache__/const.cpython-313.pyc create mode 100644 Home Assistant Plugin/custom_components/gsmnode/__pycache__/notify.cpython-313.pyc create mode 100644 Home Assistant Plugin/custom_components/gsmnode/binary_sensor.py create mode 100644 Home Assistant Plugin/custom_components/gsmnode/client.py create mode 100644 Home Assistant Plugin/custom_components/gsmnode/config_flow.py create mode 100644 Home Assistant Plugin/custom_components/gsmnode/const.py create mode 100644 Home Assistant Plugin/custom_components/gsmnode/manifest.json create mode 100644 Home Assistant Plugin/custom_components/gsmnode/notify.py create mode 100644 Home Assistant Plugin/custom_components/gsmnode/services.yaml create mode 100644 Home Assistant Plugin/custom_components/gsmnode/strings.json create mode 100644 Home Assistant Plugin/custom_components/gsmnode/translations/en.json create mode 100644 Phone App/.gitignore create mode 100644 Phone App/.metadata create mode 100644 Phone App/README.md create mode 100644 Phone App/analysis_options.yaml create mode 100644 Phone App/android/.gitignore create mode 100644 Phone App/android/app/build.gradle create mode 100644 Phone App/android/app/src/debug/AndroidManifest.xml create mode 100644 Phone App/android/app/src/main/AndroidManifest.xml create mode 100644 Phone App/android/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt create mode 100644 Phone App/android/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt create mode 100644 Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt create mode 100644 Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt create mode 100644 Phone App/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png create mode 100644 Phone App/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png create mode 100644 Phone App/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 Phone App/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png create mode 100644 Phone App/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png create mode 100644 Phone App/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png create mode 100644 Phone App/android/app/src/main/res/drawable/launch_background.xml create mode 100644 Phone App/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 Phone App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 Phone App/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 Phone App/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 Phone App/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 Phone App/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 Phone App/android/app/src/main/res/values-night/styles.xml create mode 100644 Phone App/android/app/src/main/res/values/colors.xml create mode 100644 Phone App/android/app/src/main/res/values/styles.xml create mode 100644 Phone App/android/app/src/profile/AndroidManifest.xml create mode 100644 Phone App/android/build.gradle create mode 100644 Phone App/android/gradle.properties create mode 100644 Phone App/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 Phone App/android/settings.gradle create mode 100644 Phone App/android_overlay/app/src/main/AndroidManifest.xml create mode 100644 Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt create mode 100644 Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt create mode 100644 Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt create mode 100644 Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt create mode 100644 Phone App/assets/icon/adaptive-foreground.png create mode 100644 Phone App/assets/icon/app-icon.png create mode 100644 Phone App/lib/config.dart create mode 100644 Phone App/lib/main.dart create mode 100644 Phone App/lib/models/message.dart create mode 100644 Phone App/lib/screens/home_screen.dart create mode 100644 Phone App/lib/screens/login_screen.dart create mode 100644 Phone App/lib/services/api_client.dart create mode 100644 Phone App/lib/services/gateway_service.dart create mode 100644 Phone App/lib/services/sms_service.dart create mode 100644 Phone App/lib/services/storage.dart create mode 100644 Phone App/lib/theme.dart create mode 100644 Phone App/lib/widgets/gsmnode_mark.dart create mode 100644 Phone App/pubspec.yaml create mode 100644 README.md create mode 100644 Web App/README.md create mode 100644 Web App/server/.env.example create mode 100644 Web App/server/Run-WebApp.ps1 create mode 100644 Web App/server/dist/apple-touch-icon.png create mode 100644 Web App/server/dist/assets/index-Ab7NGFI_.css create mode 100644 Web App/server/dist/assets/index-B7DZHD5s.js create mode 100644 Web App/server/dist/favicon-32.png create mode 100644 Web App/server/dist/favicon.svg create mode 100644 Web App/server/dist/gsmnode-horizontal-white.png create mode 100644 Web App/server/dist/gsmnode-horizontal.png create mode 100644 Web App/server/dist/gsmnode-mark.svg create mode 100644 Web App/server/dist/icon-192.png create mode 100644 Web App/server/dist/icon-512.png create mode 100644 Web App/server/dist/index.html create mode 100644 Web App/server/go.mod create mode 100644 Web App/server/main.go create mode 100644 Web App/web/.gitignore create mode 100644 Web App/web/index.html create mode 100644 Web App/web/package-lock.json create mode 100644 Web App/web/package.json create mode 100644 Web App/web/public/apple-touch-icon.png create mode 100644 Web App/web/public/favicon-32.png create mode 100644 Web App/web/public/favicon.svg create mode 100644 Web App/web/public/gsmnode-horizontal-white.png create mode 100644 Web App/web/public/gsmnode-horizontal.png create mode 100644 Web App/web/public/gsmnode-mark.svg create mode 100644 Web App/web/public/icon-192.png create mode 100644 Web App/web/public/icon-512.png create mode 100644 Web App/web/src/App.vue create mode 100644 Web App/web/src/api.js create mode 100644 Web App/web/src/components/ApiStatus.vue create mode 100644 Web App/web/src/components/PageHeader.vue create mode 100644 Web App/web/src/components/StatusBadge.vue create mode 100644 Web App/web/src/main.js create mode 100644 Web App/web/src/router.js create mode 100644 Web App/web/src/store/auth.js create mode 100644 Web App/web/src/style.css create mode 100644 Web App/web/src/theme.js create mode 100644 Web App/web/src/views/Call.vue create mode 100644 Web App/web/src/views/Devices.vue create mode 100644 Web App/web/src/views/Inbox.vue create mode 100644 Web App/web/src/views/Layout.vue create mode 100644 Web App/web/src/views/Login.vue create mode 100644 Web App/web/src/views/Messages.vue create mode 100644 Web App/web/src/views/Send.vue create mode 100644 Web App/web/src/views/Webhooks.vue create mode 100644 Web App/web/vite.config.js diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..b373b84 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "web-app", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "cwd": "Web App/web", + "port": 5173 + }, + { + "name": "api-server", + "runtimeExecutable": "./api-server.exe", + "runtimeArgs": [], + "cwd": "API Server", + "port": 8080 + } + ] +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..5816aa8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,49 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:docs.sms-gate.app)", + "Bash(go version *)", + "Bash(npm --version)", + "Bash(flutter --version)", + "Bash(java -version)", + "Bash(curl -s -m 5 -o /dev/null -w \"%{http_code}\" http://10.2.1.10:8028/api/health)", + "Bash(winget --version)", + "Bash(winget install *)", + "WebFetch(domain:capcom6.github.io)", + "Bash(curl -s -m 5 http://10.2.1.10:8028/api/health)", + "Bash(curl -s -m 5 -o /dev/null -w \"superuser-auth endpoint: %{http_code}\\\\n\" -X POST http://10.2.1.10:8028/api/collections/_superusers/auth-with-password)", + "Bash(curl -s -m 5 -o /dev/null -w \"legacy admins endpoint: %{http_code}\\\\n\" -X POST http://10.2.1.10:8028/api/admins/auth-with-password)", + "Bash(ls \"C:/Users/jania/.claude/projects/E--VS-Code-Projects-SMS-Gateway-Project/memory/\" 2>&1)", + "Bash(npm install *)", + "Bash(npm run *)", + "Bash(git --version)", + "Bash(winget search *)", + "PowerShell(winget install --id Microsoft.OpenJDK.17 -e --accept-source-agreements --accept-package-agreements --disable-interactivity 2>&1)", + "Bash(rm -f test/widget_test.dart)", + "Bash(rmdir test *)", + "Bash(cp /tmp/phone_login.png \"C:/Users/jania/AppData/Local/Temp/claude/E--VS-Code-Projects-SMS-Gateway-Project/1197fea8-15b7-4bf5-83ae-354f909b16ae/scratchpad/phone_login.png\" && echo copied && ls -lh \"C:/Users/jania/AppData/Local/Temp/claude/E--VS-Code-Projects-SMS-Gateway-Project/1197fea8-15b7-4bf5-83ae-354f909b16ae/scratchpad/phone_login.png\")", + "Bash(/c/Users/jania/AppData/Local/Android/sdk/platform-tools/adb.exe -s R3CT206F84W shell pidof app.smsgateway.sms_gateway_phone)", + "Bash(/c/Users/jania/AppData/Local/Android/sdk/platform-tools/adb.exe -s R3CT206F84W shell dumpsys activity activities)", + "Bash(python -m py_compile __init__.py const.py notify.py)", + "Bash(python -c \"import json; json.load\\(open\\('manifest.json'\\)\\); print\\('MANIFEST JSON OK'\\)\")", + "Bash(./api-server.exe)", + "Bash(./web-app.exe)", + "Bash(ipconfig)", + "Bash(curl -s -m 2 -o /dev/null http://127.0.0.1:8090/)", + "Bash(break)", + "Bash(curl -s -m 5 http://127.0.0.1:8090/)", + "Bash(curl -s -m 5 http://127.0.0.1:8090/api/health)", + "Bash(curl -s -m 5 -o /dev/null -w 'OPTIONS /api/auth/login -> %{http_code}\\\\n' -X OPTIONS -H 'Origin: http://10.2.1.101:8090' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: authorization,content-type' http://10.2.1.101:8080/api/auth/login)", + "Bash(curl -s -m 5 -D - -o /dev/null -X POST -H 'Origin: http://10.2.1.101:8090' -H 'Content-Type: application/json' -d '{\"email\":\"e2e@test.local\",\"password\":\"e2eTestPass123\"}' http://10.2.1.101:8080/api/auth/login)", + "Bash(curl -s -m 2 -o /dev/null http://127.0.0.1:8080/api/health)", + "Bash(node -e ' *)", + "Bash(python -c \"import json; print\\('manifest v', json.load\\(open\\('manifest.json'\\)\\)['version']\\)\")", + "Bash(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/health)", + "Bash(echo \"API :8080 -> $\\(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/health 2>&1\\)\")", + "Bash(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/)", + "Bash(echo \"Web :8090 -> $\\(curl -s -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/ 2>&1\\)\")", + "Bash(tasklist //FI \"IMAGENAME eq api-server.exe\")", + "Bash(tasklist //FI \"IMAGENAME eq web-app.exe\")" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94c475b --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Design assets / source kits — excluded per project decision +Design/ + +# Node +node_modules/ + +# Flutter / Dart +Phone App/build/ +Phone App/.dart_tool/ +Phone App/.flutter-plugins +Phone App/.flutter-plugins-dependencies +Phone App/.packages +Phone App/pubspec.lock + +# Android / Gradle +.gradle/ +.kotlin/ +*.iml +Phone App/android/local.properties +Phone App/android/key.properties + +# Go build output / binaries +*.exe + +# Environment & secrets +.env +*.local + +# OS / editor +.DS_Store +Thumbs.db diff --git a/API Server/.env.example b/API Server/.env.example new file mode 100644 index 0000000..76c74ad --- /dev/null +++ b/API Server/.env.example @@ -0,0 +1,28 @@ +# API Server configuration +# Copy to .env and adjust. The server also reads plain environment variables. + +# Address the API Server listens on +API_ADDR=:8080 + +# PocketBase base URL (no trailing slash) +POCKETBASE_URL=http://10.2.1.10:8028 + +# PocketBase superuser credentials. The API Server is the ONLY thing that talks +# to PocketBase, so it authenticates as a superuser and enforces ownership in +# application logic. Lock all collection API rules to superuser-only. +PB_ADMIN_EMAIL=admin@example.com +PB_ADMIN_PASSWORD=change-me + +# Secret used to sign client JWTs (use a long random string in production) +JWT_SECRET=dev-insecure-change-me-please + +# Access token lifetime (Go duration: 30m, 24h, 168h ...) +JWT_ACCESS_TTL=24h + +# How long a message/call may stay unprocessed before it's marked Failed. +# A device must pull it (Pending) and report a result (Processed) within this +# window, else the background sweeper fails it. +MESSAGE_TTL=5m + +# CORS allowed origins for the Web App (comma separated, or * for any) +CORS_ALLOW_ORIGINS=* diff --git a/API Server/.gitignore b/API Server/.gitignore new file mode 100644 index 0000000..6dfa40c --- /dev/null +++ b/API Server/.gitignore @@ -0,0 +1,5 @@ +.env +/server +/server.exe +/tmp/ +*.log diff --git a/API Server/README.md b/API Server/README.md new file mode 100644 index 0000000..d19f4a0 --- /dev/null +++ b/API Server/README.md @@ -0,0 +1,154 @@ +# gsmnode — API Server + +Go service that is the **single trusted entry point** in front of PocketBase. +The Web App and Phone App talk only to this server; it performs all PocketBase +access as a superuser and enforces ownership in application logic. + +``` +Web App ─┐ + ├─► API Server (:8080) ─► PocketBase (10.2.1.10:8028) +Phone App ─┘ +``` + +The server root (`GET /`) serves a gsmnode-branded **web panel**: a live health +readout plus a quick reference of both API audiences. Open +http://localhost:8080/ in a browser to check the server at a glance. + +The panel is a **Vue 3 + Tailwind v4** app in [`panel/`](panel/), built into +`internal/api/dist` and embedded into the Go binary at compile time: + +```powershell +cd panel +npm install +npm run build # outputs to ../internal/api/dist +cd ..; go build -o api-server.exe ./cmd/server # embeds the fresh dist +``` + +For panel development with hot reload (proxies `/api` to a running server on +`:8080`): `cd panel; npm run dev` → http://localhost:5174. + +## Requirements + +- Go 1.26+ (`go version`) +- A reachable PocketBase v0.23+ instance and its **superuser** credentials +- Node 18+ (only for the setup scripts) + +## 1. Configure + +```powershell +Copy-Item .env.example .env +# edit .env: set PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, and a strong JWT_SECRET +``` + +| Variable | Purpose | Default | +|---|---|---| +| `API_ADDR` | Listen address | `:8080` | +| `POCKETBASE_URL` | PocketBase base URL | `http://10.2.1.10:8028` | +| `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD` | PocketBase superuser login | — (required) | +| `JWT_SECRET` | Signs client JWTs | dev placeholder | +| `JWT_ACCESS_TTL` | Access-token lifetime | `24h` | +| `CORS_ALLOW_ORIGINS` | Comma list, or `*` | `*` | + +## 2. Set up PocketBase collections + +Creates/updates `devices`, `messages`, `inbox`, `webhooks` (the default `users` +auth collection is reused). Idempotent. + +```powershell +$env:POCKETBASE_URL="http://10.2.1.10:8028" +$env:PB_ADMIN_EMAIL="admin@example.com" +$env:PB_ADMIN_PASSWORD="your-password" +node scripts/setup-pocketbase.mjs +``` + +Create a login user: + +```powershell +node scripts/create-user.mjs user@example.com "user-password" "Display Name" +``` + +## 3. Run + +```powershell +./scripts/Run-ApiServer.ps1 +# or: go run ./cmd/server +``` + +Health check: `GET http://localhost:8080/api/health`. + +## API + +### Authentication scheme + +- **Client API** (`/api/...`): `Authorization: Bearer ` from `/api/auth/login`. + Used by the Web App and integrators. +- **Mobile API** (`/api/mobile/...`): `Authorization: Bearer ` + returned by device registration. Used by the Phone App. + +### Client / 3rd-party endpoints + +| Method | Path | Description | +|---|---|---| +| `GET` | `/api/health` | Readiness probe (public) | +| `POST` | `/api/auth/login` | `{email, password}` → JWT + user | +| `POST` | `/api/auth/refresh` | New JWT (auth) | +| `GET` | `/api/auth/me` | Current user | +| `GET` | `/api/devices` | List your devices | +| `DELETE` | `/api/devices/{id}` | Remove a device | +| `POST` | `/api/messages` | Enqueue SMS `{phone_numbers[], text_message, device_id?, sim_number?, schedule_at?}` | +| `POST` | `/api/calls` | Enqueue a phone call `{phone_number, device_id?}` | +| `GET` | `/api/messages` | List messages (`?status=&device_id=&type=&page=&per_page=`) | +| `GET` | `/api/messages/{id}` | Message state | +| `GET` | `/api/inbox` | Received SMS | +| `GET` | `/api/webhooks` | List webhooks | +| `POST` | `/api/webhooks` | Register `{event, url, device_id?}` | +| `DELETE` | `/api/webhooks/{id}` | Delete webhook | + +### Mobile / device endpoints + +| Method | Path | Description | +|---|---|---| +| `POST` | `/api/mobile/v1/device` | Register device (auth: **user JWT**) → returns `auth_token` | +| `POST` | `/api/mobile/v1/ping` | Heartbeat (auth: device token) | +| `GET` | `/api/mobile/v1/messages` | Pull pending messages; marks them `Processed` | +| `PATCH` | `/api/mobile/v1/messages/{id}` | Report `{status, error?}` (`Sent`/`Delivered`/`Failed`) | +| `POST` | `/api/mobile/v1/inbox` | Report received SMS `{phone_number, message, received_at?}` | + +### Message lifecycle + +`Pending` → (device pulls) `Processed` → `Sent` → `Delivered`, or `Failed`. + +Calls are stored as messages with `type: "call"` (SMS is `type: "sms"`) and flow +through the same pull/report pipeline. The device dials the number natively and +reports `Sent` (a call has no delivery report) or `Failed`. + +### Webhook events + +`sms:received`, `sms:sent`, `sms:delivered`, `sms:failed`. Delivered as +`POST {event, device_id, payload, created_at}` to the registered URL. + +## Quick smoke test + +```powershell +# log in +$login = curl -s -X POST http://localhost:8080/api/auth/login ` + -H "Content-Type: application/json" ` + -d '{"email":"user@example.com","password":"user-password"}' | ConvertFrom-Json +$token = $login.access_token + +# register a device (as the phone app would) +curl -s -X POST http://localhost:8080/api/mobile/v1/device ` + -H "Authorization: Bearer $token" -H "Content-Type: application/json" ` + -d '{"device_id":"test-1","name":"Test Phone","platform":"android"}' +``` + +## Project layout + +``` +cmd/server/main.go entry point, wiring, graceful shutdown +internal/config env/.env configuration +internal/pb PocketBase REST client (superuser) +internal/auth JWT issue/verify, device token generation +internal/api router, middleware, handlers +scripts/ PocketBase setup + run helpers +``` diff --git a/API Server/cmd/server/main.go b/API Server/cmd/server/main.go new file mode 100644 index 0000000..d1f6c1d --- /dev/null +++ b/API Server/cmd/server/main.go @@ -0,0 +1,64 @@ +// Command server runs the gsmnode API Server. It is the single trusted +// entry point in front of PocketBase: the Web App and Phone App talk only to +// this service, which in turn performs all PocketBase access. +package main + +import ( + "context" + "errors" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "smsgateway/apiserver/internal/api" + "smsgateway/apiserver/internal/auth" + "smsgateway/apiserver/internal/config" + "smsgateway/apiserver/internal/pb" +) + +func main() { + log.SetFlags(log.LstdFlags | log.Lmsgprefix) + log.SetPrefix("[api] ") + + cfg := config.Load() + + client := pb.New(cfg.PocketBaseURL, cfg.PBAdminEmail, cfg.PBAdminPass) + jwtMgr := auth.NewManager(cfg.JWTSecret, cfg.JWTAccessTTL) + srv := api.New(cfg, client, jwtMgr) + + // Background worker that fails messages no device processed in time. + workerCtx, workerCancel := context.WithCancel(context.Background()) + defer workerCancel() + srv.StartExpiryWorker(workerCtx) + + httpServer := &http.Server{ + Addr: cfg.Addr, + Handler: srv.Handler(), + ReadHeaderTimeout: 10 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + } + + go func() { + log.Printf("listening on %s (PocketBase: %s)", cfg.Addr, cfg.PocketBaseURL) + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("server error: %v", err) + } + }() + + // Graceful shutdown. + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + <-stop + log.Println("shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := httpServer.Shutdown(ctx); err != nil { + log.Printf("shutdown error: %v", err) + } + log.Println("stopped") +} diff --git a/API Server/go.mod b/API Server/go.mod new file mode 100644 index 0000000..5c06511 --- /dev/null +++ b/API Server/go.mod @@ -0,0 +1,5 @@ +module smsgateway/apiserver + +go 1.26 + +require github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/API Server/go.sum b/API Server/go.sum new file mode 100644 index 0000000..f56d3e6 --- /dev/null +++ b/API Server/go.sum @@ -0,0 +1,2 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= diff --git a/API Server/internal/api/auth.go b/API Server/internal/api/auth.go new file mode 100644 index 0000000..8cbefcf --- /dev/null +++ b/API Server/internal/api/auth.go @@ -0,0 +1,101 @@ +package api + +import ( + "net/http" + "strings" +) + +type loginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type userDTO struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name,omitempty"` +} + +type tokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresAt string `json:"expires_at"` + User userDTO `json:"user"` +} + +// handleLogin authenticates a user against PocketBase and issues a client JWT. +func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { + var req loginRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + req.Email = strings.TrimSpace(req.Email) + if req.Email == "" || req.Password == "" { + writeError(w, http.StatusBadRequest, "email and password are required") + return + } + + res, err := s.pb.AuthWithPassword(r.Context(), colUsers, req.Email, req.Password) + if err != nil { + writeError(w, http.StatusUnauthorized, "invalid credentials") + return + } + + user := recordToUser(res.Record) + token, exp, err := s.jwt.Issue(user.ID, user.Email, user.Name) + if err != nil { + writeError(w, http.StatusInternalServerError, "could not issue token") + return + } + writeJSON(w, http.StatusOK, tokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresAt: exp.UTC().Format("2006-01-02T15:04:05Z07:00"), + User: user, + }) +} + +// handleRefresh issues a fresh token for an already-authenticated user. +func (s *Server) handleRefresh(w http.ResponseWriter, r *http.Request) { + id, email := userFromCtx(r.Context()) + rec, err := s.pb.GetOne(r.Context(), colUsers, id) + if err != nil { + writeUpstreamError(w, err) + return + } + user := recordToUser(rec) + if user.Email == "" { + user.Email = email + } + token, exp, err := s.jwt.Issue(user.ID, user.Email, user.Name) + if err != nil { + writeError(w, http.StatusInternalServerError, "could not issue token") + return + } + writeJSON(w, http.StatusOK, tokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresAt: exp.UTC().Format("2006-01-02T15:04:05Z07:00"), + User: user, + }) +} + +// handleMe returns the current user's profile. +func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) { + id, _ := userFromCtx(r.Context()) + rec, err := s.pb.GetOne(r.Context(), colUsers, id) + if err != nil { + writeUpstreamError(w, err) + return + } + writeJSON(w, http.StatusOK, recordToUser(rec)) +} + +func recordToUser(rec map[string]any) userDTO { + return userDTO{ + ID: asString(rec["id"]), + Email: asString(rec["email"]), + Name: asString(rec["name"]), + } +} diff --git a/API Server/internal/api/calls.go b/API Server/internal/api/calls.go new file mode 100644 index 0000000..114bb52 --- /dev/null +++ b/API Server/internal/api/calls.go @@ -0,0 +1,54 @@ +package api + +import ( + "net/http" + "strings" + + "smsgateway/apiserver/internal/pb" +) + +type enqueueCallRequest struct { + PhoneNumber string `json:"phone_number"` + DeviceID string `json:"device_id"` +} + +// handleEnqueueCall queues an outbound phone call for one of the user's devices. +// A call is stored as a message with type "call"; the device pulls it through +// the same mobile pipeline as SMS and places the call natively. +func (s *Server) handleEnqueueCall(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + + var req enqueueCallRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + phone := strings.TrimSpace(req.PhoneNumber) + if phone == "" { + writeError(w, http.StatusBadRequest, "phone_number is required") + return + } + + deviceRecID, err := s.resolveDevice(r, uid, req.DeviceID) + if err != nil { + writeUpstreamError(w, err) + return + } + if deviceRecID == "" { + writeError(w, http.StatusBadRequest, "no device available; register a device first") + return + } + + rec, err := s.pb.Create(r.Context(), colMessages, pb.Record{ + "phone_numbers": []string{phone}, + "type": msgTypeCall, + "device": deviceRecID, + "owner": uid, + "status": statusPending, + }) + if err != nil { + writeUpstreamError(w, err) + return + } + writeJSON(w, http.StatusAccepted, recordToMessage(rec)) +} diff --git a/API Server/internal/api/devices.go b/API Server/internal/api/devices.go new file mode 100644 index 0000000..432d4e9 --- /dev/null +++ b/API Server/internal/api/devices.go @@ -0,0 +1,191 @@ +package api + +import ( + "net/http" + "strings" + "time" + + "smsgateway/apiserver/internal/auth" + "smsgateway/apiserver/internal/pb" +) + +type deviceDTO struct { + ID string `json:"id"` + DeviceID string `json:"device_id"` + Name string `json:"name"` + Platform string `json:"platform"` + AppVersion string `json:"app_version"` + Status string `json:"status"` + LastSeenAt string `json:"last_seen_at"` +} + +// onlineWindow is how recently a device must have pinged to count as online. +// The phone pings every ~60s, so this tolerates a couple of missed pings. +const onlineWindow = 3 * time.Minute + +func recordToDevice(rec pb.Record) deviceDTO { + lastSeen := asString(rec["last_seen_at"]) + status := "offline" + if deviceOnline(lastSeen) { + status = "online" + } + return deviceDTO{ + ID: asString(rec["id"]), + DeviceID: asString(rec["device_id"]), + Name: asString(rec["name"]), + Platform: asString(rec["platform"]), + AppVersion: asString(rec["app_version"]), + Status: status, + LastSeenAt: lastSeen, + } +} + +// deviceOnline reports whether last_seen_at is within the online window. It +// accepts the PocketBase datetime format as well as RFC3339. +func deviceOnline(lastSeenAt string) bool { + if lastSeenAt == "" { + return false + } + for _, layout := range []string{ + "2006-01-02 15:04:05.999Z07:00", + "2006-01-02 15:04:05.999Z", + time.RFC3339, + } { + if t, err := time.Parse(layout, lastSeenAt); err == nil { + return time.Since(t) < onlineWindow + } + } + return false +} + +// handleListDevices returns the authenticated user's devices. +func (s *Server) handleListDevices(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + res, err := s.pb.List(r.Context(), colDevices, pb.ListOptions{ + Filter: "owner = " + pbQuote(uid), + Sort: "-created", + PerPage: 200, + }) + if err != nil { + writeUpstreamError(w, err) + return + } + out := make([]deviceDTO, 0, len(res.Items)) + for _, rec := range res.Items { + out = append(out, recordToDevice(rec)) + } + writeJSON(w, http.StatusOK, map[string]any{"items": out, "total": res.TotalItems}) +} + +// handleDeleteDevice removes a device owned by the user. +func (s *Server) handleDeleteDevice(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + id := r.PathValue("id") + + rec, err := s.pb.GetOne(r.Context(), colDevices, id) + if err != nil { + writeUpstreamError(w, err) + return + } + if asString(rec["owner"]) != uid { + writeError(w, http.StatusForbidden, "not your device") + return + } + if err := s.pb.Delete(r.Context(), colDevices, id); err != nil { + writeUpstreamError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +type registerDeviceRequest struct { + DeviceID string `json:"device_id"` + Name string `json:"name"` + Platform string `json:"platform"` + AppVersion string `json:"app_version"` + PushToken string `json:"push_token"` +} + +type registerDeviceResponse struct { + deviceDTO + AuthToken string `json:"auth_token"` +} + +// handleRegisterDevice registers (or re-registers) a mobile device for the +// authenticated user and returns an opaque device token. Re-registering with +// the same device_id rotates the token and updates metadata. +func (s *Server) handleRegisterDevice(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + + var req registerDeviceRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + req.DeviceID = strings.TrimSpace(req.DeviceID) + if req.DeviceID == "" { + writeError(w, http.StatusBadRequest, "device_id is required") + return + } + if req.Platform == "" { + req.Platform = "android" + } + + token, err := auth.NewDeviceToken() + if err != nil { + writeError(w, http.StatusInternalServerError, "could not generate token") + return + } + + now := time.Now().UTC().Format("2006-01-02 15:04:05.000Z") + fields := pb.Record{ + "device_id": req.DeviceID, + "name": req.Name, + "platform": req.Platform, + "app_version": req.AppVersion, + "push_token": req.PushToken, + "status": "online", + "last_seen_at": now, + "auth_token": token, + "owner": uid, + } + + // Upsert by (owner, device_id). + existing, err := s.pb.FindFirst(r.Context(), colDevices, + "owner = "+pbQuote(uid)+" && device_id = "+pbQuote(req.DeviceID), "") + if err != nil { + writeUpstreamError(w, err) + return + } + + var rec pb.Record + if existing != nil { + rec, err = s.pb.Update(r.Context(), colDevices, asString(existing["id"]), fields) + } else { + rec, err = s.pb.Create(r.Context(), colDevices, fields) + } + if err != nil { + writeUpstreamError(w, err) + return + } + + writeJSON(w, http.StatusOK, registerDeviceResponse{ + deviceDTO: recordToDevice(rec), + AuthToken: token, + }) +} + +// handlePing updates a device heartbeat (last_seen_at + online status). +func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) { + device := deviceFromCtx(r.Context()) + now := time.Now().UTC().Format("2006-01-02 15:04:05.000Z") + _, err := s.pb.Update(r.Context(), colDevices, asString(device["id"]), pb.Record{ + "status": "online", + "last_seen_at": now, + }) + if err != nil { + writeUpstreamError(w, err) + return + } + writeJSON(w, http.StatusOK, map[string]any{"status": "ok"}) +} diff --git a/API Server/internal/api/dist/assets/index-FjfYnLdU.css b/API Server/internal/api/dist/assets/index-FjfYnLdU.css new file mode 100644 index 0000000..076224a --- /dev/null +++ b/API Server/internal/api/dist/assets/index-FjfYnLdU.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap";/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"IBM Plex Sans", ui-sans-serif, system-ui, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;--spacing:.25rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--font-weight-medium:500;--font-weight-semibold:600;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:"IBM Plex Sans", ui-sans-serif, system-ui, "Segoe UI", sans-serif;--default-mono-font-family:"JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;--font-display:"Space Grotesk", ui-sans-serif, system-ui, "Segoe UI", sans-serif;--radius-sm:6px;--radius-md:8px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.mx-auto{margin-inline:auto}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.gn-btn-sec{border-radius:var(--radius-md);border:1px solid var(--border-strong);background:var(--surface-card);height:40px;color:var(--text-primary);font-family:var(--font-sans);cursor:pointer;transition:background-color var(--dur-fast) var(--ease-standard),transform var(--dur-fast) var(--ease-standard);justify-content:center;align-items:center;gap:8px;padding:0 16px;font-size:.875rem;font-weight:600;display:inline-flex}.flex{display:flex}.inline-flex{display:inline-flex}.gn-btn-sm{border-radius:var(--radius-sm);height:32px;padding:0 12px;font-size:.75rem}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-8{height:calc(var(--spacing) * 8)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-4{width:calc(var(--spacing) * 4)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.flex-1{flex:1}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.overflow-hidden{overflow:hidden}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:12px}.rounded-sm{border-radius:6px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-subtle{border-color:var(--border-subtle)}.bg-card{background-color:var(--surface-card)}.bg-current{background-color:currentColor}.bg-danger-tint{background-color:var(--danger-tint)}.bg-success-tint{background-color:var(--success-tint)}.bg-sunken{background-color:var(--bg-sunken)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-1{padding-block:var(--spacing)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.text-left{text-align:left}.gn-eyebrow{font-family:var(--font-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--text-muted);font-size:11px}.font-mono{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,monospace}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-brand-text{color:var(--text-brand)}.text-danger{color:var(--danger)}.text-muted{color:var(--text-muted)}.text-primary{color:var(--text-primary)}.text-secondary{color:var(--text-secondary)}.text-success{color:var(--success)}.text-warning{color:var(--warning)}.shadow-sm{--tw-shadow:var(--sh-sm);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:var(--sh-xs);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:bg-sunken:hover{background-color:var(--bg-sunken)}}}:root{--green-100:oklch(95% .04 152);--green-200:oklch(88% .07 152);--green-300:oklch(78% .11 152);--green-500:oklch(60% .14 152);--green-600:oklch(52% .13 152);--green-700:oklch(44% .11 152);--green-on-dark:oklch(72% .14 152);--ink:#12161c;--ink-900:#0a0d11;--ink-800:#1a1f27;--paper:#fafaf9;--white:#fff;--gray-50:#f4f5f4;--gray-100:#e9ebea;--gray-200:#dcdfde;--gray-300:#c2c7c6;--gray-400:#9aa0a2;--gray-500:#6b7278;--gray-600:#4a5157;--gray-700:#333a40;--success:oklch(60% .14 152);--warning:oklch(72% .15 75);--danger:oklch(60% .17 25);--info:oklch(62% .13 245);--success-x:oklch(95% .04 152);--warning-x:oklch(95% .05 80);--danger-x:oklch(95% .05 25);--info-x:oklch(95% .04 245);--bg-page:var(--paper);--bg-sunken:var(--gray-50);--surface-card:var(--white);--surface-raised:var(--white);--surface-inverse:var(--ink);--surface-code:var(--ink-900);--border-subtle:#12161c14;--border-strong:#12161c33;--border-focus:var(--green-500);--text-primary:var(--ink);--text-secondary:var(--gray-700);--text-muted:var(--gray-500);--text-inverse:#f5f6f4;--text-brand:var(--green-600);--text-link:var(--green-600);--brand:var(--green-500);--brand-hover:var(--green-600);--brand-active:var(--green-700);--brand-tint:var(--green-100);--brand-contrast:var(--white);--brand-on-dark:var(--green-on-dark);--success-tint:var(--success-x);--warning-tint:var(--warning-x);--danger-tint:var(--danger-x);--info-tint:var(--info-x);--ring-focus:0 0 0 3px oklch(60% .14 152/.45);--ring-danger:0 0 0 3px oklch(60% .17 25/.35);--sh-xs:0 1px 2px #12161c0a;--sh-sm:0 1px 2px #12161c0f, 0 1px 1px #12161c0a;--sh-md:0 4px 12px #12161c14;--sh-lg:0 8px 24px #12161c24;--sh-xl:0 16px 40px #12161c2e;--dur-instant:.1s;--dur-fast:.12s;--dur-base:.18s;--dur-slow:.26s;--ease-standard:cubic-bezier(.2, 0, 0, 1);--ease-entrance:cubic-bezier(.16, 1, .3, 1)}[data-gsm-theme=dark]{--bg-page:var(--ink-900);--bg-sunken:#0e1216;--surface-card:#14191f;--surface-raised:#1a2027;--surface-inverse:var(--white);--surface-code:#05070a;--border-subtle:#ffffff12;--border-strong:#ffffff2e;--border-focus:var(--green-on-dark);--text-primary:#f5f6f4;--text-secondary:var(--gray-200);--text-muted:var(--gray-400);--text-inverse:var(--ink);--text-brand:var(--green-on-dark);--text-link:var(--green-on-dark);--brand:var(--green-500);--brand-hover:var(--green-on-dark);--brand-active:var(--green-600);--brand-tint:oklch(60% .14 152/.16);--brand-contrast:var(--white);--success:var(--green-300);--warning:oklch(80% .14 80);--danger:oklch(72% .16 25);--info:oklch(72% .12 245);--success-tint:oklch(60% .14 152/.16);--warning-tint:oklch(72% .15 75/.16);--danger-tint:oklch(60% .17 25/.18);--info-tint:oklch(62% .13 245/.16);--ring-focus:0 0 0 3px oklch(72% .14 152/.45);--sh-xs:0 1px 2px #0006;--sh-sm:0 1px 3px #00000080, 0 1px 2px #0006;--sh-md:0 4px 12px #00000080, 0 2px 4px #0006;--sh-lg:0 8px 24px #0009, 0 4px 8px #0006;--sh-xl:0 16px 40px #000000b3;color-scheme:dark}html,body,#app{height:100%}body{font-family:var(--font-sans);background:var(--bg-page);color:var(--text-primary);-webkit-font-smoothing:antialiased}h1,h2,h3{font-family:var(--font-display);letter-spacing:-.02em}.gn-input:focus,.gn-textarea:focus{border-color:var(--border-focus);box-shadow:var(--ring-focus);outline:none}.gn-input::placeholder,.gn-textarea::placeholder{color:var(--text-muted)}.gn-btn-pri:hover:not(:disabled){background:var(--brand-hover)}.gn-btn-pri:active:not(:disabled){background:var(--brand-active);transform:translateY(1px)}.gn-btn-pri:focus-visible{box-shadow:var(--ring-focus);outline:none}.gn-btn-pri:disabled{opacity:.45;cursor:not-allowed}.gn-btn-sec:hover:not(:disabled){background:var(--bg-sunken)}.gn-btn-sec:active:not(:disabled){transform:translateY(1px)}.gn-btn-sec:focus-visible{box-shadow:var(--ring-focus);outline:none}.gn-btn-sec:disabled{opacity:.45;cursor:not-allowed}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} diff --git a/API Server/internal/api/dist/assets/index-Hipq9mYq.js b/API Server/internal/api/dist/assets/index-Hipq9mYq.js new file mode 100644 index 0000000..53ca99a --- /dev/null +++ b/API Server/internal/api/dist/assets/index-Hipq9mYq.js @@ -0,0 +1,72 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function s(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(r){if(r.ep)return;r.ep=!0;const i=s(r);fetch(r.href,i)}})();/** +* @vue/shared v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Ns(e){const t=Object.create(null);for(const s of e.split(","))t[s]=1;return s=>s in t}const B={},nt=[],Pe=()=>{},Vn=()=>!1,ss=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ns=e=>e.startsWith("onUpdate:"),te=Object.assign,Ws=(e,t)=>{const s=e.indexOf(t);s>-1&&e.splice(s,1)},Qr=Object.prototype.hasOwnProperty,$=(e,t)=>Qr.call(e,t),M=Array.isArray,rt=e=>Ft(e)==="[object Map]",Bn=e=>Ft(e)==="[object Set]",an=e=>Ft(e)==="[object Date]",I=e=>typeof e=="function",J=e=>typeof e=="string",Me=e=>typeof e=="symbol",H=e=>e!==null&&typeof e=="object",kn=e=>(H(e)||I(e))&&I(e.then)&&I(e.catch),Gn=Object.prototype.toString,Ft=e=>Gn.call(e),ei=e=>Ft(e).slice(8,-1),qn=e=>Ft(e)==="[object Object]",Ks=e=>J(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,yt=Ns(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),rs=e=>{const t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},ti=/-\w/g,de=rs(e=>e.replace(ti,t=>t.slice(1).toUpperCase())),si=/\B([A-Z])/g,Qe=rs(e=>e.replace(si,"-$1").toLowerCase()),Jn=rs(e=>e.charAt(0).toUpperCase()+e.slice(1)),hs=rs(e=>e?`on${Jn(e)}`:""),Ee=(e,t)=>!Object.is(e,t),ps=(e,...t)=>{for(let s=0;s{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:n,value:s})},ni=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let dn;const is=()=>dn||(dn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Us(e){if(M(e)){const t={};for(let s=0;s{if(s){const n=s.split(ii);n.length>1&&(t[n[0].trim()]=n[1].trim())}}),t}function Dt(e){let t="";if(J(e))t=e;else if(M(e))for(let s=0;s!!(e&&e.__v_isRef===!0),Ae=e=>J(e)?e:e==null?"":M(e)||H(e)&&(e.toString===Gn||!I(e.toString))?Xn(e)?Ae(e.value):JSON.stringify(e,Zn,2):String(e),Zn=(e,t)=>Xn(t)?Zn(e,t.value):rt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((s,[n,r],i)=>(s[gs(n,i)+" =>"]=r,s),{})}:Bn(t)?{[`Set(${t.size})`]:[...t.values()].map(s=>gs(s))}:Me(t)?gs(t):H(t)&&!M(t)&&!qn(t)?String(t):t,gs=(e,t="")=>{var s;return Me(e)?`Symbol(${(s=e.description)!=null?s:t})`:e};/** +* @vue/reactivity v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Z;class ai{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&Z&&(Z.active?(this.parent=Z,this.index=(Z.scopes||(Z.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,s;if(this.scopes)for(t=0,s=this.scopes.length;t0&&--this._on===0){if(Z===this)Z=this.prevScope;else{let t=Z;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let s,n;for(s=0,n=this.effects.length;s0)return;if(vt){let t=vt;for(vt=void 0;t;){const s=t.next;t.next=void 0,t.flags&=-9,t=s}}let e;for(;xt;){let t=xt;for(xt=void 0;t;){const s=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(n){e||(e=n)}t=s}}if(e)throw e}function sr(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function nr(e){let t,s=e.depsTail,n=s;for(;n;){const r=n.prevDep;n.version===-1?(n===s&&(s=r),Gs(n),hi(n)):t=n,n.dep.activeLink=n.prevActiveLink,n.prevActiveLink=void 0,n=r}e.deps=t,e.depsTail=s}function Es(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(rr(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function rr(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ot)||(e.globalVersion=Ot,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Es(e))))return;e.flags|=2;const t=e.dep,s=V,n=he;V=e,he=!0;try{sr(e);const r=e.fn(e._value);(t.version===0||Ee(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{V=s,he=n,nr(e),e.flags&=-3}}function Gs(e,t=!1){const{dep:s,prevSub:n,nextSub:r}=e;if(n&&(n.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=n,e.nextSub=void 0),s.subs===e&&(s.subs=n,!n&&s.computed)){s.computed.flags&=-5;for(let i=s.computed.deps;i;i=i.nextDep)Gs(i,!0)}!t&&!--s.sc&&s.map&&s.map.delete(s.key)}function hi(e){const{prevDep:t,nextDep:s}=e;t&&(t.nextDep=s,e.prevDep=void 0),s&&(s.prevDep=t,e.nextDep=void 0)}let he=!0;const ir=[];function Ie(){ir.push(he),he=!1}function Re(){const e=ir.pop();he=e===void 0?!0:e}function hn(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const s=V;V=void 0;try{t()}finally{V=s}}}let Ot=0;class pi{constructor(t,s){this.sub=t,this.dep=s,this.version=s.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class qs{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!V||!he||V===this.computed)return;let s=this.activeLink;if(s===void 0||s.sub!==V)s=this.activeLink=new pi(V,this),V.deps?(s.prevDep=V.depsTail,V.depsTail.nextDep=s,V.depsTail=s):V.deps=V.depsTail=s,or(s);else if(s.version===-1&&(s.version=this.version,s.nextDep)){const n=s.nextDep;n.prevDep=s.prevDep,s.prevDep&&(s.prevDep.nextDep=n),s.prevDep=V.depsTail,s.nextDep=void 0,V.depsTail.nextDep=s,V.depsTail=s,V.deps===s&&(V.deps=n)}return s}trigger(t){this.version++,Ot++,this.notify(t)}notify(t){Bs();try{for(let s=this.subs;s;s=s.prevSub)s.sub.notify()&&s.sub.dep.notify()}finally{ks()}}}function or(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let n=t.deps;n;n=n.nextDep)or(n)}const s=e.dep.subs;s!==e&&(e.prevSub=s,s&&(s.nextSub=e)),e.dep.subs=e}}const As=new WeakMap,Xe=Symbol(""),Os=Symbol(""),Pt=Symbol("");function Q(e,t,s){if(he&&V){let n=As.get(e);n||As.set(e,n=new Map);let r=n.get(s);r||(n.set(s,r=new qs),r.map=n,r.key=s),r.track()}}function je(e,t,s,n,r,i){const o=As.get(e);if(!o){Ot++;return}const l=f=>{f&&f.trigger()};if(Bs(),t==="clear")o.forEach(l);else{const f=M(e),d=f&&Ks(s);if(f&&s==="length"){const a=Number(n);o.forEach((h,w)=>{(w==="length"||w===Pt||!Me(w)&&w>=a)&&l(h)})}else switch((s!==void 0||o.has(void 0))&&l(o.get(s)),d&&l(o.get(Pt)),t){case"add":f?d&&l(o.get("length")):(l(o.get(Xe)),rt(e)&&l(o.get(Os)));break;case"delete":f||(l(o.get(Xe)),rt(e)&&l(o.get(Os)));break;case"set":rt(e)&&l(o.get(Xe));break}}ks()}function et(e){const t=j(e);return t===e?t:(Q(t,"iterate",Pt),ue(e)?t:t.map(pe))}function os(e){return Q(e=j(e),"iterate",Pt),e}function Te(e,t){return $e(e)?ct(Ze(e)?pe(t):t):pe(t)}const gi={__proto__:null,[Symbol.iterator](){return _s(this,Symbol.iterator,e=>Te(this,e))},concat(...e){return et(this).concat(...e.map(t=>M(t)?et(t):t))},entries(){return _s(this,"entries",e=>(e[1]=Te(this,e[1]),e))},every(e,t){return Fe(this,"every",e,t,void 0,arguments)},filter(e,t){return Fe(this,"filter",e,t,s=>s.map(n=>Te(this,n)),arguments)},find(e,t){return Fe(this,"find",e,t,s=>Te(this,s),arguments)},findIndex(e,t){return Fe(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Fe(this,"findLast",e,t,s=>Te(this,s),arguments)},findLastIndex(e,t){return Fe(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Fe(this,"forEach",e,t,void 0,arguments)},includes(...e){return bs(this,"includes",e)},indexOf(...e){return bs(this,"indexOf",e)},join(e){return et(this).join(e)},lastIndexOf(...e){return bs(this,"lastIndexOf",e)},map(e,t){return Fe(this,"map",e,t,void 0,arguments)},pop(){return pt(this,"pop")},push(...e){return pt(this,"push",e)},reduce(e,...t){return pn(this,"reduce",e,t)},reduceRight(e,...t){return pn(this,"reduceRight",e,t)},shift(){return pt(this,"shift")},some(e,t){return Fe(this,"some",e,t,void 0,arguments)},splice(...e){return pt(this,"splice",e)},toReversed(){return et(this).toReversed()},toSorted(e){return et(this).toSorted(e)},toSpliced(...e){return et(this).toSpliced(...e)},unshift(...e){return pt(this,"unshift",e)},values(){return _s(this,"values",e=>Te(this,e))}};function _s(e,t,s){const n=os(e),r=n[t]();return n!==e&&!ue(e)&&(r._next=r.next,r.next=()=>{const i=r._next();return i.done||(i.value=s(i.value)),i}),r}const mi=Array.prototype;function Fe(e,t,s,n,r,i){const o=os(e),l=o!==e&&!ue(e),f=o[t];if(f!==mi[t]){const h=f.apply(e,i);return l?pe(h):h}let d=s;o!==e&&(l?d=function(h,w){return s.call(this,Te(e,h),w,e)}:s.length>2&&(d=function(h,w){return s.call(this,h,w,e)}));const a=f.call(o,d,n);return l&&r?r(a):a}function pn(e,t,s,n){const r=os(e),i=r!==e&&!ue(e);let o=s,l=!1;r!==e&&(i?(l=n.length===0,o=function(d,a,h){return l&&(l=!1,d=Te(e,d)),s.call(this,d,Te(e,a),h,e)}):s.length>3&&(o=function(d,a,h){return s.call(this,d,a,h,e)}));const f=r[t](o,...n);return l?Te(e,f):f}function bs(e,t,s){const n=j(e);Q(n,"iterate",Pt);const r=n[t](...s);return(r===-1||r===!1)&&Xs(s[0])?(s[0]=j(s[0]),n[t](...s)):r}function pt(e,t,s=[]){Ie(),Bs();const n=j(e)[t].apply(e,s);return ks(),Re(),n}const _i=Ns("__proto__,__v_isRef,__isVue"),lr=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(Me));function bi(e){Me(e)||(e=String(e));const t=j(this);return Q(t,"has",e),t.hasOwnProperty(e)}class cr{constructor(t=!1,s=!1){this._isReadonly=t,this._isShallow=s}get(t,s,n){if(s==="__v_skip")return t.__v_skip;const r=this._isReadonly,i=this._isShallow;if(s==="__v_isReactive")return!r;if(s==="__v_isReadonly")return r;if(s==="__v_isShallow")return i;if(s==="__v_raw")return n===(r?i?Oi:dr:i?ar:ur).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(n)?t:void 0;const o=M(t);if(!r){let f;if(o&&(f=gi[s]))return f;if(s==="hasOwnProperty")return bi}const l=Reflect.get(t,s,ee(t)?t:n);if((Me(s)?lr.has(s):_i(s))||(r||Q(t,"get",s),i))return l;if(ee(l)){const f=o&&Ks(s)?l:l.value;return r&&H(f)?Ms(f):f}return H(l)?r?Ms(l):zs(l):l}}class fr extends cr{constructor(t=!1){super(!1,t)}set(t,s,n,r){let i=t[s];const o=M(t)&&Ks(s);if(!this._isShallow){const d=$e(i);if(!ue(n)&&!$e(n)&&(i=j(i),n=j(n)),!o&&ee(i)&&!ee(n))return d||(i.value=n),!0}const l=o?Number(s)e,Ut=e=>Reflect.getPrototypeOf(e);function wi(e,t,s){return function(...n){const r=this.__v_raw,i=j(r),o=rt(i),l=e==="entries"||e===Symbol.iterator&&o,f=e==="keys"&&o,d=r[e](...n),a=s?Ps:t?ct:pe;return!t&&Q(i,"iterate",f?Os:Xe),te(Object.create(d),{next(){const{value:h,done:w}=d.next();return w?{value:h,done:w}:{value:l?[a(h[0]),a(h[1])]:a(h),done:w}}})}}function Vt(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function Ti(e,t){const s={get(r){const i=this.__v_raw,o=j(i),l=j(r);e||(Ee(r,l)&&Q(o,"get",r),Q(o,"get",l));const{has:f}=Ut(o),d=t?Ps:e?ct:pe;if(f.call(o,r))return d(i.get(r));if(f.call(o,l))return d(i.get(l));i!==o&&i.get(r)},get size(){const r=this.__v_raw;return!e&&Q(j(r),"iterate",Xe),r.size},has(r){const i=this.__v_raw,o=j(i),l=j(r);return e||(Ee(r,l)&&Q(o,"has",r),Q(o,"has",l)),r===l?i.has(r):i.has(r)||i.has(l)},forEach(r,i){const o=this,l=o.__v_raw,f=j(l),d=t?Ps:e?ct:pe;return!e&&Q(f,"iterate",Xe),l.forEach((a,h)=>r.call(i,d(a),d(h),o))}};return te(s,e?{add:Vt("add"),set:Vt("set"),delete:Vt("delete"),clear:Vt("clear")}:{add(r){const i=j(this),o=Ut(i),l=j(r),f=!t&&!ue(r)&&!$e(r)?l:r;return o.has.call(i,f)||Ee(r,f)&&o.has.call(i,r)||Ee(l,f)&&o.has.call(i,l)||(i.add(f),je(i,"add",f,f)),this},set(r,i){!t&&!ue(i)&&!$e(i)&&(i=j(i));const o=j(this),{has:l,get:f}=Ut(o);let d=l.call(o,r);d||(r=j(r),d=l.call(o,r));const a=f.call(o,r);return o.set(r,i),d?Ee(i,a)&&je(o,"set",r,i):je(o,"add",r,i),this},delete(r){const i=j(this),{has:o,get:l}=Ut(i);let f=o.call(i,r);f||(r=j(r),f=o.call(i,r)),l&&l.call(i,r);const d=i.delete(r);return f&&je(i,"delete",r,void 0),d},clear(){const r=j(this),i=r.size!==0,o=r.clear();return i&&je(r,"clear",void 0,void 0),o}}),["keys","values","entries",Symbol.iterator].forEach(r=>{s[r]=wi(r,e,t)}),s}function Js(e,t){const s=Ti(e,t);return(n,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?n:Reflect.get($(s,r)&&r in n?s:n,r,i)}const Ci={get:Js(!1,!1)},Ei={get:Js(!1,!0)},Ai={get:Js(!0,!1)};const ur=new WeakMap,ar=new WeakMap,dr=new WeakMap,Oi=new WeakMap;function Pi(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function zs(e){return $e(e)?e:Ys(e,!1,xi,Ci,ur)}function Mi(e){return Ys(e,!1,Si,Ei,ar)}function Ms(e){return Ys(e,!0,vi,Ai,dr)}function Ys(e,t,s,n,r){if(!H(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const i=r.get(e);if(i)return i;const o=Pi(ei(e));if(o===0)return e;const l=new Proxy(e,o===2?n:s);return r.set(e,l),l}function Ze(e){return $e(e)?Ze(e.__v_raw):!!(e&&e.__v_isReactive)}function $e(e){return!!(e&&e.__v_isReadonly)}function ue(e){return!!(e&&e.__v_isShallow)}function Xs(e){return e?!!e.__v_raw:!1}function j(e){const t=e&&e.__v_raw;return t?j(t):e}function Ii(e){return!$(e,"__v_skip")&&Object.isExtensible(e)&&zn(e,"__v_skip",!0),e}const pe=e=>H(e)?zs(e):e,ct=e=>H(e)?Ms(e):e;function ee(e){return e?e.__v_isRef===!0:!1}function mt(e){return Ri(e,!1)}function Ri(e,t){return ee(e)?e:new Fi(e,t)}class Fi{constructor(t,s){this.dep=new qs,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=s?t:j(t),this._value=s?t:pe(t),this.__v_isShallow=s}get value(){return this.dep.track(),this._value}set value(t){const s=this._rawValue,n=this.__v_isShallow||ue(t)||$e(t);t=n?t:j(t),Ee(t,s)&&(this._rawValue=t,this._value=n?t:pe(t),this.dep.trigger())}}function We(e){return ee(e)?e.value:e}const Di={get:(e,t,s)=>t==="__v_raw"?e:We(Reflect.get(e,t,s)),set:(e,t,s,n)=>{const r=e[t];return ee(r)&&!ee(s)?(r.value=s,!0):Reflect.set(e,t,s,n)}};function hr(e){return Ze(e)?e:new Proxy(e,Di)}class Li{constructor(t,s,n){this.fn=t,this.setter=s,this._value=void 0,this.dep=new qs(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=Ot-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!s,this.isSSR=n}notify(){if(this.flags|=16,!(this.flags&8)&&V!==this)return tr(this,!0),!0}get value(){const t=this.dep.track();return rr(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function ji(e,t,s=!1){let n,r;return I(e)?n=e:(n=e.get,r=e.set),new Li(n,r,s)}const Bt={},qt=new WeakMap;let Ye;function $i(e,t=!1,s=Ye){if(s){let n=qt.get(s);n||qt.set(s,n=[]),n.push(e)}}function Hi(e,t,s=B){const{immediate:n,deep:r,once:i,scheduler:o,augmentJob:l,call:f}=s,d=A=>r?A:ue(A)||r===!1||r===0?Ue(A,1):Ue(A);let a,h,w,T,D=!1,P=!1;if(ee(e)?(h=()=>e.value,D=ue(e)):Ze(e)?(h=()=>d(e),D=!0):M(e)?(P=!0,D=e.some(A=>Ze(A)||ue(A)),h=()=>e.map(A=>{if(ee(A))return A.value;if(Ze(A))return d(A);if(I(A))return f?f(A,2):A()})):I(e)?t?h=f?()=>f(e,2):e:h=()=>{if(w){Ie();try{w()}finally{Re()}}const A=Ye;Ye=a;try{return f?f(e,3,[T]):e(T)}finally{Ye=A}}:h=Pe,t&&r){const A=h,Y=r===!0?1/0:r;h=()=>Ue(A(),Y)}const G=di(),W=()=>{a.stop(),G&&G.active&&Ws(G.effects,a)};if(i&&t){const A=t;t=(...Y)=>{const me=A(...Y);return W(),me}}let F=P?new Array(e.length).fill(Bt):Bt;const k=A=>{if(!(!(a.flags&1)||!a.dirty&&!A))if(t){const Y=a.run();if(A||r||D||(P?Y.some((me,_e)=>Ee(me,F[_e])):Ee(Y,F))){w&&w();const me=Ye;Ye=a;try{const _e=[Y,F===Bt?void 0:P&&F[0]===Bt?[]:F,T];F=Y,f?f(t,3,_e):t(..._e)}finally{Ye=me}}}else a.run()};return l&&l(k),a=new Qn(h),a.scheduler=o?()=>o(k,!1):k,T=A=>$i(A,!1,a),w=a.onStop=()=>{const A=qt.get(a);if(A){if(f)f(A,4);else for(const Y of A)Y();qt.delete(a)}},t?n?k(!0):F=a.run():o?o(k.bind(null,!0),!0):a.run(),W.pause=a.pause.bind(a),W.resume=a.resume.bind(a),W.stop=W,W}function Ue(e,t=1/0,s){if(t<=0||!H(e)||e.__v_skip||(s=s||new Map,(s.get(e)||0)>=t))return e;if(s.set(e,t),t--,ee(e))Ue(e.value,t,s);else if(M(e))for(let n=0;n{Ue(n,t,s)});else if(qn(e)){for(const n in e)Ue(e[n],t,s);for(const n of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,n)&&Ue(e[n],t,s)}return e}/** +* @vue/runtime-core v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Lt(e,t,s,n){try{return n?e(...n):e()}catch(r){ls(r,t,s)}}function ge(e,t,s,n){if(I(e)){const r=Lt(e,t,s,n);return r&&kn(r)&&r.catch(i=>{ls(i,t,s)}),r}if(M(e)){const r=[];for(let i=0;i>>1,r=re[n],i=Mt(r);i=Mt(s)?re.push(e):re.splice(Ki(t),0,e),e.flags|=1,gr()}}function gr(){Jt||(Jt=pr.then(_r))}function Ui(e){M(e)?it.push(...e):Ke&&e.id===-1?Ke.splice(st+1,0,e):e.flags&1||(it.push(e),e.flags|=1),gr()}function gn(e,t,s=we+1){for(;sMt(s)-Mt(n));if(it.length=0,Ke){Ke.push(...t);return}for(Ke=t,st=0;ste.id==null?e.flags&2?-1:1/0:e.id;function _r(e){try{for(we=0;we{n._d&&Zt(-1);const i=zt(t);let o;try{o=e(...r)}finally{zt(i),n._d&&Zt(1)}return o};return n._n=!0,n._c=!0,n._d=!0,n}function Je(e,t,s,n){const r=e.dirs,i=t&&t.dirs;for(let o=0;o1)return s&&I(t)?t.call(n&&n.proxy):t}}const ki=Symbol.for("v-scx"),Gi=()=>St(ki);function ys(e,t,s){return yr(e,t,s)}function yr(e,t,s=B){const{immediate:n,deep:r,flush:i,once:o}=s,l=te({},s),f=t&&n||!t&&i!=="post";let d;if(Rt){if(i==="sync"){const T=Gi();d=T.__watcherHandles||(T.__watcherHandles=[])}else if(!f){const T=()=>{};return T.stop=Pe,T.resume=Pe,T.pause=Pe,T}}const a=ie;l.call=(T,D,P)=>ge(T,a,D,P);let h=!1;i==="post"?l.scheduler=T=>{le(T,a&&a.suspense)}:i!=="sync"&&(h=!0,l.scheduler=(T,D)=>{D?T():Zs(T)}),l.augmentJob=T=>{t&&(T.flags|=4),h&&(T.flags|=2,a&&(T.id=a.uid,T.i=a))};const w=Hi(e,t,l);return Rt&&(d?d.push(w):f&&w()),w}function qi(e,t,s){const n=this.proxy,r=J(e)?e.includes(".")?xr(n,e):()=>n[e]:e.bind(n,n);let i;I(t)?i=t:(i=t.handler,s=t);const o=jt(this),l=yr(r,i.bind(n),s);return o(),l}function xr(e,t){const s=t.split(".");return()=>{let n=e;for(let r=0;re.__isTeleport,xs=Symbol("_leaveCb");function Qs(e,t){e.shapeFlag&6&&e.component?(e.transition=t,Qs(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function vr(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function mn(e,t){let s;return!!((s=Object.getOwnPropertyDescriptor(e,t))&&!s.configurable)}const Yt=new WeakMap;function wt(e,t,s,n,r=!1){if(M(e)){e.forEach((P,G)=>wt(P,t&&(M(t)?t[G]:t),s,n,r));return}if(Tt(n)&&!r){n.shapeFlag&512&&n.type.__asyncResolved&&n.component.subTree.component&&wt(e,t,s,n.component.subTree);return}const i=n.shapeFlag&4?nn(n.component):n.el,o=r?null:i,{i:l,r:f}=e,d=t&&t.r,a=l.refs===B?l.refs={}:l.refs,h=l.setupState,w=j(h),T=h===B?Vn:P=>mn(a,P)?!1:$(w,P),D=(P,G)=>!(G&&mn(a,G));if(d!=null&&d!==f){if(_n(t),J(d))a[d]=null,T(d)&&(h[d]=null);else if(ee(d)){const P=t;D(d,P.k)&&(d.value=null),P.k&&(a[P.k]=null)}}if(I(f)){Ie();try{Lt(f,l,12,[o,a])}finally{Re()}}else{const P=J(f),G=ee(f);if(P||G){const W=()=>{if(e.f){const F=P?T(f)?h[f]:a[f]:D()||!e.k?f.value:a[e.k];if(r)M(F)&&Ws(F,i);else if(M(F))F.includes(i)||F.push(i);else if(P)a[f]=[i],T(f)&&(h[f]=a[f]);else{const k=[i];D(f,e.k)&&(f.value=k),e.k&&(a[e.k]=k)}}else P?(a[f]=o,T(f)&&(h[f]=o)):G&&(D(f,e.k)&&(f.value=o),e.k&&(a[e.k]=o))};if(o){const F=()=>{W(),Yt.delete(e)};F.id=-1,Yt.set(e,F),le(F,s)}else _n(e),W()}}}function _n(e){const t=Yt.get(e);t&&(t.flags|=8,Yt.delete(e))}is().requestIdleCallback;is().cancelIdleCallback;const Tt=e=>!!e.type.__asyncLoader,Sr=e=>e.type.__isKeepAlive;function Yi(e,t){wr(e,"a",t)}function Xi(e,t){wr(e,"da",t)}function wr(e,t,s=ie){const n=e.__wdc||(e.__wdc=()=>{let r=s;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(cs(t,n,s),s){let r=s.parent;for(;r&&r.parent;)Sr(r.parent.vnode)&&Zi(n,t,s,r),r=r.parent}}function Zi(e,t,s,n){const r=cs(t,e,n,!0);en(()=>{Ws(n[t],r)},s)}function cs(e,t,s=ie,n=!1){if(s){const r=s[e]||(s[e]=[]),i=t.__weh||(t.__weh=(...o)=>{Ie();const l=jt(s),f=ge(t,s,e,o);return l(),Re(),f});return n?r.unshift(i):r.push(i),i}}const He=e=>(t,s=ie)=>{(!Rt||e==="sp")&&cs(e,(...n)=>t(...n),s)},Qi=He("bm"),Tr=He("m"),eo=He("bu"),to=He("u"),so=He("bum"),en=He("um"),no=He("sp"),ro=He("rtg"),io=He("rtc");function oo(e,t=ie){cs("ec",e,t)}const lo=Symbol.for("v-ndc");function co(e,t,s,n){let r;const i=s,o=M(e);if(o||J(e)){const l=o&&Ze(e);let f=!1,d=!1;l&&(f=!ue(e),d=$e(e),e=os(e)),r=new Array(e.length);for(let a=0,h=e.length;at(l,f,void 0,i));else{const l=Object.keys(e);r=new Array(l.length);for(let f=0,d=l.length;fe?Br(e)?nn(e):Is(e.parent):null,Ct=te(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>Is(e.parent),$root:e=>Is(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>Er(e),$forceUpdate:e=>e.f||(e.f=()=>{Zs(e.update)}),$nextTick:e=>e.n||(e.n=Wi.bind(e.proxy)),$watch:e=>qi.bind(e)}),vs=(e,t)=>e!==B&&!e.__isScriptSetup&&$(e,t),fo={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:s,setupState:n,data:r,props:i,accessCache:o,type:l,appContext:f}=e;if(t[0]!=="$"){const w=o[t];if(w!==void 0)switch(w){case 1:return n[t];case 2:return r[t];case 4:return s[t];case 3:return i[t]}else{if(vs(n,t))return o[t]=1,n[t];if(r!==B&&$(r,t))return o[t]=2,r[t];if($(i,t))return o[t]=3,i[t];if(s!==B&&$(s,t))return o[t]=4,s[t];Rs&&(o[t]=0)}}const d=Ct[t];let a,h;if(d)return t==="$attrs"&&Q(e.attrs,"get",""),d(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(s!==B&&$(s,t))return o[t]=4,s[t];if(h=f.config.globalProperties,$(h,t))return h[t]},set({_:e},t,s){const{data:n,setupState:r,ctx:i}=e;return vs(r,t)?(r[t]=s,!0):n!==B&&$(n,t)?(n[t]=s,!0):$(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=s,!0)},has({_:{data:e,setupState:t,accessCache:s,ctx:n,appContext:r,props:i,type:o}},l){let f;return!!(s[l]||e!==B&&l[0]!=="$"&&$(e,l)||vs(t,l)||$(i,l)||$(n,l)||$(Ct,l)||$(r.config.globalProperties,l)||(f=o.__cssModules)&&f[l])},defineProperty(e,t,s){return s.get!=null?e._.accessCache[t]=0:$(s,"value")&&this.set(e,t,s.value,null),Reflect.defineProperty(e,t,s)}};function bn(e){return M(e)?e.reduce((t,s)=>(t[s]=null,t),{}):e}let Rs=!0;function uo(e){const t=Er(e),s=e.proxy,n=e.ctx;Rs=!1,t.beforeCreate&&yn(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:f,inject:d,created:a,beforeMount:h,mounted:w,beforeUpdate:T,updated:D,activated:P,deactivated:G,beforeDestroy:W,beforeUnmount:F,destroyed:k,unmounted:A,render:Y,renderTracked:me,renderTriggered:_e,errorCaptured:Ne,serverPrefetch:$t,expose:ke,inheritAttrs:ut,components:Ht,directives:Nt,filters:as}=t;if(d&&ao(d,n,null),o)for(const q in o){const U=o[q];I(U)&&(n[q]=U.bind(s))}if(r){const q=r.call(s,s);H(q)&&(e.data=zs(q))}if(Rs=!0,i)for(const q in i){const U=i[q],Ge=I(U)?U.bind(s,s):I(U.get)?U.get.bind(s,s):Pe,Wt=!I(U)&&I(U.set)?U.set.bind(s):Pe,qe=Gr({get:Ge,set:Wt});Object.defineProperty(n,q,{enumerable:!0,configurable:!0,get:()=>qe.value,set:be=>qe.value=be})}if(l)for(const q in l)Cr(l[q],n,s,q);if(f){const q=I(f)?f.call(s):f;Reflect.ownKeys(q).forEach(U=>{Bi(U,q[U])})}a&&yn(a,e,"c");function se(q,U){M(U)?U.forEach(Ge=>q(Ge.bind(s))):U&&q(U.bind(s))}if(se(Qi,h),se(Tr,w),se(eo,T),se(to,D),se(Yi,P),se(Xi,G),se(oo,Ne),se(io,me),se(ro,_e),se(so,F),se(en,A),se(no,$t),M(ke))if(ke.length){const q=e.exposed||(e.exposed={});ke.forEach(U=>{Object.defineProperty(q,U,{get:()=>s[U],set:Ge=>s[U]=Ge,enumerable:!0})})}else e.exposed||(e.exposed={});Y&&e.render===Pe&&(e.render=Y),ut!=null&&(e.inheritAttrs=ut),Ht&&(e.components=Ht),Nt&&(e.directives=Nt),$t&&vr(e)}function ao(e,t,s=Pe){M(e)&&(e=Fs(e));for(const n in e){const r=e[n];let i;H(r)?"default"in r?i=St(r.from||n,r.default,!0):i=St(r.from||n):i=St(r),ee(i)?Object.defineProperty(t,n,{enumerable:!0,configurable:!0,get:()=>i.value,set:o=>i.value=o}):t[n]=i}}function yn(e,t,s){ge(M(e)?e.map(n=>n.bind(t.proxy)):e.bind(t.proxy),t,s)}function Cr(e,t,s,n){let r=n.includes(".")?xr(s,n):()=>s[n];if(J(e)){const i=t[e];I(i)&&ys(r,i)}else if(I(e))ys(r,e.bind(s));else if(H(e))if(M(e))e.forEach(i=>Cr(i,t,s,n));else{const i=I(e.handler)?e.handler.bind(s):t[e.handler];I(i)&&ys(r,i,e)}}function Er(e){const t=e.type,{mixins:s,extends:n}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let f;return l?f=l:!r.length&&!s&&!n?f=t:(f={},r.length&&r.forEach(d=>Xt(f,d,o,!0)),Xt(f,t,o)),H(t)&&i.set(t,f),f}function Xt(e,t,s,n=!1){const{mixins:r,extends:i}=t;i&&Xt(e,i,s,!0),r&&r.forEach(o=>Xt(e,o,s,!0));for(const o in t)if(!(n&&o==="expose")){const l=ho[o]||s&&s[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const ho={data:xn,props:vn,emits:vn,methods:_t,computed:_t,beforeCreate:ne,created:ne,beforeMount:ne,mounted:ne,beforeUpdate:ne,updated:ne,beforeDestroy:ne,beforeUnmount:ne,destroyed:ne,unmounted:ne,activated:ne,deactivated:ne,errorCaptured:ne,serverPrefetch:ne,components:_t,directives:_t,watch:go,provide:xn,inject:po};function xn(e,t){return t?e?function(){return te(I(e)?e.call(this,this):e,I(t)?t.call(this,this):t)}:t:e}function po(e,t){return _t(Fs(e),Fs(t))}function Fs(e){if(M(e)){const t={};for(let s=0;st==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${de(t)}Modifiers`]||e[`${Qe(t)}Modifiers`];function yo(e,t,...s){if(e.isUnmounted)return;const n=e.vnode.props||B;let r=s;const i=t.startsWith("update:"),o=i&&bo(n,t.slice(7));o&&(o.trim&&(r=s.map(a=>J(a)?a.trim():a)),o.number&&(r=s.map(ni)));let l,f=n[l=hs(t)]||n[l=hs(de(t))];!f&&i&&(f=n[l=hs(Qe(t))]),f&&ge(f,e,6,r);const d=n[l+"Once"];if(d){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,ge(d,e,6,r)}}const xo=new WeakMap;function Or(e,t,s=!1){const n=s?xo:t.emitsCache,r=n.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!I(e)){const f=d=>{const a=Or(d,t,!0);a&&(l=!0,te(o,a))};!s&&t.mixins.length&&t.mixins.forEach(f),e.extends&&f(e.extends),e.mixins&&e.mixins.forEach(f)}return!i&&!l?(H(e)&&n.set(e,null),null):(M(i)?i.forEach(f=>o[f]=null):te(o,i),H(e)&&n.set(e,o),o)}function fs(e,t){return!e||!ss(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),$(e,t[0].toLowerCase()+t.slice(1))||$(e,Qe(t))||$(e,t))}function Sn(e){const{type:t,vnode:s,proxy:n,withProxy:r,propsOptions:[i],slots:o,attrs:l,emit:f,render:d,renderCache:a,props:h,data:w,setupState:T,ctx:D,inheritAttrs:P}=e,G=zt(e);let W,F;try{if(s.shapeFlag&4){const A=r||n,Y=A;W=Ce(d.call(Y,A,a,h,T,w,D)),F=l}else{const A=t;W=Ce(A.length>1?A(h,{attrs:l,slots:o,emit:f}):A(h,null)),F=t.props?l:vo(l)}}catch(A){Et.length=0,ls(A,e,1),W=oe(Be)}let k=W;if(F&&P!==!1){const A=Object.keys(F),{shapeFlag:Y}=k;A.length&&Y&7&&(i&&A.some(ns)&&(F=So(F,i)),k=ft(k,F,!1,!0))}return s.dirs&&(k=ft(k,null,!1,!0),k.dirs=k.dirs?k.dirs.concat(s.dirs):s.dirs),s.transition&&Qs(k,s.transition),W=k,zt(G),W}const vo=e=>{let t;for(const s in e)(s==="class"||s==="style"||ss(s))&&((t||(t={}))[s]=e[s]);return t},So=(e,t)=>{const s={};for(const n in e)(!ns(n)||!(n.slice(9)in t))&&(s[n]=e[n]);return s};function wo(e,t,s){const{props:n,children:r,component:i}=e,{props:o,children:l,patchFlag:f}=t,d=i.emitsOptions;if(t.dirs||t.transition)return!0;if(s&&f>=0){if(f&1024)return!0;if(f&16)return n?wn(n,o,d):!!o;if(f&8){const a=t.dynamicProps;for(let h=0;hObject.create(Mr),Rr=e=>Object.getPrototypeOf(e)===Mr;function Co(e,t,s,n=!1){const r={},i=Ir();e.propsDefaults=Object.create(null),Fr(e,t,r,i);for(const o in e.propsOptions[0])o in r||(r[o]=void 0);s?e.props=n?r:Mi(r):e.type.props?e.props=r:e.props=i,e.attrs=i}function Eo(e,t,s,n){const{props:r,attrs:i,vnode:{patchFlag:o}}=e,l=j(r),[f]=e.propsOptions;let d=!1;if((n||o>0)&&!(o&16)){if(o&8){const a=e.vnode.dynamicProps;for(let h=0;h{f=!0;const[w,T]=Dr(h,t,!0);te(o,w),T&&l.push(...T)};!s&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!i&&!f)return H(e)&&n.set(e,nt),nt;if(M(i))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",sn=e=>M(e)?e.map(Ce):[Ce(e)],Oo=(e,t,s)=>{if(t._n)return t;const n=Vi((...r)=>sn(t(...r)),s);return n._c=!1,n},Lr=(e,t,s)=>{const n=e._ctx;for(const r in e){if(tn(r))continue;const i=e[r];if(I(i))t[r]=Oo(r,i,n);else if(i!=null){const o=sn(i);t[r]=()=>o}}},jr=(e,t)=>{const s=sn(t);e.slots.default=()=>s},$r=(e,t,s)=>{for(const n in t)(s||!tn(n))&&(e[n]=t[n])},Po=(e,t,s)=>{const n=e.slots=Ir();if(e.vnode.shapeFlag&32){const r=t._;r?($r(n,t,s),s&&zn(n,"_",r,!0)):Lr(t,n)}else t&&jr(e,t)},Mo=(e,t,s)=>{const{vnode:n,slots:r}=e;let i=!0,o=B;if(n.shapeFlag&32){const l=t._;l?s&&l===1?i=!1:$r(r,t,s):(i=!t.$stable,Lr(t,r)),o=t}else t&&(jr(e,t),o={default:1});if(i)for(const l in r)!tn(l)&&o[l]==null&&delete r[l]},le=Lo;function Io(e){return Ro(e)}function Ro(e,t){const s=is();s.__VUE__=!0;const{insert:n,remove:r,patchProp:i,createElement:o,createText:l,createComment:f,setText:d,setElementText:a,parentNode:h,nextSibling:w,setScopeId:T=Pe,insertStaticContent:D}=e,P=(c,u,p,b=null,_=null,g=null,v=void 0,x=null,y=!!u.dynamicChildren)=>{if(c===u)return;c&&!gt(c,u)&&(b=Kt(c),be(c,_,g,!0),c=null),u.patchFlag===-2&&(y=!1,u.dynamicChildren=null);const{type:m,ref:E,shapeFlag:S}=u;switch(m){case us:G(c,u,p,b);break;case Be:W(c,u,p,b);break;case ws:c==null&&F(u,p,b,v);break;case ae:Ht(c,u,p,b,_,g,v,x,y);break;default:S&1?Y(c,u,p,b,_,g,v,x,y):S&6?Nt(c,u,p,b,_,g,v,x,y):(S&64||S&128)&&m.process(c,u,p,b,_,g,v,x,y,dt)}E!=null&&_?wt(E,c&&c.ref,g,u||c,!u):E==null&&c&&c.ref!=null&&wt(c.ref,null,g,c,!0)},G=(c,u,p,b)=>{if(c==null)n(u.el=l(u.children),p,b);else{const _=u.el=c.el;u.children!==c.children&&d(_,u.children)}},W=(c,u,p,b)=>{c==null?n(u.el=f(u.children||""),p,b):u.el=c.el},F=(c,u,p,b)=>{[c.el,c.anchor]=D(c.children,u,p,b,c.el,c.anchor)},k=({el:c,anchor:u},p,b)=>{let _;for(;c&&c!==u;)_=w(c),n(c,p,b),c=_;n(u,p,b)},A=({el:c,anchor:u})=>{let p;for(;c&&c!==u;)p=w(c),r(c),c=p;r(u)},Y=(c,u,p,b,_,g,v,x,y)=>{if(u.type==="svg"?v="svg":u.type==="math"&&(v="mathml"),c==null)me(u,p,b,_,g,v,x,y);else{const m=c.el&&c.el._isVueCE?c.el:null;try{m&&m._beginPatch(),$t(c,u,_,g,v,x,y)}finally{m&&m._endPatch()}}},me=(c,u,p,b,_,g,v,x)=>{let y,m;const{props:E,shapeFlag:S,transition:C,dirs:O}=c;if(y=c.el=o(c.type,g,E&&E.is,E),S&8?a(y,c.children):S&16&&Ne(c.children,y,null,b,_,Ss(c,g),v,x),O&&Je(c,null,b,"created"),_e(y,c,c.scopeId,v,b),E){for(const K in E)K!=="value"&&!yt(K)&&i(y,K,null,E[K],g,b);"value"in E&&i(y,"value",null,E.value,g),(m=E.onVnodeBeforeMount)&&Se(m,b,c)}O&&Je(c,null,b,"beforeMount");const R=Fo(_,C);R&&C.beforeEnter(y),n(y,u,p),((m=E&&E.onVnodeMounted)||R||O)&&le(()=>{try{m&&Se(m,b,c),R&&C.enter(y),O&&Je(c,null,b,"mounted")}finally{}},_)},_e=(c,u,p,b,_)=>{if(p&&T(c,p),b)for(let g=0;g{for(let m=y;m{const x=u.el=c.el;let{patchFlag:y,dynamicChildren:m,dirs:E}=u;y|=c.patchFlag&16;const S=c.props||B,C=u.props||B;let O;if(p&&ze(p,!1),(O=C.onVnodeBeforeUpdate)&&Se(O,p,u,c),E&&Je(u,c,p,"beforeUpdate"),p&&ze(p,!0),m&&(!c.dynamicChildren||c.dynamicChildren.length!==m.length)&&(y=0,v=!1,m=null),(S.innerHTML&&C.innerHTML==null||S.textContent&&C.textContent==null)&&a(x,""),m?ke(c.dynamicChildren,m,x,p,b,Ss(u,_),g):v||U(c,u,x,null,p,b,Ss(u,_),g,!1),y>0){if(y&16)ut(x,S,C,p,_);else if(y&2&&S.class!==C.class&&i(x,"class",null,C.class,_),y&4&&i(x,"style",S.style,C.style,_),y&8){const R=u.dynamicProps;for(let K=0;K{O&&Se(O,p,u,c),E&&Je(u,c,p,"updated")},b)},ke=(c,u,p,b,_,g,v)=>{for(let x=0;x{if(u!==p){if(u!==B)for(const g in u)!yt(g)&&!(g in p)&&i(c,g,u[g],null,_,b);for(const g in p){if(yt(g))continue;const v=p[g],x=u[g];v!==x&&g!=="value"&&i(c,g,x,v,_,b)}"value"in p&&i(c,"value",u.value,p.value,_)}},Ht=(c,u,p,b,_,g,v,x,y)=>{const m=u.el=c?c.el:l(""),E=u.anchor=c?c.anchor:l("");let{patchFlag:S,dynamicChildren:C,slotScopeIds:O}=u;O&&(x=x?x.concat(O):O),c==null?(n(m,p,b),n(E,p,b),Ne(u.children||[],p,E,_,g,v,x,y)):S>0&&S&64&&C&&c.dynamicChildren&&c.dynamicChildren.length===C.length?(ke(c.dynamicChildren,C,p,_,g,v,x),(u.key!=null||_&&u===_.subTree)&&Hr(c,u,!0)):U(c,u,p,E,_,g,v,x,y)},Nt=(c,u,p,b,_,g,v,x,y)=>{u.slotScopeIds=x,c==null?u.shapeFlag&512?_.ctx.activate(u,p,b,v,y):as(u,p,b,_,g,v,y):rn(c,u,y)},as=(c,u,p,b,_,g,v)=>{const x=c.component=Vo(c,b,_);if(Sr(c)&&(x.ctx.renderer=dt),ko(x,!1,v),x.asyncDep){if(_&&_.registerDep(x,se,v),!c.el){const y=x.subTree=oe(Be);W(null,y,u,p),c.placeholder=y.el}}else se(x,c,u,p,_,g,v)},rn=(c,u,p)=>{const b=u.component=c.component;if(wo(c,u,p))if(b.asyncDep&&!b.asyncResolved){q(b,u,p);return}else b.next=u,b.update();else u.el=c.el,b.vnode=u},se=(c,u,p,b,_,g,v)=>{const x=()=>{if(c.isMounted){let{next:S,bu:C,u:O,parent:R,vnode:K}=c;{const xe=Nr(c);if(xe){S&&(S.el=K.el,q(c,S,v)),xe.asyncDep.then(()=>{le(()=>{c.isUnmounted||m()},_)});return}}let N=S,z;ze(c,!1),S?(S.el=K.el,q(c,S,v)):S=K,C&&ps(C),(z=S.props&&S.props.onVnodeBeforeUpdate)&&Se(z,R,S,K),ze(c,!0);const X=Sn(c),ye=c.subTree;c.subTree=X,P(ye,X,h(ye.el),Kt(ye),c,_,g),S.el=X.el,N===null&&To(c,X.el),O&&le(O,_),(z=S.props&&S.props.onVnodeUpdated)&&le(()=>Se(z,R,S,K),_)}else{let S;const{el:C,props:O}=u,{bm:R,m:K,parent:N,root:z,type:X}=c,ye=Tt(u);ze(c,!1),R&&ps(R),!ye&&(S=O&&O.onVnodeBeforeMount)&&Se(S,N,u),ze(c,!0);{z.ce&&z.ce._hasShadowRoot()&&z.ce._injectChildStyle(X,c.parent?c.parent.type:void 0);const xe=c.subTree=Sn(c);P(null,xe,p,b,c,_,g),u.el=xe.el}if(K&&le(K,_),!ye&&(S=O&&O.onVnodeMounted)){const xe=u;le(()=>Se(S,N,xe),_)}(u.shapeFlag&256||N&&Tt(N.vnode)&&N.vnode.shapeFlag&256)&&c.a&&le(c.a,_),c.isMounted=!0,u=p=b=null}};c.scope.on();const y=c.effect=new Qn(x);c.scope.off();const m=c.update=y.run.bind(y),E=c.job=y.runIfDirty.bind(y);E.i=c,E.id=c.uid,y.scheduler=()=>Zs(E),ze(c,!0),m()},q=(c,u,p)=>{u.component=c;const b=c.vnode.props;c.vnode=u,c.next=null,Eo(c,u.props,b,p),Mo(c,u.children,p),Ie(),gn(c),Re()},U=(c,u,p,b,_,g,v,x,y=!1)=>{const m=c&&c.children,E=c?c.shapeFlag:0,S=u.children,{patchFlag:C,shapeFlag:O}=u;if(C>0){if(C&128){Wt(m,S,p,b,_,g,v,x,y);return}else if(C&256){Ge(m,S,p,b,_,g,v,x,y);return}}O&8?(E&16&&at(m,_,g),S!==m&&a(p,S)):E&16?O&16?Wt(m,S,p,b,_,g,v,x,y):at(m,_,g,!0):(E&8&&a(p,""),O&16&&Ne(S,p,b,_,g,v,x,y))},Ge=(c,u,p,b,_,g,v,x,y)=>{c=c||nt,u=u||nt;const m=c.length,E=u.length,S=Math.min(m,E);let C;for(C=0;CE?at(c,_,g,!0,!1,S):Ne(u,p,b,_,g,v,x,y,S)},Wt=(c,u,p,b,_,g,v,x,y)=>{let m=0;const E=u.length;let S=c.length-1,C=E-1;for(;m<=S&&m<=C;){const O=c[m],R=u[m]=y?Le(u[m]):Ce(u[m]);if(gt(O,R))P(O,R,p,null,_,g,v,x,y);else break;m++}for(;m<=S&&m<=C;){const O=c[S],R=u[C]=y?Le(u[C]):Ce(u[C]);if(gt(O,R))P(O,R,p,null,_,g,v,x,y);else break;S--,C--}if(m>S){if(m<=C){const O=C+1,R=OC)for(;m<=S;)be(c[m],_,g,!0),m++;else{const O=m,R=m,K=new Map;for(m=R;m<=C;m++){const ce=u[m]=y?Le(u[m]):Ce(u[m]);ce.key!=null&&K.set(ce.key,m)}let N,z=0;const X=C-R+1;let ye=!1,xe=0;const ht=new Array(X);for(m=0;m=X){be(ce,_,g,!0);continue}let ve;if(ce.key!=null)ve=K.get(ce.key);else for(N=R;N<=C;N++)if(ht[N-R]===0&>(ce,u[N])){ve=N;break}ve===void 0?be(ce,_,g,!0):(ht[ve-R]=m+1,ve>=xe?xe=ve:ye=!0,P(ce,u[ve],p,null,_,g,v,x,y),z++)}const cn=ye?Do(ht):nt;for(N=cn.length-1,m=X-1;m>=0;m--){const ce=R+m,ve=u[ce],fn=u[ce+1],un=ce+1{const{el:g,type:v,transition:x,children:y,shapeFlag:m}=c;if(m&6){qe(c.component.subTree,u,p,b);return}if(m&128){c.suspense.move(u,p,b);return}if(m&64){v.move(c,u,p,dt);return}if(v===ae){n(g,u,p);for(let S=0;Sx.enter(g),_));else{const{leave:S,delayLeave:C,afterLeave:O}=x,R=()=>{c.ctx.isUnmounted?r(g):n(g,u,p)},K=()=>{const N=g._isLeaving||!!g[xs];g._isLeaving&&g[xs](!0),x.persisted&&!N?R():S(g,()=>{R(),O&&O()})};C?C(g,R,K):K()}else n(g,u,p)},be=(c,u,p,b=!1,_=!1)=>{const{type:g,props:v,ref:x,children:y,dynamicChildren:m,shapeFlag:E,patchFlag:S,dirs:C,cacheIndex:O,memo:R}=c;if(S===-2&&(_=!1),x!=null&&(Ie(),wt(x,null,p,c,!0),Re()),O!=null&&(u.renderCache[O]=void 0),E&256){u.ctx.deactivate(c);return}const K=E&1&&C,N=!Tt(c);let z;if(N&&(z=v&&v.onVnodeBeforeUnmount)&&Se(z,u,c),E&6)Zr(c.component,p,b);else{if(E&128){c.suspense.unmount(p,b);return}K&&Je(c,null,u,"beforeUnmount"),E&64?c.type.remove(c,u,p,dt,b):m&&!m.hasOnce&&(g!==ae||S>0&&S&64)?at(m,u,p,!1,!0):(g===ae&&S&384||!_&&E&16)&&at(y,u,p),b&&on(c)}const X=R!=null&&O==null;(N&&(z=v&&v.onVnodeUnmounted)||K||X)&&le(()=>{z&&Se(z,u,c),K&&Je(c,null,u,"unmounted"),X&&(c.el=null)},p)},on=c=>{const{type:u,el:p,anchor:b,transition:_}=c;if(u===ae){Xr(p,b);return}if(u===ws){A(c);return}const g=()=>{r(p),_&&!_.persisted&&_.afterLeave&&_.afterLeave()};if(c.shapeFlag&1&&_&&!_.persisted){const{leave:v,delayLeave:x}=_,y=()=>v(p,g);x?x(c.el,g,y):y()}else g()},Xr=(c,u)=>{let p;for(;c!==u;)p=w(c),r(c),c=p;r(u)},Zr=(c,u,p)=>{const{bum:b,scope:_,job:g,subTree:v,um:x,m:y,a:m}=c;Cn(y),Cn(m),b&&ps(b),_.stop(),g&&(g.flags|=8,be(v,c,u,p)),x&&le(x,u),le(()=>{c.isUnmounted=!0},u)},at=(c,u,p,b=!1,_=!1,g=0)=>{for(let v=g;v{if(c.shapeFlag&6)return Kt(c.component.subTree);if(c.shapeFlag&128)return c.suspense.next();const u=w(c.anchor||c.el),p=u&&u[Ji];return p?w(p):u};let ds=!1;const ln=(c,u,p)=>{let b;c==null?u._vnode&&(be(u._vnode,null,null,!0),b=u._vnode.component):P(u._vnode||null,c,u,null,null,null,p),u._vnode=c,ds||(ds=!0,gn(b),mr(),ds=!1)},dt={p:P,um:be,m:qe,r:on,mt:as,mc:Ne,pc:U,pbc:ke,n:Kt,o:e};return{render:ln,hydrate:void 0,createApp:_o(ln)}}function Ss({type:e,props:t},s){return s==="svg"&&e==="foreignObject"||s==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:s}function ze({effect:e,job:t},s){s?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Fo(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function Hr(e,t,s=!1){const n=e.children,r=t.children;if(M(n)&&M(r))for(let i=0;i>1,e[s[l]]0&&(t[n]=s[i-1]),s[i]=n)}}for(i=s.length,o=s[i-1];i-- >0;)s[i]=o,o=t[o];return s}function Nr(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:Nr(t)}function Cn(e){if(e)for(let t=0;te.__isSuspense;function Lo(e,t){t&&t.pendingBranch?M(e)?t.effects.push(...e):t.effects.push(e):Ui(e)}const ae=Symbol.for("v-fgt"),us=Symbol.for("v-txt"),Be=Symbol.for("v-cmt"),ws=Symbol.for("v-stc"),Et=[];let fe=null;function Ve(e=!1){Et.push(fe=e?null:[])}function jo(){Et.pop(),fe=Et[Et.length-1]||null}let It=1;function Zt(e,t=!1){It+=e,e<0&&fe&&t&&(fe.hasOnce=!0)}function Ur(e){return e.dynamicChildren=It>0?fe||nt:null,jo(),It>0&&fe&&fe.push(e),e}function At(e,t,s,n,r,i){return Ur(L(e,t,s,n,r,i,!0))}function Ls(e,t,s,n,r){return Ur(oe(e,t,s,n,r,!0))}function Qt(e){return e?e.__v_isVNode===!0:!1}function gt(e,t){return e.type===t.type&&e.key===t.key}const Vr=({key:e})=>e??null,kt=({ref:e,ref_key:t,ref_for:s})=>(typeof e=="number"&&(e=""+e),e!=null?J(e)||ee(e)||I(e)?{i:Oe,r:e,k:t,f:!!s}:e:null);function L(e,t=null,s=null,n=0,r=null,i=e===ae?0:1,o=!1,l=!1){const f={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Vr(t),ref:t&&kt(t),scopeId:br,slotScopeIds:null,children:s,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:n,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:Oe};return l?(es(f,s),i&128&&e.normalize(f)):s&&(f.shapeFlag|=J(s)?8:16),It>0&&!o&&fe&&(f.patchFlag>0||i&6)&&f.patchFlag!==32&&fe.push(f),f}const oe=$o;function $o(e,t=null,s=null,n=0,r=null,i=!1){if((!e||e===lo)&&(e=Be),Qt(e)){const l=ft(e,t,!0);return s&&es(l,s),It>0&&!i&&fe&&(l.shapeFlag&6?fe[fe.indexOf(e)]=l:fe.push(l)),l.patchFlag=-2,l}if(zo(e)&&(e=e.__vccOpts),t){t=Ho(t);let{class:l,style:f}=t;l&&!J(l)&&(t.class=Dt(l)),H(f)&&(Xs(f)&&!M(f)&&(f=te({},f)),t.style=Us(f))}const o=J(e)?1:Kr(e)?128:zi(e)?64:H(e)?4:I(e)?2:0;return L(e,t,s,n,r,o,i,!0)}function Ho(e){return e?Xs(e)||Rr(e)?te({},e):e:null}function ft(e,t,s=!1,n=!1){const{props:r,ref:i,patchFlag:o,children:l,transition:f}=e,d=t?Wo(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:d,key:d&&Vr(d),ref:t&&t.ref?s&&i?M(i)?i.concat(kt(t)):[i,kt(t)]:kt(t):i,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ae?o===-1?16:o|16:o,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:f,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&ft(e.ssContent),ssFallback:e.ssFallback&&ft(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return f&&n&&Qs(a,f.clone(a)),a}function Gt(e=" ",t=0){return oe(us,null,e,t)}function No(e="",t=!1){return t?(Ve(),Ls(Be,null,e)):oe(Be,null,e)}function Ce(e){return e==null||typeof e=="boolean"?oe(Be):M(e)?oe(ae,null,e.slice()):Qt(e)?Le(e):oe(us,null,String(e))}function Le(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:ft(e)}function es(e,t){let s=0;const{shapeFlag:n}=e;if(t==null)t=null;else if(M(t))s=16;else if(typeof t=="object")if(n&65){const r=t.default;r&&(r._c&&(r._d=!1),es(e,r()),r._c&&(r._d=!0));return}else{s=32;const r=t._;!r&&!Rr(t)?t._ctx=Oe:r===3&&Oe&&(Oe.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(I(t)){if(n&65){es(e,{default:t});return}t={default:t,_ctx:Oe},s=32}else t=String(t),n&64?(s=16,t=[Gt(t)]):s=8;e.children=t,e.shapeFlag|=s}function Wo(...e){const t={};for(let s=0;sie||Oe;let ts,js;{const e=is(),t=(s,n)=>{let r;return(r=e[s])||(r=e[s]=[]),r.push(n),i=>{r.length>1?r.forEach(o=>o(i)):r[0](i)}};ts=t("__VUE_INSTANCE_SETTERS__",s=>ie=s),js=t("__VUE_SSR_SETTERS__",s=>Rt=s)}const jt=e=>{const t=ie;return ts(e),e.scope.on(),()=>{e.scope.off(),ts(t)}},En=()=>{ie&&ie.scope.off(),ts(null)};function Br(e){return e.vnode.shapeFlag&4}let Rt=!1;function ko(e,t=!1,s=!1){t&&js(t);const{props:n,children:r}=e.vnode,i=Br(e);Co(e,n,i,t),Po(e,r,s||t);const o=i?Go(e,t):void 0;return t&&js(!1),o}function Go(e,t){const s=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,fo);const{setup:n}=s;if(n){Ie();const r=e.setupContext=n.length>1?Jo(e):null,i=jt(e),o=Lt(n,e,0,[e.props,r]),l=kn(o);if(Re(),i(),(l||e.sp)&&!Tt(e)&&vr(e),l){if(o.then(En,En),t)return o.then(f=>{An(e,f)}).catch(f=>{ls(f,e,0)});e.asyncDep=o}else An(e,o)}else kr(e)}function An(e,t,s){I(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:H(t)&&(e.setupState=hr(t)),kr(e)}function kr(e,t,s){const n=e.type;e.render||(e.render=n.render||Pe);{const r=jt(e);Ie();try{uo(e)}finally{Re(),r()}}}const qo={get(e,t){return Q(e,"get",""),e[t]}};function Jo(e){const t=s=>{e.exposed=s||{}};return{attrs:new Proxy(e.attrs,qo),slots:e.slots,emit:e.emit,expose:t}}function nn(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(hr(Ii(e.exposed)),{get(t,s){if(s in t)return t[s];if(s in Ct)return Ct[s](e)},has(t,s){return s in t||s in Ct}})):e.proxy}function zo(e){return I(e)&&"__vccOpts"in e}const Gr=(e,t)=>ji(e,t,Rt);function $s(e,t,s){try{Zt(-1);const n=arguments.length;return n===2?H(t)&&!M(t)?Qt(t)?oe(e,null,[t]):oe(e,t):oe(e,null,t):(n>3?s=Array.prototype.slice.call(arguments,2):n===3&&Qt(s)&&(s=[s]),oe(e,t,s))}finally{Zt(1)}}const Yo="3.5.39";/** +* @vue/runtime-dom v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Hs;const On=typeof window<"u"&&window.trustedTypes;if(On)try{Hs=On.createPolicy("vue",{createHTML:e=>e})}catch{}const qr=Hs?e=>Hs.createHTML(e):e=>e,Xo="http://www.w3.org/2000/svg",Zo="http://www.w3.org/1998/Math/MathML",De=typeof document<"u"?document:null,Pn=De&&De.createElement("template"),Qo={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const r=t==="svg"?De.createElementNS(Xo,e):t==="mathml"?De.createElementNS(Zo,e):s?De.createElement(e,{is:s}):De.createElement(e);return e==="select"&&n&&n.multiple!=null&&r.setAttribute("multiple",n.multiple),r},createText:e=>De.createTextNode(e),createComment:e=>De.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>De.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,r,i){const o=s?s.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),s),!(r===i||!(r=r.nextSibling)););else{Pn.innerHTML=qr(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const l=Pn.content;if(n==="svg"||n==="mathml"){const f=l.firstChild;for(;f.firstChild;)l.appendChild(f.firstChild);l.removeChild(f)}t.insertBefore(l,s)}return[o?o.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},el=Symbol("_vtc");function tl(e,t,s){const n=e[el];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const Mn=Symbol("_vod"),sl=Symbol("_vsh"),nl=Symbol(""),rl=/(?:^|;)\s*display\s*:/;function il(e,t,s){const n=e.style,r=J(s);let i=!1;if(s&&!r){if(t)if(J(t))for(const o of t.split(";")){const l=o.slice(0,o.indexOf(":")).trim();s[l]==null&&bt(n,l,"")}else for(const o in t)s[o]==null&&bt(n,o,"");for(const o in s){o==="display"&&(i=!0);const l=s[o];l!=null?ll(e,o,!J(t)&&t?t[o]:void 0,l)||bt(n,o,l):bt(n,o,"")}}else if(r){if(t!==s){const o=n[nl];o&&(s+=";"+o),n.cssText=s,i=rl.test(s)}}else t&&e.removeAttribute("style");Mn in e&&(e[Mn]=i?n.display:"",e[sl]&&(n.display="none"))}const In=/\s*!important$/;function bt(e,t,s){if(M(s))s.forEach(n=>bt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=ol(e,t);In.test(s)?e.setProperty(Qe(n),s.replace(In,""),"important"):e[n]=s}}const Rn=["Webkit","Moz","ms"],Ts={};function ol(e,t){const s=Ts[t];if(s)return s;let n=de(t);if(n!=="filter"&&n in e)return Ts[t]=n;n=Jn(n);for(let r=0;rCs||(pl.then(()=>Cs=0),Cs=Date.now());function ml(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;const r=s.value;if(M(r)){const i=n.stopImmediatePropagation;n.stopImmediatePropagation=()=>{i.call(n),n._stopped=!0};const o=r.slice(),l=[n];for(let f=0;fe.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,_l=(e,t,s,n,r,i)=>{const o=r==="svg";t==="class"?tl(e,n,o):t==="style"?il(e,s,n):ss(t)?ns(t)||ul(e,t,s,n,i):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):bl(e,t,n,o))?(Ln(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Dn(e,t,n,o,i,t!=="value")):e._isVueCE&&(yl(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!J(n)))?Ln(e,de(t),n,i,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),Dn(e,t,n,o))};function bl(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&$n(t)&&I(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return $n(t)&&J(s)?!1:t in e}function yl(e,t){const s=e._def.props;if(!s)return!1;const n=de(t);return Array.isArray(s)?s.some(r=>de(r)===n):Object.keys(s).some(r=>de(r)===n)}const xl=te({patchProp:_l},Qo);let Hn;function vl(){return Hn||(Hn=Io(xl))}const Sl=((...e)=>{const t=vl().createApp(...e),{mount:s}=t;return t.mount=n=>{const r=Tl(n);if(!r)return;const i=t._component;!I(i)&&!i.render&&!i.template&&(i.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const o=s(r,!1,wl(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),o},t});function wl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function Tl(e){return J(e)?document.querySelector(e):e}/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Nn=e=>e==="";/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Cl=(...e)=>e.filter((t,s,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===s).join(" ").trim();/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Wn=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const El=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,s,n)=>n?n.toUpperCase():s.toLowerCase());/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Al=e=>{const t=El(e);return t.charAt(0).toUpperCase()+t.slice(1)};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var tt={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ol=Symbol("lucide-icons");function Pl(){return St(Ol,{})}/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ml=({name:e,iconNode:t,absoluteStrokeWidth:s,"absolute-stroke-width":n,strokeWidth:r,"stroke-width":i,size:o,color:l,...f},{slots:d})=>{const{size:a,color:h,strokeWidth:w=2,absoluteStrokeWidth:T=!1,class:D=""}=Pl(),P=Gr(()=>{const G=Nn(s)||Nn(n)||s===!0||n===!0||T===!0,W=r||i||w||tt["stroke-width"];return G?Number(W)*24/Number(o??a??tt.width):W});return $s("svg",{...tt,...f,width:o??a??tt.width,height:o??a??tt.height,stroke:l??h??tt.stroke,"stroke-width":P.value,class:Cl("lucide",D,...e?[`lucide-${Wn(Al(e))}-icon`,`lucide-${Wn(e)}`]:["lucide-icon"])},[...t.map(G=>$s(...G)),...d.default?[d.default()]:[]])};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Jr=(e,t)=>(s,{slots:n,attrs:r})=>$s(Ml,{...r,...s,iconNode:t,name:e},n.default?{default:n.default}:void 0);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Il=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]],Rl=Jr("moon",Il);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Fl=[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]],Dl=Jr("sun",Fl),zr="gsmnode-theme";function Ll(){try{return localStorage.getItem(zr)==="dark"?"dark":"light"}catch{return"light"}}const lt=mt(Ll());function Yr(e){lt.value=e,document.documentElement.setAttribute("data-gsm-theme",e);try{localStorage.setItem(zr,e)}catch{}}function Kn(){Yr(lt.value==="dark"?"light":"dark")}Yr(lt.value);const jl={class:"overflow-hidden rounded-lg border border-subtle bg-card shadow-xs"},$l={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},Hl={class:"text-base font-semibold text-primary"},Nl={class:"gn-eyebrow"},Wl={class:"w-full text-left text-sm"},Kl={class:"border-t border-subtle px-5 py-2.5 font-mono text-xs whitespace-nowrap"},Ul={class:"text-primary"},Vl={class:"border-t border-subtle px-5 py-2.5 text-secondary"},Un={__name:"EndpointTable",props:{title:String,auth:String,endpoints:Array},setup(e){const t={GET:"text-success",POST:"text-brand-text",PATCH:"text-warning",DELETE:"text-danger"};return(s,n)=>(Ve(),At("div",jl,[L("div",$l,[L("div",Hl,Ae(e.title),1),L("span",Nl,Ae(e.auth),1)]),L("table",Wl,[n[0]||(n[0]=L("thead",null,[L("tr",{class:"gn-eyebrow"},[L("th",{class:"px-5 py-2.5 font-medium"},"Endpoint"),L("th",{class:"px-5 py-2.5 font-medium"},"Description")])],-1)),L("tbody",null,[(Ve(!0),At(ae,null,co(e.endpoints,r=>(Ve(),At("tr",{key:r.method+r.path,class:"transition-colors hover:bg-sunken"},[L("td",Kl,[L("span",{class:Dt(["font-semibold",t[r.method]])},Ae(r.method),3),L("span",Ul,Ae(r.path),1)]),L("td",Vl,Ae(r.desc),1)]))),128))])])]))}},Bl={class:"mx-auto flex max-w-3xl flex-col gap-6 px-6 pt-12 pb-16"},kl={class:"flex items-center gap-4"},Gl=["src"],ql=["title"],Jl={class:"rounded-lg border border-subtle bg-card shadow-sm"},zl={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},Yl={class:"flex flex-wrap items-center gap-x-6 gap-y-2 px-5 py-4 font-mono text-xs text-secondary"},Xl={__name:"App",setup(e){const t=mt("checking"),s=mt(null),n=mt(null),r=mt(null);let i=null;async function o(){const a=performance.now();try{const h=await fetch("/api/health");n.value=Math.round(performance.now()-a),s.value=h.status,t.value=h.ok?"ok":"error"}catch{n.value=null,s.value=null,t.value="unreachable"}r.value=new Date}Tr(()=>{o(),i=setInterval(o,1e4)}),en(()=>clearInterval(i));const l={checking:{label:"checking",cls:"bg-sunken text-secondary"},ok:{label:"operational",cls:"bg-success-tint text-success"},error:{label:"error",cls:"bg-danger-tint text-danger"},unreachable:{label:"unreachable",cls:"bg-danger-tint text-danger"}},f=[{method:"POST",path:"/api/auth/login",desc:"Exchange email + password for a JWT"},{method:"GET",path:"/api/devices",desc:"List registered gateway devices"},{method:"DELETE",path:"/api/devices/{id}",desc:"Remove a device"},{method:"POST",path:"/api/messages",desc:"Queue an outbound SMS"},{method:"GET",path:"/api/messages",desc:"Outbound message history"},{method:"POST",path:"/api/calls",desc:"Queue an outbound phone call"},{method:"GET",path:"/api/inbox",desc:"Messages received by your devices"},{method:"GET",path:"/api/webhooks",desc:"List webhook subscriptions"},{method:"POST",path:"/api/webhooks",desc:"Register a webhook"}],d=[{method:"POST",path:"/api/mobile/v1/device",desc:"Register this phone as a gateway"},{method:"GET",path:"/api/mobile/v1/messages",desc:"Pull pending messages to send"},{method:"PATCH",path:"/api/mobile/v1/messages/{id}",desc:"Report sent / delivered / failed"},{method:"POST",path:"/api/mobile/v1/inbox",desc:"Push a received SMS"},{method:"POST",path:"/api/mobile/v1/ping",desc:"Device heartbeat"}];return(a,h)=>(Ve(),At("div",Bl,[L("div",kl,[L("img",{src:We(lt)==="dark"?"/gsmnode-horizontal-white.png":"/gsmnode-horizontal.png",alt:"gsmnode",class:"h-8"},null,8,Gl),h[2]||(h[2]=L("span",{class:"gn-eyebrow mt-1.5"},"API server",-1)),h[3]||(h[3]=L("div",{class:"flex-1"},null,-1)),L("button",{class:"gn-btn-sec gn-btn-sm",title:We(lt)==="dark"?"Switch to light theme":"Switch to dark theme",onClick:h[0]||(h[0]=(...w)=>We(Kn)&&We(Kn)(...w))},[We(lt)==="dark"?(Ve(),Ls(We(Dl),{key:0,class:"h-4 w-4"})):(Ve(),Ls(We(Rl),{key:1,class:"h-4 w-4"})),h[1]||(h[1]=Gt(" Theme ",-1))],8,ql)]),L("div",Jl,[L("div",zl,[h[5]||(h[5]=L("div",{class:"text-base font-semibold text-primary"},"Status",-1)),L("span",{class:Dt(["inline-flex items-center gap-1.5 rounded-sm px-2.5 py-1 font-mono text-xs font-medium",l[t.value].cls])},[h[4]||(h[4]=L("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Gt(" "+Ae(l[t.value].label),1),t.value==="error"?(Ve(),At(ae,{key:0},[Gt(Ae(s.value),1)],64)):No("",!0)],2)]),L("div",Yl,[h[6]||(h[6]=L("span",null,"GET /api/health",-1)),L("span",null,Ae(n.value!==null?n.value+"ms":"—"),1),L("span",null,Ae(r.value?"checked "+r.value.toLocaleTimeString():"—"),1)])]),oe(Un,{title:"Client API",auth:"Bearer JWT",endpoints:f}),oe(Un,{title:"Mobile API",auth:"Device token",endpoints:d}),h[7]||(h[7]=L("p",{class:"text-center font-mono text-[11px] text-muted"}," gsmnode — turn any Android phone into an SMS gateway. ",-1))]))}};Sl(Xl).mount("#app"); diff --git a/API Server/internal/api/dist/favicon-32.png b/API Server/internal/api/dist/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..23301f08f08ab2a23fe5b940a8994bf2f3cce797 GIT binary patch literal 759 zcmV|H~v=l|9uC}?_AD(96|*7 zw=H_X&#Yir4WbnU=y~%$V8AdKdk>;^pD5P!9IYP;_b7Z(&{(;f1?UzL(!;fxfHAnh z0_BdwwU4A66foHY8z?!&8kd_7yAj6sLITL85af7NG7}GECJQL ztRf6JMCxP2ZJH=W{H4#fgHa*B|xGw?haX%DPhm=6Ns%*!S`{P>#p zDiAtc*umo4O9zjM@ zDvmeTq4kp^ST`i=f>QULZE$&ihDIwxvB18(ji_F~KlqqQyIpRTeeMA~?R&?w9Et^6 z4_ri=M$5AtqZ7Zd>7JEmITQ=zr!U6UZO4$QO+&{(FB&?Z!RHrwKay`A7ru-Q^DKvA z0STQ(i<&LvSeuoL(=YDeN$*?Un=VlaNJx*_ss%=L4))=9z|ZT`C2E0KGbca=wn{gM zbo`#EN$8jC1At{}&Kp7Z?EjW8y=%@AM4=Zz68BO82IggcA$#_ZX5qz}8Y=dH;%qe2 zhf`pfh?RDgUVkiQE+COxXU@ + + + + + + + + \ No newline at end of file diff --git a/API Server/internal/api/dist/gsmnode-horizontal-white.png b/API Server/internal/api/dist/gsmnode-horizontal-white.png new file mode 100644 index 0000000000000000000000000000000000000000..ac100c0a094d3ff30d5db56e0794c27c2a02a455 GIT binary patch literal 11104 zcmeHt_gholvu`LOQbd|aQ;L8@dJ(0IfFd=J&_P6s1P#(fTBu4Fkluqp2u%c}7X^e+ zMS2Ml2}P=a5FnJhd0+4MoOAzzd!Oh2!ro!;H8X40%$}LgOoV~neR>)W8W0FXuk}FP z5Coz?1NT3user#1QQFlY5D!R8{Vw#S^*S!_t(~LQ=GJDdZwMl17JQS8m$8}hq1X)* zW<=;)yUP*j9~3w#;iKAy$jB|WIq)S%YE5ctEeATrP>=@FSAgoF7UR1#dTev>Zk8skT4vAsBh5q25$FB+<7c-ZRmWzC6D*_V&KIP`PAFg2!GB@2@ z?wGEA;HDdWrf>co353k^$M-&0U^c^0RBOs_H*0DTDql zASYuz`~M|Ma`4%ehnn*L6Zn5k2NI|~oI7rEx8Tr)q!WnP99h7g2T@a=ove-e`lD2T zGhITy@^DT@>QDJgsxNL!##DMW`j@YN?BXbrX(`1-J!vWifz}=XM?W8qln?(|x2vMQ z&>GQAB;L~3aNvkp(eTuXX1=Q;J0%Fj zABCi`nYm=p^`Oyzn}*q+Xq;guCSN}|mL$4G&-6}40o7}+Z zRulpT%tQBA1VQt$S17TbTB+x;lQl7#J8)Qy9UTZ1O-Bw!y53XeW{y)nTsBHk(0

9Qi9WAHmlgG0V9aF~KJQ`x}4R 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 literal 0 HcmV?d00001 diff --git a/API Server/internal/api/dist/gsmnode-horizontal.png b/API Server/internal/api/dist/gsmnode-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd2937882be756086acb72136a40562e3a0afc4 GIT binary patch literal 11370 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 literal 0 HcmV?d00001 diff --git a/API Server/internal/api/dist/index.html b/API Server/internal/api/dist/index.html new file mode 100644 index 0000000..2deda0a --- /dev/null +++ b/API Server/internal/api/dist/index.html @@ -0,0 +1,16 @@ + + + + + + + + + gsmnode · API Server + + + + +

+ + diff --git a/API Server/internal/api/health.go b/API Server/internal/api/health.go new file mode 100644 index 0000000..e94ea67 --- /dev/null +++ b/API Server/internal/api/health.go @@ -0,0 +1,11 @@ +package api + +import "net/http" + +// handleHealth reports server readiness. +func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{ + "status": "ok", + "service": "gsmnode-api", + }) +} diff --git a/API Server/internal/api/inbox.go b/API Server/internal/api/inbox.go new file mode 100644 index 0000000..4420fd2 --- /dev/null +++ b/API Server/internal/api/inbox.go @@ -0,0 +1,96 @@ +package api + +import ( + "net/http" + "strings" + "time" + + "smsgateway/apiserver/internal/pb" +) + +type inboxDTO struct { + ID string `json:"id"` + DeviceID string `json:"device_id"` + PhoneNumber string `json:"phone_number"` + Message string `json:"message"` + ReceivedAt string `json:"received_at"` +} + +func recordToInbox(rec pb.Record) inboxDTO { + return inboxDTO{ + ID: asString(rec["id"]), + DeviceID: asString(rec["device"]), + PhoneNumber: asString(rec["phone_number"]), + Message: asString(rec["message"]), + ReceivedAt: asString(rec["received_at"]), + } +} + +// handleListInbox lists received SMS for the authenticated user. +func (s *Server) handleListInbox(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + res, err := s.pb.List(r.Context(), colInbox, pb.ListOptions{ + Filter: "owner = " + pbQuote(uid), + Sort: "-received_at", + Page: queryInt(r, "page", 1), + PerPage: clampPerPage(queryInt(r, "per_page", 50)), + }) + if err != nil { + writeUpstreamError(w, err) + return + } + out := make([]inboxDTO, 0, len(res.Items)) + for _, rec := range res.Items { + out = append(out, recordToInbox(rec)) + } + writeJSON(w, http.StatusOK, map[string]any{ + "items": out, "total": res.TotalItems, "page": res.Page, + }) +} + +type receiveSMSRequest struct { + PhoneNumber string `json:"phone_number"` + Message string `json:"message"` + ReceivedAt string `json:"received_at"` +} + +// handleReceiveSMS records an incoming SMS reported by a device and fires +// sms:received webhooks. +func (s *Server) handleReceiveSMS(w http.ResponseWriter, r *http.Request) { + device := deviceFromCtx(r.Context()) + + var req receiveSMSRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + if strings.TrimSpace(req.PhoneNumber) == "" { + writeError(w, http.StatusBadRequest, "phone_number is required") + return + } + receivedAt := req.ReceivedAt + if receivedAt == "" { + receivedAt = time.Now().UTC().Format("2006-01-02 15:04:05.000Z") + } + + rec, err := s.pb.Create(r.Context(), colInbox, pb.Record{ + "device": asString(device["id"]), + "owner": asString(device["owner"]), + "phone_number": req.PhoneNumber, + "message": req.Message, + "received_at": receivedAt, + }) + if err != nil { + writeUpstreamError(w, err) + return + } + + s.dispatchWebhooks(asString(device["owner"]), asString(device["id"]), "sms:received", map[string]any{ + "inbox_id": asString(rec["id"]), + "phone_number": req.PhoneNumber, + "message": req.Message, + "received_at": receivedAt, + }) + + writeJSON(w, http.StatusCreated, recordToInbox(rec)) +} diff --git a/API Server/internal/api/messages.go b/API Server/internal/api/messages.go new file mode 100644 index 0000000..48cdb98 --- /dev/null +++ b/API Server/internal/api/messages.go @@ -0,0 +1,314 @@ +package api + +import ( + "net/http" + "strings" + "time" + + "smsgateway/apiserver/internal/pb" +) + +// Message lifecycle states. +const ( + statusPending = "Pending" // enqueued, not yet picked up by a device + statusProcessed = "Processed" // delivered to a device for sending + statusSent = "Sent" // device handed it to the SIM/radio + statusDelivered = "Delivered" // delivery report received + statusFailed = "Failed" // sending failed +) + +var validReportStates = map[string]bool{ + statusSent: true, statusDelivered: true, statusFailed: true, statusProcessed: true, +} + +// Message kinds. +const ( + msgTypeSMS = "sms" + msgTypeCall = "call" +) + +type messageDTO struct { + ID string `json:"id"` + Type string `json:"type"` + PhoneNumbers []string `json:"phone_numbers"` + TextMessage string `json:"text_message"` + DeviceID string `json:"device_id,omitempty"` + SimNumber int `json:"sim_number,omitempty"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + ScheduleAt string `json:"schedule_at,omitempty"` + CreatedAt string `json:"created_at"` +} + +func recordToMessage(rec pb.Record) messageDTO { + msgType := asString(rec["type"]) + if msgType == "" { + msgType = msgTypeSMS + } + return messageDTO{ + ID: asString(rec["id"]), + Type: msgType, + PhoneNumbers: asStringSlice(rec["phone_numbers"]), + TextMessage: asString(rec["text_message"]), + DeviceID: asString(rec["device"]), + SimNumber: asInt(rec["sim_number"]), + Status: asString(rec["status"]), + Error: asString(rec["error"]), + ScheduleAt: asString(rec["schedule_at"]), + CreatedAt: asString(rec["created"]), + } +} + +type enqueueRequest struct { + PhoneNumbers []string `json:"phone_numbers"` + TextMessage string `json:"text_message"` + DeviceID string `json:"device_id"` + SimNumber int `json:"sim_number"` + ScheduleAt string `json:"schedule_at"` +} + +// handleEnqueueMessage queues an outbound SMS for one of the user's devices. +func (s *Server) handleEnqueueMessage(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + + var req enqueueRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + phones := cleanPhones(req.PhoneNumbers) + if len(phones) == 0 { + writeError(w, http.StatusBadRequest, "at least one phone number is required") + return + } + if strings.TrimSpace(req.TextMessage) == "" { + writeError(w, http.StatusBadRequest, "text_message is required") + return + } + + // Resolve the target device: explicit device_id, else the user's first + // online device. + deviceRecID, err := s.resolveDevice(r, uid, req.DeviceID) + if err != nil { + writeUpstreamError(w, err) + return + } + if deviceRecID == "" { + writeError(w, http.StatusBadRequest, "no device available; register a device first") + return + } + + fields := pb.Record{ + "phone_numbers": phones, + "text_message": req.TextMessage, + "type": msgTypeSMS, + "device": deviceRecID, + "owner": uid, + "status": statusPending, + } + if req.SimNumber > 0 { + fields["sim_number"] = req.SimNumber + } + if req.ScheduleAt != "" { + fields["schedule_at"] = req.ScheduleAt + } + + rec, err := s.pb.Create(r.Context(), colMessages, fields) + if err != nil { + writeUpstreamError(w, err) + return + } + writeJSON(w, http.StatusAccepted, recordToMessage(rec)) +} + +// resolveDevice returns the PocketBase device record id to target. requested may +// be either the internal record id or the client-facing device_id. +func (s *Server) resolveDevice(r *http.Request, uid, requested string) (string, error) { + requested = strings.TrimSpace(requested) + if requested != "" { + dev, err := s.pb.FindFirst(r.Context(), colDevices, + "owner = "+pbQuote(uid)+" && (id = "+pbQuote(requested)+" || device_id = "+pbQuote(requested)+")", "") + if err != nil { + return "", err + } + if dev == nil { + return "", nil + } + return asString(dev["id"]), nil + } + dev, err := s.pb.FindFirst(r.Context(), colDevices, + "owner = "+pbQuote(uid), "-last_seen_at") + if err != nil || dev == nil { + return "", err + } + return asString(dev["id"]), nil +} + +// handleListMessages lists the user's messages with optional filters. +func (s *Server) handleListMessages(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + + filters := []string{"owner = " + pbQuote(uid)} + if st := r.URL.Query().Get("status"); st != "" { + filters = append(filters, "status = "+pbQuote(st)) + } + if dev := r.URL.Query().Get("device_id"); dev != "" { + filters = append(filters, "device = "+pbQuote(dev)) + } + if t := r.URL.Query().Get("type"); t != "" { + filters = append(filters, "type = "+pbQuote(t)) + } + + res, err := s.pb.List(r.Context(), colMessages, pb.ListOptions{ + Filter: strings.Join(filters, " && "), + Sort: "-created", + Page: queryInt(r, "page", 1), + PerPage: clampPerPage(queryInt(r, "per_page", 50)), + }) + if err != nil { + writeUpstreamError(w, err) + return + } + out := make([]messageDTO, 0, len(res.Items)) + for _, rec := range res.Items { + out = append(out, recordToMessage(rec)) + } + writeJSON(w, http.StatusOK, map[string]any{ + "items": out, "total": res.TotalItems, "page": res.Page, + }) +} + +// handleGetMessage returns a single message's state. +func (s *Server) handleGetMessage(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + rec, err := s.pb.GetOne(r.Context(), colMessages, r.PathValue("id")) + if err != nil { + writeUpstreamError(w, err) + return + } + if asString(rec["owner"]) != uid { + writeError(w, http.StatusForbidden, "not your message") + return + } + writeJSON(w, http.StatusOK, recordToMessage(rec)) +} + +// handlePullMessages returns pending messages for the calling device and marks +// them Processed so they are not handed out twice. +func (s *Server) handlePullMessages(w http.ResponseWriter, r *http.Request) { + device := deviceFromCtx(r.Context()) + deviceID := asString(device["id"]) + + res, err := s.pb.List(r.Context(), colMessages, pb.ListOptions{ + Filter: "device = " + pbQuote(deviceID) + " && status = " + pbQuote(statusPending), + Sort: "created", + PerPage: clampPerPage(queryInt(r, "limit", 20)), + }) + if err != nil { + writeUpstreamError(w, err) + return + } + + out := make([]messageDTO, 0, len(res.Items)) + for _, rec := range res.Items { + id := asString(rec["id"]) + updated, uerr := s.pb.Update(r.Context(), colMessages, id, pb.Record{"status": statusProcessed}) + if uerr != nil { + // Skip a message we couldn't claim rather than failing the pull. + continue + } + out = append(out, recordToMessage(updated)) + } + writeJSON(w, http.StatusOK, map[string]any{"items": out}) +} + +type reportRequest struct { + Status string `json:"status"` + Error string `json:"error"` +} + +// handleReportMessage records a delivery state reported by the device and fires +// the matching webhooks. +func (s *Server) handleReportMessage(w http.ResponseWriter, r *http.Request) { + device := deviceFromCtx(r.Context()) + id := r.PathValue("id") + + var req reportRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + if !validReportStates[req.Status] { + writeError(w, http.StatusBadRequest, "invalid status") + return + } + + rec, err := s.pb.GetOne(r.Context(), colMessages, id) + if err != nil { + writeUpstreamError(w, err) + return + } + if asString(rec["device"]) != asString(device["id"]) { + writeError(w, http.StatusForbidden, "message not assigned to this device") + return + } + + fields := pb.Record{"status": req.Status, "error": req.Error} + now := time.Now().UTC().Format("2006-01-02 15:04:05.000Z") + switch req.Status { + case statusSent: + fields["sent_at"] = now + case statusDelivered: + fields["delivered_at"] = now + } + updated, err := s.pb.Update(r.Context(), colMessages, id, fields) + if err != nil { + writeUpstreamError(w, err) + return + } + + // Dispatch webhooks for terminal/notable states. + if event := eventForStatus(req.Status); event != "" { + s.dispatchWebhooks(asString(device["owner"]), asString(device["id"]), event, map[string]any{ + "message_id": id, + "phone_numbers": asStringSlice(updated["phone_numbers"]), + "status": req.Status, + "error": req.Error, + }) + } + + writeJSON(w, http.StatusOK, recordToMessage(updated)) +} + +func eventForStatus(status string) string { + switch status { + case statusSent: + return "sms:sent" + case statusDelivered: + return "sms:delivered" + case statusFailed: + return "sms:failed" + default: + return "" + } +} + +func cleanPhones(in []string) []string { + out := make([]string, 0, len(in)) + for _, p := range in { + if p = strings.TrimSpace(p); p != "" { + out = append(out, p) + } + } + return out +} + +func clampPerPage(n int) int { + if n <= 0 { + return 50 + } + if n > 200 { + return 200 + } + return n +} diff --git a/API Server/internal/api/panel.go b/API Server/internal/api/panel.go new file mode 100644 index 0000000..add9942 --- /dev/null +++ b/API Server/internal/api/panel.go @@ -0,0 +1,23 @@ +package api + +import ( + "embed" + "io/fs" + "net/http" +) + +// The gsmnode web panel: a Vue 3 + Tailwind app (source in panel/, built +// with `npm run build` into dist/) embedded at compile time and served at +// the server root. +// +//go:embed all:dist +var panelFS embed.FS + +// panelHandler serves the built panel assets. +func panelHandler() http.Handler { + sub, err := fs.Sub(panelFS, "dist") + if err != nil { + panic(err) // embedded dist is malformed; unreachable in a valid build + } + return http.FileServerFS(sub) +} diff --git a/API Server/internal/api/respond.go b/API Server/internal/api/respond.go new file mode 100644 index 0000000..8ada244 --- /dev/null +++ b/API Server/internal/api/respond.go @@ -0,0 +1,65 @@ +package api + +import ( + "encoding/json" + "errors" + "io" + "log" + "net/http" + + "smsgateway/apiserver/internal/pb" +) + +// writeJSON writes v as a JSON response with the given status code. +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if v == nil { + return + } + if err := json.NewEncoder(w).Encode(v); err != nil { + log.Printf("writeJSON: %v", err) + } +} + +// errorBody is the standard error envelope. +type errorBody struct { + Error string `json:"error"` +} + +// writeError writes a JSON error response. +func writeError(w http.ResponseWriter, status int, msg string) { + writeJSON(w, status, errorBody{Error: msg}) +} + +// writeUpstreamError maps a PocketBase error onto an appropriate HTTP status. +func writeUpstreamError(w http.ResponseWriter, err error) { + var apiErr *pb.APIError + if errors.As(err, &apiErr) { + switch apiErr.Status { + case http.StatusNotFound: + writeError(w, http.StatusNotFound, "not found") + case http.StatusBadRequest: + writeError(w, http.StatusBadRequest, apiErr.Message) + case http.StatusForbidden: + writeError(w, http.StatusForbidden, "forbidden") + default: + log.Printf("upstream error: %v (%s)", apiErr, apiErr.Body) + writeError(w, http.StatusBadGateway, "upstream error") + } + return + } + log.Printf("internal error: %v", err) + writeError(w, http.StatusInternalServerError, "internal error") +} + +// decodeJSON reads and decodes a JSON request body into v. +func decodeJSON(r *http.Request, v any) error { + defer io.Copy(io.Discard, r.Body) + dec := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 1<<20)) + dec.DisallowUnknownFields() + if err := dec.Decode(v); err != nil { + return err + } + return nil +} diff --git a/API Server/internal/api/server.go b/API Server/internal/api/server.go new file mode 100644 index 0000000..862a8fd --- /dev/null +++ b/API Server/internal/api/server.go @@ -0,0 +1,243 @@ +package api + +import ( + "context" + "log" + "net/http" + "strings" + "time" + + "smsgateway/apiserver/internal/auth" + "smsgateway/apiserver/internal/config" + "smsgateway/apiserver/internal/pb" +) + +// Collection names in PocketBase. +const ( + colUsers = "users" + colDevices = "devices" + colMessages = "messages" + colInbox = "inbox" + colWebhooks = "webhooks" +) + +// Server wires together the HTTP handlers and their dependencies. +type Server struct { + cfg config.Config + pb *pb.Client + jwt *auth.Manager +} + +// New constructs a Server. +func New(cfg config.Config, client *pb.Client, jwt *auth.Manager) *Server { + return &Server{cfg: cfg, pb: client, jwt: jwt} +} + +// Handler returns the root HTTP handler with all routes registered. +func (s *Server) Handler() http.Handler { + mux := http.NewServeMux() + + // Web panel (public) — embedded Vue + Tailwind app. Only the explicit + // panel paths are routed to it so unknown /api/* paths still 404 as JSON. + panel := panelHandler() + mux.Handle("GET /{$}", panel) + mux.Handle("GET /assets/", panel) + mux.Handle("GET /favicon.svg", panel) + mux.Handle("GET /favicon-32.png", panel) + mux.Handle("GET /gsmnode-horizontal.png", panel) + mux.Handle("GET /gsmnode-horizontal-white.png", panel) + + // Health (public) + mux.HandleFunc("GET /api/health", s.handleHealth) + + // Client / 3rd-party API (JWT auth) — used by the Web App and integrators. + mux.HandleFunc("POST /api/auth/login", s.handleLogin) + mux.HandleFunc("POST /api/auth/refresh", s.requireUser(s.handleRefresh)) + mux.HandleFunc("GET /api/auth/me", s.requireUser(s.handleMe)) + + mux.HandleFunc("GET /api/devices", s.requireUser(s.handleListDevices)) + mux.HandleFunc("DELETE /api/devices/{id}", s.requireUser(s.handleDeleteDevice)) + + mux.HandleFunc("GET /api/messages", s.requireUser(s.handleListMessages)) + mux.HandleFunc("POST /api/messages", s.requireUser(s.handleEnqueueMessage)) + mux.HandleFunc("GET /api/messages/{id}", s.requireUser(s.handleGetMessage)) + + mux.HandleFunc("POST /api/calls", s.requireUser(s.handleEnqueueCall)) + + mux.HandleFunc("GET /api/inbox", s.requireUser(s.handleListInbox)) + + mux.HandleFunc("GET /api/webhooks", s.requireUser(s.handleListWebhooks)) + mux.HandleFunc("POST /api/webhooks", s.requireUser(s.handleCreateWebhook)) + mux.HandleFunc("DELETE /api/webhooks/{id}", s.requireUser(s.handleDeleteWebhook)) + + // Mobile / device API — the phone app registers, pulls work, and reports. + // Registration is authenticated with the user's JWT; everything else with + // the opaque device token returned at registration. + mux.HandleFunc("POST /api/mobile/v1/device", s.requireUser(s.handleRegisterDevice)) + mux.HandleFunc("POST /api/mobile/v1/ping", s.requireDevice(s.handlePing)) + mux.HandleFunc("GET /api/mobile/v1/messages", s.requireDevice(s.handlePullMessages)) + mux.HandleFunc("PATCH /api/mobile/v1/messages/{id}", s.requireDevice(s.handleReportMessage)) + mux.HandleFunc("POST /api/mobile/v1/inbox", s.requireDevice(s.handleReceiveSMS)) + + return s.withMiddleware(mux) +} + +// withMiddleware applies CORS, request logging, and panic recovery globally. +func (s *Server) withMiddleware(next http.Handler) http.Handler { + return s.recoverer(s.cors(s.logger(next))) +} + +func (s *Server) logger(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} + next.ServeHTTP(sw, r) + log.Printf("%s %s %d %s", r.Method, r.URL.Path, sw.status, time.Since(start).Round(time.Millisecond)) + }) +} + +func (s *Server) recoverer(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + log.Printf("panic: %v", rec) + writeError(w, http.StatusInternalServerError, "internal error") + } + }() + next.ServeHTTP(w, r) + }) +} + +func (s *Server) cors(next http.Handler) http.Handler { + allowed := map[string]bool{} + wildcard := false + for _, o := range s.cfg.AllowOrigins { + if o == "*" { + wildcard = true + } + allowed[o] = true + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin != "" && (wildcard || allowed[origin]) { + if wildcard { + w.Header().Set("Access-Control-Allow-Origin", "*") + } else { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Add("Vary", "Origin") + } + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + } + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} + +// statusWriter captures the response status code for logging. +type statusWriter struct { + http.ResponseWriter + status int + wrote bool +} + +func (w *statusWriter) WriteHeader(code int) { + if !w.wrote { + w.status = code + w.wrote = true + } + w.ResponseWriter.WriteHeader(code) +} + +func (w *statusWriter) Write(b []byte) (int, error) { + w.wrote = true + return w.ResponseWriter.Write(b) +} + +// --- auth context --- + +type ctxKey int + +const ( + ctxUser ctxKey = iota + ctxDevice +) + +// userFromCtx returns the authenticated user id/email from the request context. +func userFromCtx(ctx context.Context) (id, email string) { + if c, ok := ctx.Value(ctxUser).(*auth.Claims); ok { + return c.Subject, c.Email + } + return "", "" +} + +// deviceFromCtx returns the authenticated device record from the request context. +func deviceFromCtx(ctx context.Context) pb.Record { + if d, ok := ctx.Value(ctxDevice).(pb.Record); ok { + return d + } + return nil +} + +// requireUser wraps a handler to require a valid client JWT. +func (s *Server) requireUser(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := bearerToken(r) + if token == "" { + writeError(w, http.StatusUnauthorized, "missing bearer token") + return + } + claims, err := s.jwt.Verify(token) + if err != nil { + writeError(w, http.StatusUnauthorized, "invalid or expired token") + return + } + ctx := context.WithValue(r.Context(), ctxUser, claims) + next(w, r.WithContext(ctx)) + } +} + +// requireDevice wraps a handler to require a valid device token. The matching +// device record is loaded and attached to the request context. +func (s *Server) requireDevice(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := bearerToken(r) + if token == "" { + writeError(w, http.StatusUnauthorized, "missing device token") + return + } + device, err := s.pb.FindFirst(r.Context(), colDevices, + "auth_token = "+pbQuote(token), "") + if err != nil { + writeUpstreamError(w, err) + return + } + if device == nil { + writeError(w, http.StatusUnauthorized, "unknown device token") + return + } + ctx := context.WithValue(r.Context(), ctxDevice, device) + next(w, r.WithContext(ctx)) + } +} + +// bearerToken extracts a token from the Authorization header (with or without +// the "Bearer " prefix). +func bearerToken(r *http.Request) string { + h := r.Header.Get("Authorization") + if h == "" { + return "" + } + if after, ok := strings.CutPrefix(h, "Bearer "); ok { + return strings.TrimSpace(after) + } + return strings.TrimSpace(h) +} + +// pbQuote safely quotes a string value for a PocketBase filter expression. +func pbQuote(s string) string { + return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` +} diff --git a/API Server/internal/api/sweeper.go b/API Server/internal/api/sweeper.go new file mode 100644 index 0000000..8460f5c --- /dev/null +++ b/API Server/internal/api/sweeper.go @@ -0,0 +1,74 @@ +package api + +import ( + "context" + "log" + "time" + + "smsgateway/apiserver/internal/pb" +) + +// sweepInterval is how often stale messages are checked. +const sweepInterval = 30 * time.Second + +// StartExpiryWorker launches a background loop that fails messages/calls which +// no device picked up or reported on within the configured MessageTTL. Without +// it, a message targeting an offline device would stay Pending forever. +func (s *Server) StartExpiryWorker(ctx context.Context) { + go func() { + ticker := time.NewTicker(sweepInterval) + defer ticker.Stop() + // Run once at startup so stale items clear promptly. + s.expireStaleMessages(ctx) + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + s.expireStaleMessages(ctx) + } + } + }() +} + +// expireStaleMessages marks Pending/Processed messages older than MessageTTL as +// Failed. "Pending" means no online device pulled it; "Processed" means a device +// pulled it but never reported a terminal state (e.g. it crashed). +func (s *Server) expireStaleMessages(ctx context.Context) { + cutoff := time.Now().UTC().Add(-s.cfg.MessageTTL).Format("2006-01-02 15:04:05.000Z") + filter := `(status = "` + statusPending + `" || status = "` + statusProcessed + + `") && updated < "` + cutoff + `"` + + res, err := s.pb.List(ctx, colMessages, pb.ListOptions{Filter: filter, PerPage: 200}) + if err != nil { + log.Printf("expiry sweep failed: %v", err) + return + } + + expired := 0 + for _, rec := range res.Items { + // Don't expire messages scheduled for the future. + if asString(rec["schedule_at"]) != "" { + continue + } + id := asString(rec["id"]) + const reason = "expired: no device processed the message within the timeout" + if _, err := s.pb.Update(ctx, colMessages, id, pb.Record{ + "status": statusFailed, + "error": reason, + }); err != nil { + log.Printf("expire message %s: %v", id, err) + continue + } + expired++ + s.dispatchWebhooks(asString(rec["owner"]), asString(rec["device"]), "sms:failed", map[string]any{ + "message_id": id, + "type": asString(rec["type"]), + "status": statusFailed, + "error": reason, + }) + } + if expired > 0 { + log.Printf("expired %d stale message(s) older than %s", expired, s.cfg.MessageTTL) + } +} diff --git a/API Server/internal/api/util.go b/API Server/internal/api/util.go new file mode 100644 index 0000000..f280146 --- /dev/null +++ b/API Server/internal/api/util.go @@ -0,0 +1,65 @@ +package api + +import ( + "net/http" + "strconv" +) + +// asString coerces an arbitrary JSON value to a string. +func asString(v any) string { + switch t := v.(type) { + case string: + return t + case nil: + return "" + default: + return "" + } +} + +// asInt coerces a JSON number/string to an int. +func asInt(v any) int { + switch t := v.(type) { + case float64: + return int(t) + case int: + return t + case string: + if n, err := strconv.Atoi(t); err == nil { + return n + } + } + return 0 +} + +// asStringSlice coerces a JSON array (or single string) to []string. +func asStringSlice(v any) []string { + switch t := v.(type) { + case []any: + out := make([]string, 0, len(t)) + for _, e := range t { + if s, ok := e.(string); ok { + out = append(out, s) + } + } + return out + case []string: + return t + case string: + if t == "" { + return nil + } + return []string{t} + } + return nil +} + +// queryInt parses a query parameter as an int, falling back to def. +func queryInt(r *http.Request, key string, def int) int { + if v := r.URL.Query().Get(key); v != "" { + if n, err := strconv.Atoi(v); err == nil { + return n + } + } + return def +} diff --git a/API Server/internal/api/webhooks.go b/API Server/internal/api/webhooks.go new file mode 100644 index 0000000..97585de --- /dev/null +++ b/API Server/internal/api/webhooks.go @@ -0,0 +1,170 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "log" + "net/http" + "time" + + "smsgateway/apiserver/internal/pb" +) + +var validWebhookEvents = map[string]bool{ + "sms:received": true, + "sms:sent": true, + "sms:delivered": true, + "sms:failed": true, +} + +type webhookDTO struct { + ID string `json:"id"` + Event string `json:"event"` + URL string `json:"url"` + DeviceID string `json:"device_id,omitempty"` +} + +func recordToWebhook(rec pb.Record) webhookDTO { + return webhookDTO{ + ID: asString(rec["id"]), + Event: asString(rec["event"]), + URL: asString(rec["url"]), + DeviceID: asString(rec["device"]), + } +} + +// handleListWebhooks lists the user's registered webhooks. +func (s *Server) handleListWebhooks(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + res, err := s.pb.List(r.Context(), colWebhooks, pb.ListOptions{ + Filter: "owner = " + pbQuote(uid), + Sort: "-created", + PerPage: 200, + }) + if err != nil { + writeUpstreamError(w, err) + return + } + out := make([]webhookDTO, 0, len(res.Items)) + for _, rec := range res.Items { + out = append(out, recordToWebhook(rec)) + } + writeJSON(w, http.StatusOK, map[string]any{"items": out, "total": res.TotalItems}) +} + +type createWebhookRequest struct { + Event string `json:"event"` + URL string `json:"url"` + DeviceID string `json:"device_id"` +} + +// handleCreateWebhook registers a webhook for the user. +func (s *Server) handleCreateWebhook(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + + var req createWebhookRequest + if err := decodeJSON(r, &req); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + if !validWebhookEvents[req.Event] { + writeError(w, http.StatusBadRequest, "invalid event") + return + } + if req.URL == "" { + writeError(w, http.StatusBadRequest, "url is required") + return + } + + fields := pb.Record{"owner": uid, "event": req.Event, "url": req.URL} + if req.DeviceID != "" { + fields["device"] = req.DeviceID + } + rec, err := s.pb.Create(r.Context(), colWebhooks, fields) + if err != nil { + writeUpstreamError(w, err) + return + } + writeJSON(w, http.StatusCreated, recordToWebhook(rec)) +} + +// handleDeleteWebhook removes a webhook owned by the user. +func (s *Server) handleDeleteWebhook(w http.ResponseWriter, r *http.Request) { + uid, _ := userFromCtx(r.Context()) + id := r.PathValue("id") + + rec, err := s.pb.GetOne(r.Context(), colWebhooks, id) + if err != nil { + writeUpstreamError(w, err) + return + } + if asString(rec["owner"]) != uid { + writeError(w, http.StatusForbidden, "not your webhook") + return + } + if err := s.pb.Delete(r.Context(), colWebhooks, id); err != nil { + writeUpstreamError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// dispatchWebhooks delivers an event to every matching webhook for the owner. +// Delivery is best-effort and runs in the background so it never blocks the +// device/client request. +func (s *Server) dispatchWebhooks(owner, deviceID, event string, payload map[string]any) { + if owner == "" { + return + } + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + res, err := s.pb.List(ctx, colWebhooks, pb.ListOptions{ + Filter: "owner = " + pbQuote(owner) + " && event = " + pbQuote(event), + PerPage: 200, + }) + if err != nil { + log.Printf("webhook lookup failed for %s/%s: %v", owner, event, err) + return + } + + body := map[string]any{ + "event": event, + "device_id": deviceID, + "payload": payload, + "created_at": time.Now().UTC().Format(time.RFC3339), + } + raw, _ := json.Marshal(body) + + for _, hook := range res.Items { + // A webhook scoped to a specific device only fires for that device. + if dev := asString(hook["device"]); dev != "" && dev != deviceID { + continue + } + deliverWebhook(ctx, asString(hook["url"]), raw) + } + }() +} + +func deliverWebhook(ctx context.Context, url string, body []byte) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) + if err != nil { + log.Printf("webhook build request %s: %v", url, err) + return + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("User-Agent", "gsmnode-api/1.0") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + log.Printf("webhook delivery to %s failed: %v", url, err) + return + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + log.Printf("webhook %s returned %d", url, resp.StatusCode) + } +} diff --git a/API Server/internal/auth/jwt.go b/API Server/internal/auth/jwt.go new file mode 100644 index 0000000..135d238 --- /dev/null +++ b/API Server/internal/auth/jwt.go @@ -0,0 +1,64 @@ +// Package auth issues and verifies the client-facing JWTs used by the Web App +// and other integrators. +package auth + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// Claims is the JWT payload for an authenticated user session. +type Claims struct { + Email string `json:"email"` + Name string `json:"name,omitempty"` + jwt.RegisteredClaims +} + +// Manager issues and verifies JWTs. +type Manager struct { + secret []byte + ttl time.Duration +} + +// NewManager creates a JWT manager. +func NewManager(secret []byte, ttl time.Duration) *Manager { + return &Manager{secret: secret, ttl: ttl} +} + +// Issue creates a signed access token for the given user. +func (m *Manager) Issue(userID, email, name string) (token string, expiresAt time.Time, err error) { + now := time.Now() + expiresAt = now.Add(m.ttl) + claims := Claims{ + Email: email, + Name: name, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(expiresAt), + }, + } + t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signed, err := t.SignedString(m.secret) + return signed, expiresAt, err +} + +// Verify parses and validates a token, returning its claims. +func (m *Manager) Verify(token string) (*Claims, error) { + claims := &Claims{} + parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("unexpected signing method") + } + return m.secret, nil + }) + if err != nil { + return nil, err + } + if !parsed.Valid { + return nil, errors.New("invalid token") + } + return claims, nil +} diff --git a/API Server/internal/auth/token.go b/API Server/internal/auth/token.go new file mode 100644 index 0000000..8e21f4f --- /dev/null +++ b/API Server/internal/auth/token.go @@ -0,0 +1,16 @@ +package auth + +import ( + "crypto/rand" + "encoding/hex" +) + +// NewDeviceToken returns a cryptographically random opaque token used to +// authenticate a registered mobile device. +func NewDeviceToken() (string, error) { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/API Server/internal/config/config.go b/API Server/internal/config/config.go new file mode 100644 index 0000000..7c20824 --- /dev/null +++ b/API Server/internal/config/config.go @@ -0,0 +1,95 @@ +package config + +import ( + "log" + "os" + "strings" + "time" +) + +// Config holds all runtime configuration for the API Server. +type Config struct { + Addr string + PocketBaseURL string + PBAdminEmail string + PBAdminPass string + JWTSecret []byte + JWTAccessTTL time.Duration + AllowOrigins []string + MessageTTL time.Duration +} + +// Load reads configuration from environment variables, applying sensible +// defaults. A .env file, if present in the working directory, is loaded first. +func Load() Config { + loadDotEnv(".env") + + cfg := Config{ + Addr: getenv("API_ADDR", ":8080"), + PocketBaseURL: strings.TrimRight(getenv("POCKETBASE_URL", "http://10.2.1.10:8028"), "/"), + PBAdminEmail: getenv("PB_ADMIN_EMAIL", ""), + PBAdminPass: getenv("PB_ADMIN_PASSWORD", ""), + JWTSecret: []byte(getenv("JWT_SECRET", "dev-insecure-change-me-please")), + JWTAccessTTL: getdur("JWT_ACCESS_TTL", 24*time.Hour), + AllowOrigins: splitCSV(getenv("CORS_ALLOW_ORIGINS", "*")), + MessageTTL: getdur("MESSAGE_TTL", 5*time.Minute), + } + + if cfg.PBAdminEmail == "" || cfg.PBAdminPass == "" { + log.Println("WARNING: PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD are not set; PocketBase calls will fail") + } + return cfg +} + +func getenv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func getdur(key string, def time.Duration) time.Duration { + if v := os.Getenv(key); v != "" { + if d, err := time.ParseDuration(v); err == nil { + return d + } + log.Printf("invalid duration for %s=%q, using default %s", key, v, def) + } + return def +} + +func splitCSV(s string) []string { + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + if p = strings.TrimSpace(p); p != "" { + out = append(out, p) + } + } + return out +} + +// loadDotEnv loads KEY=VALUE pairs from a .env file into the process env if they +// are not already set. It is intentionally minimal (no quoting rules beyond +// trimming surrounding quotes). +func loadDotEnv(path string) { + data, err := os.ReadFile(path) + if err != nil { + return + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, val, ok := strings.Cut(line, "=") + if !ok { + continue + } + key = strings.TrimSpace(key) + val = strings.Trim(strings.TrimSpace(val), `"'`) + if _, exists := os.LookupEnv(key); !exists { + _ = os.Setenv(key, val) + } + } +} diff --git a/API Server/internal/pb/client.go b/API Server/internal/pb/client.go new file mode 100644 index 0000000..5097417 --- /dev/null +++ b/API Server/internal/pb/client.go @@ -0,0 +1,265 @@ +// Package pb is a thin REST client for PocketBase (v0.23+). The API Server is +// the only component that talks to PocketBase: it authenticates as a superuser +// and performs all CRUD on behalf of users, enforcing ownership in app logic. +package pb + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "sync" + "time" +) + +// Record is a generic PocketBase record (a JSON object). +type Record map[string]any + +// ListResult is the envelope returned by PocketBase list endpoints. +type ListResult struct { + Page int `json:"page"` + PerPage int `json:"perPage"` + TotalItems int `json:"totalItems"` + TotalPages int `json:"totalPages"` + Items []Record `json:"items"` +} + +// APIError is returned for non-2xx PocketBase responses. +type APIError struct { + Status int + Message string + Body string +} + +func (e *APIError) Error() string { + return fmt.Sprintf("pocketbase: %d %s", e.Status, e.Message) +} + +// NotFound reports whether err is a 404 from PocketBase. +func NotFound(err error) bool { + var ae *APIError + if e, ok := err.(*APIError); ok { + ae = e + } + return ae != nil && ae.Status == http.StatusNotFound +} + +// Client is a PocketBase REST client with automatic superuser token refresh. +type Client struct { + baseURL string + adminEmail string + adminPass string + http *http.Client + + mu sync.Mutex + token string + tokenExp time.Time +} + +// New creates a PocketBase client. +func New(baseURL, adminEmail, adminPass string) *Client { + return &Client{ + baseURL: baseURL, + adminEmail: adminEmail, + adminPass: adminPass, + http: &http.Client{Timeout: 15 * time.Second}, + } +} + +// AuthResult is returned by AuthWithPassword. +type AuthResult struct { + Token string `json:"token"` + Record Record `json:"record"` +} + +// authenticate logs in as a superuser and caches the token. +func (c *Client) authenticate(ctx context.Context) (string, error) { + c.mu.Lock() + defer c.mu.Unlock() + + if c.token != "" && time.Now().Before(c.tokenExp) { + return c.token, nil + } + + body := map[string]string{"identity": c.adminEmail, "password": c.adminPass} + var res AuthResult + if err := c.do(ctx, http.MethodPost, + "/api/collections/_superusers/auth-with-password", "", body, &res); err != nil { + return "", fmt.Errorf("superuser auth: %w", err) + } + c.token = res.Token + // PocketBase superuser tokens are long-lived; refresh conservatively. + c.tokenExp = time.Now().Add(10 * time.Minute) + return c.token, nil +} + +// AuthWithPassword verifies credentials against an auth collection (e.g. "users") +// and returns the user record. Used to validate logins; the API Server then +// issues its own JWT. +func (c *Client) AuthWithPassword(ctx context.Context, collection, identity, password string) (*AuthResult, error) { + token, err := c.authenticate(ctx) + if err != nil { + return nil, err + } + body := map[string]string{"identity": identity, "password": password} + var res AuthResult + path := "/api/collections/" + url.PathEscape(collection) + "/auth-with-password" + if err := c.do(ctx, http.MethodPost, path, token, body, &res); err != nil { + return nil, err + } + return &res, nil +} + +// Create inserts a record into a collection. +func (c *Client) Create(ctx context.Context, collection string, data any) (Record, error) { + token, err := c.authenticate(ctx) + if err != nil { + return nil, err + } + var out Record + path := "/api/collections/" + url.PathEscape(collection) + "/records" + if err := c.do(ctx, http.MethodPost, path, token, data, &out); err != nil { + return nil, err + } + return out, nil +} + +// Update patches a record. +func (c *Client) Update(ctx context.Context, collection, id string, data any) (Record, error) { + token, err := c.authenticate(ctx) + if err != nil { + return nil, err + } + var out Record + path := "/api/collections/" + url.PathEscape(collection) + "/records/" + url.PathEscape(id) + if err := c.do(ctx, http.MethodPatch, path, token, data, &out); err != nil { + return nil, err + } + return out, nil +} + +// Delete removes a record. +func (c *Client) Delete(ctx context.Context, collection, id string) error { + token, err := c.authenticate(ctx) + if err != nil { + return err + } + path := "/api/collections/" + url.PathEscape(collection) + "/records/" + url.PathEscape(id) + return c.do(ctx, http.MethodDelete, path, token, nil, nil) +} + +// GetOne fetches a single record by id. +func (c *Client) GetOne(ctx context.Context, collection, id string) (Record, error) { + token, err := c.authenticate(ctx) + if err != nil { + return nil, err + } + var out Record + path := "/api/collections/" + url.PathEscape(collection) + "/records/" + url.PathEscape(id) + if err := c.do(ctx, http.MethodGet, path, token, nil, &out); err != nil { + return nil, err + } + return out, nil +} + +// ListOptions controls a list query. +type ListOptions struct { + Filter string + Sort string + Expand string + Page int + PerPage int +} + +// List queries records with optional filter/sort/pagination. +func (c *Client) List(ctx context.Context, collection string, opt ListOptions) (*ListResult, error) { + token, err := c.authenticate(ctx) + if err != nil { + return nil, err + } + q := url.Values{} + if opt.Filter != "" { + q.Set("filter", opt.Filter) + } + if opt.Sort != "" { + q.Set("sort", opt.Sort) + } + if opt.Expand != "" { + q.Set("expand", opt.Expand) + } + if opt.Page > 0 { + q.Set("page", strconv.Itoa(opt.Page)) + } + if opt.PerPage > 0 { + q.Set("perPage", strconv.Itoa(opt.PerPage)) + } + path := "/api/collections/" + url.PathEscape(collection) + "/records?" + q.Encode() + var out ListResult + if err := c.do(ctx, http.MethodGet, path, token, nil, &out); err != nil { + return nil, err + } + return &out, nil +} + +// FindFirst returns the first record matching filter, or (nil, nil) if none. +func (c *Client) FindFirst(ctx context.Context, collection, filter, sort string) (Record, error) { + res, err := c.List(ctx, collection, ListOptions{Filter: filter, Sort: sort, PerPage: 1}) + if err != nil { + return nil, err + } + if len(res.Items) == 0 { + return nil, nil + } + return res.Items[0], nil +} + +// do performs an HTTP request against PocketBase and decodes the JSON response. +func (c *Client) do(ctx context.Context, method, path, token string, body, out any) error { + var reader io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return err + } + reader = bytes.NewReader(b) + } + + req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader) + if err != nil { + return err + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if token != "" { + req.Header.Set("Authorization", token) + } + + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + raw, _ := io.ReadAll(resp.Body) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + msg := http.StatusText(resp.StatusCode) + var pbErr struct { + Message string `json:"message"` + } + if json.Unmarshal(raw, &pbErr) == nil && pbErr.Message != "" { + msg = pbErr.Message + } + return &APIError{Status: resp.StatusCode, Message: msg, Body: string(raw)} + } + if out != nil && len(raw) > 0 { + if err := json.Unmarshal(raw, out); err != nil { + return fmt.Errorf("decode response: %w", err) + } + } + return nil +} diff --git a/API Server/panel/index.html b/API Server/panel/index.html new file mode 100644 index 0000000..2fd2777 --- /dev/null +++ b/API Server/panel/index.html @@ -0,0 +1,15 @@ + + + + + + + + + gsmnode · API Server + + +
+ + + diff --git a/API Server/panel/package-lock.json b/API Server/panel/package-lock.json new file mode 100644 index 0000000..8cbc07c --- /dev/null +++ b/API Server/panel/package-lock.json @@ -0,0 +1,1974 @@ +{ + "name": "gsmnode-api-panel", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gsmnode-api-panel", + "version": "1.0.0", + "dependencies": { + "@lucide/vue": "^1.23.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "tailwindcss": "^4.0.0", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lucide/vue": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.23.0.tgz", + "integrity": "sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/API Server/panel/package.json b/API Server/panel/package.json new file mode 100644 index 0000000..0697fa5 --- /dev/null +++ b/API Server/panel/package.json @@ -0,0 +1,21 @@ +{ + "name": "gsmnode-api-panel", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@lucide/vue": "^1.23.0", + "vue": "^3.5.13" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "tailwindcss": "^4.0.0", + "vite": "^6.0.7" + } +} diff --git a/API Server/panel/public/favicon-32.png b/API Server/panel/public/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..23301f08f08ab2a23fe5b940a8994bf2f3cce797 GIT binary patch literal 759 zcmV|H~v=l|9uC}?_AD(96|*7 zw=H_X&#Yir4WbnU=y~%$V8AdKdk>;^pD5P!9IYP;_b7Z(&{(;f1?UzL(!;fxfHAnh z0_BdwwU4A66foHY8z?!&8kd_7yAj6sLITL85af7NG7}GECJQL ztRf6JMCxP2ZJH=W{H4#fgHa*B|xGw?haX%DPhm=6Ns%*!S`{P>#p zDiAtc*umo4O9zjM@ zDvmeTq4kp^ST`i=f>QULZE$&ihDIwxvB18(ji_F~KlqqQyIpRTeeMA~?R&?w9Et^6 z4_ri=M$5AtqZ7Zd>7JEmITQ=zr!U6UZO4$QO+&{(FB&?Z!RHrwKay`A7ru-Q^DKvA z0STQ(i<&LvSeuoL(=YDeN$*?Un=VlaNJx*_ss%=L4))=9z|ZT`C2E0KGbca=wn{gM zbo`#EN$8jC1At{}&Kp7Z?EjW8y=%@AM4=Zz68BO82IggcA$#_ZX5qz}8Y=dH;%qe2 zhf`pfh?RDgUVkiQE+COxXU@ + + + + + + + + \ No newline at end of file diff --git a/API Server/panel/public/gsmnode-horizontal-white.png b/API Server/panel/public/gsmnode-horizontal-white.png new file mode 100644 index 0000000000000000000000000000000000000000..ac100c0a094d3ff30d5db56e0794c27c2a02a455 GIT binary patch literal 11104 zcmeHt_gholvu`LOQbd|aQ;L8@dJ(0IfFd=J&_P6s1P#(fTBu4Fkluqp2u%c}7X^e+ zMS2Ml2}P=a5FnJhd0+4MoOAzzd!Oh2!ro!;H8X40%$}LgOoV~neR>)W8W0FXuk}FP z5Coz?1NT3user#1QQFlY5D!R8{Vw#S^*S!_t(~LQ=GJDdZwMl17JQS8m$8}hq1X)* zW<=;)yUP*j9~3w#;iKAy$jB|WIq)S%YE5ctEeATrP>=@FSAgoF7UR1#dTev>Zk8skT4vAsBh5q25$FB+<7c-ZRmWzC6D*_V&KIP`PAFg2!GB@2@ z?wGEA;HDdWrf>co353k^$M-&0U^c^0RBOs_H*0DTDql zASYuz`~M|Ma`4%ehnn*L6Zn5k2NI|~oI7rEx8Tr)q!WnP99h7g2T@a=ove-e`lD2T zGhITy@^DT@>QDJgsxNL!##DMW`j@YN?BXbrX(`1-J!vWifz}=XM?W8qln?(|x2vMQ z&>GQAB;L~3aNvkp(eTuXX1=Q;J0%Fj zABCi`nYm=p^`Oyzn}*q+Xq;guCSN}|mL$4G&-6}40o7}+Z zRulpT%tQBA1VQt$S17TbTB+x;lQl7#J8)Qy9UTZ1O-Bw!y53XeW{y)nTsBHk(0

9Qi9WAHmlgG0V9aF~KJQ`x}4R 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 literal 0 HcmV?d00001 diff --git a/API Server/panel/public/gsmnode-horizontal.png b/API Server/panel/public/gsmnode-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd2937882be756086acb72136a40562e3a0afc4 GIT binary patch literal 11370 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 literal 0 HcmV?d00001 diff --git a/API Server/panel/src/App.vue b/API Server/panel/src/App.vue new file mode 100644 index 0000000..c8218fc --- /dev/null +++ b/API Server/panel/src/App.vue @@ -0,0 +1,111 @@ + + + diff --git a/API Server/panel/src/components/EndpointTable.vue b/API Server/panel/src/components/EndpointTable.vue new file mode 100644 index 0000000..4dd8fe2 --- /dev/null +++ b/API Server/panel/src/components/EndpointTable.vue @@ -0,0 +1,40 @@ + + + diff --git a/API Server/panel/src/main.js b/API Server/panel/src/main.js new file mode 100644 index 0000000..40dbaaa --- /dev/null +++ b/API Server/panel/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import "./style.css"; +import "./theme"; + +createApp(App).mount("#app"); diff --git a/API Server/panel/src/style.css b/API Server/panel/src/style.css new file mode 100644 index 0000000..1c712a9 --- /dev/null +++ b/API Server/panel/src/style.css @@ -0,0 +1,336 @@ +/* gsmnode design system — tokens from Design/SMS Gateway logo design/tokens/*, + mapped into Tailwind v4. Signal-green + ink, developer-tool aesthetic. */ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap'); +@import "tailwindcss"; + +/* ============================================================ + RAW RAMPS + SEMANTIC ALIASES (light) + ============================================================ */ +:root { + /* Signal green (brand) — shared chroma, stepped lightness */ + --green-100: oklch(0.95 0.04 152); + --green-200: oklch(0.88 0.07 152); + --green-300: oklch(0.78 0.11 152); + --green-500: oklch(0.60 0.14 152); /* primary */ + --green-600: oklch(0.52 0.13 152); + --green-700: oklch(0.44 0.11 152); + --green-on-dark: oklch(0.72 0.14 152); + + /* Ink & paper */ + --ink: #12161C; + --ink-900: #0A0D11; + --ink-800: #1A1F27; + --paper: #FAFAF9; + --white: #FFFFFF; + + /* Cool gray ramp */ + --gray-50: #F4F5F4; + --gray-100: #E9EBEA; + --gray-200: #DCDFDE; + --gray-300: #C2C7C6; + --gray-400: #9AA0A2; + --gray-500: #6B7278; + --gray-600: #4A5157; + --gray-700: #333A40; + + /* Semantic status */ + --success: oklch(0.60 0.14 152); /* green — also "delivered" */ + --warning: oklch(0.72 0.15 75); + --danger: oklch(0.60 0.17 25); + --info: oklch(0.62 0.13 245); + --success-x: oklch(0.95 0.04 152); + --warning-x: oklch(0.95 0.05 80); + --danger-x: oklch(0.95 0.05 25); + --info-x: oklch(0.95 0.04 245); + + /* Semantic aliases — reference these in components */ + --bg-page: var(--paper); + --bg-sunken: var(--gray-50); + --surface-card: var(--white); + --surface-raised: var(--white); + --surface-inverse:var(--ink); + --surface-code: var(--ink-900); + + --border-subtle: rgba(18, 22, 28, 0.08); + --border-strong: rgba(18, 22, 28, 0.20); + --border-focus: var(--green-500); + + --text-primary: var(--ink); + --text-secondary: var(--gray-700); + --text-muted: var(--gray-500); + --text-inverse: #F5F6F4; + --text-brand: var(--green-600); + --text-link: var(--green-600); + + --brand: var(--green-500); + --brand-hover: var(--green-600); + --brand-active: var(--green-700); + --brand-tint: var(--green-100); + --brand-contrast: var(--white); + --brand-on-dark: var(--green-on-dark); + + --success-tint: var(--success-x); + --warning-tint: var(--warning-x); + --danger-tint: var(--danger-x); + --info-tint: var(--info-x); + + --ring-focus: 0 0 0 3px oklch(0.60 0.14 152 / 0.45); + --ring-danger: 0 0 0 3px oklch(0.60 0.17 25 / 0.35); + + /* Flat, low-contrast elevation */ + --sh-xs: 0 1px 2px rgba(18, 22, 28, 0.04); + --sh-sm: 0 1px 2px rgba(18, 22, 28, 0.06), 0 1px 1px rgba(18, 22, 28, 0.04); + --sh-md: 0 4px 12px rgba(18, 22, 28, 0.08); + --sh-lg: 0 8px 24px rgba(18, 22, 28, 0.14); + --sh-xl: 0 16px 40px rgba(18, 22, 28, 0.18); + + --dur-instant: 100ms; + --dur-fast: 120ms; + --dur-base: 180ms; + --dur-slow: 260ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-entrance: cubic-bezier(0.16, 1, 0.3, 1); +} + +/* ============================================================ + DARK THEME — only the semantic layer remaps. + Namespaced data-gsm-theme so it matches the design system. + ============================================================ */ +[data-gsm-theme="dark"] { + --bg-page: var(--ink-900); + --bg-sunken: #0E1216; + --surface-card: #14191F; + --surface-raised: #1A2027; + --surface-inverse:var(--white); + --surface-code: #05070A; + + --border-subtle: rgba(255, 255, 255, 0.07); + --border-strong: rgba(255, 255, 255, 0.18); + --border-focus: var(--green-on-dark); + + --text-primary: #F5F6F4; + --text-secondary: var(--gray-200); + --text-muted: var(--gray-400); + --text-inverse: var(--ink); + --text-brand: var(--green-on-dark); + --text-link: var(--green-on-dark); + + --brand: var(--green-500); + --brand-hover: var(--green-on-dark); + --brand-active: var(--green-600); + --brand-tint: oklch(0.60 0.14 152 / 0.16); + --brand-contrast: var(--white); + + --success: var(--green-300); + --warning: oklch(0.80 0.14 80); + --danger: oklch(0.72 0.16 25); + --info: oklch(0.72 0.12 245); + --success-tint: oklch(0.60 0.14 152 / 0.16); + --warning-tint: oklch(0.72 0.15 75 / 0.16); + --danger-tint: oklch(0.60 0.17 25 / 0.18); + --info-tint: oklch(0.62 0.13 245 / 0.16); + + --ring-focus: 0 0 0 3px oklch(0.72 0.14 152 / 0.45); + + --sh-xs: 0 1px 2px rgba(0, 0, 0, 0.4); + --sh-sm: 0 1px 3px rgba(0, 0, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); + --sh-md: 0 4px 12px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.4); + --sh-lg: 0 8px 24px rgba(0, 0, 0, 0.6), 0 4px 8px rgba(0, 0, 0, 0.4); + --sh-xl: 0 16px 40px rgba(0, 0, 0, 0.7); + + color-scheme: dark; +} + +/* ============================================================ + TAILWIND THEME — utilities resolve to the semantic vars, + so everything flips automatically under data-gsm-theme="dark". + ============================================================ */ +@theme inline { + --color-*: initial; + + --color-page: var(--bg-page); + --color-sunken: var(--bg-sunken); + --color-card: var(--surface-card); + --color-raised: var(--surface-raised); + --color-inverse: var(--surface-inverse); + --color-code: var(--surface-code); + + --color-subtle: var(--border-subtle); + --color-strong: var(--border-strong); + + --color-primary: var(--text-primary); + --color-secondary: var(--text-secondary); + --color-muted: var(--text-muted); + --color-on-brand: var(--brand-contrast); + --color-link: var(--text-link); + + --color-brand: var(--brand); + --color-brand-hover: var(--brand-hover); + --color-brand-active: var(--brand-active); + --color-brand-tint: var(--brand-tint); + --color-brand-text: var(--text-brand); + + --color-success: var(--success); + --color-success-tint: var(--success-tint); + --color-warning: var(--warning); + --color-warning-tint: var(--warning-tint); + --color-danger: var(--danger); + --color-danger-tint: var(--danger-tint); + --color-info: var(--info); + --color-info-tint: var(--info-tint); + + --color-white: #FFFFFF; + --color-ink: var(--ink); + --color-green: var(--green-500); + + --font-display: 'Space Grotesk', ui-sans-serif, system-ui, 'Segoe UI', sans-serif; + --font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace; + + --radius-*: initial; + --radius-xs: 4px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 15px; + --radius-2xl: 18px; + --radius-full: 9999px; + + --shadow-*: initial; + --shadow-xs: var(--sh-xs); + --shadow-sm: var(--sh-sm); + --shadow-md: var(--sh-md); + --shadow-lg: var(--sh-lg); + --shadow-xl: var(--sh-xl); + --shadow-ring: var(--ring-focus); +} + +/* ============================================================ + BASE + ============================================================ */ +html, +body, +#app { + height: 100%; +} + +body { + font-family: var(--font-sans); + background: var(--bg-page); + color: var(--text-primary); + -webkit-font-smoothing: antialiased; +} + +/* Display face for titles & metrics */ +h1, h2, h3 { + font-family: var(--font-display); + letter-spacing: -0.02em; +} + +/* Shared control styles (inputs, selects, textareas) */ +@utility gn-input { + width: 100%; + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-card); + color: var(--text-primary); + padding: 0 12px; + height: 40px; + font-size: 0.875rem; + font-family: var(--font-sans); + transition: border-color var(--dur-fast) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); +} + +@utility gn-textarea { + width: 100%; + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-card); + color: var(--text-primary); + padding: 10px 12px; + font-size: 0.875rem; + font-family: var(--font-sans); + resize: vertical; + transition: border-color var(--dur-fast) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); +} + +.gn-input:focus, +.gn-textarea:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: var(--ring-focus); +} + +.gn-input::placeholder, +.gn-textarea::placeholder { + color: var(--text-muted); +} + +/* Primary button — flat signal-green, no glow (infrastructure) */ +@utility gn-btn-pri { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 40px; + padding: 0 16px; + border-radius: var(--radius-md); + border: 1px solid transparent; + background: var(--brand); + color: var(--brand-contrast); + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease-standard), + transform var(--dur-fast) var(--ease-standard); +} + +.gn-btn-pri:hover:not(:disabled) { background: var(--brand-hover); } +.gn-btn-pri:active:not(:disabled) { background: var(--brand-active); transform: translateY(1px); } +.gn-btn-pri:focus-visible { outline: none; box-shadow: var(--ring-focus); } +.gn-btn-pri:disabled { opacity: 0.45; cursor: not-allowed; } + +/* Secondary button */ +@utility gn-btn-sec { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 40px; + padding: 0 16px; + border-radius: var(--radius-md); + border: 1px solid var(--border-strong); + background: var(--surface-card); + color: var(--text-primary); + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease-standard), + transform var(--dur-fast) var(--ease-standard); +} + +.gn-btn-sec:hover:not(:disabled) { background: var(--bg-sunken); } +.gn-btn-sec:active:not(:disabled) { transform: translateY(1px); } +.gn-btn-sec:focus-visible { outline: none; box-shadow: var(--ring-focus); } +.gn-btn-sec:disabled { opacity: 0.45; cursor: not-allowed; } + +/* Small button size modifier */ +@utility gn-btn-sm { + height: 32px; + padding: 0 12px; + font-size: 0.75rem; + border-radius: var(--radius-sm); +} + +/* Mono eyebrow — uppercase, tracked out */ +@utility gn-eyebrow { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-muted); +} diff --git a/API Server/panel/src/theme.js b/API Server/panel/src/theme.js new file mode 100644 index 0000000..b8dae4d --- /dev/null +++ b/API Server/panel/src/theme.js @@ -0,0 +1,30 @@ +import { ref } from "vue"; + +// Persisted light/dark theme, shared key with the design-system kits. +const KEY = "gsmnode-theme"; + +function initial() { + try { + return localStorage.getItem(KEY) === "dark" ? "dark" : "light"; + } catch { + return "light"; + } +} + +export const theme = ref(initial()); + +export function applyTheme(t) { + theme.value = t; + document.documentElement.setAttribute("data-gsm-theme", t); + try { + localStorage.setItem(KEY, t); + } catch { + /* private mode — theme just won't persist */ + } +} + +export function toggleTheme() { + applyTheme(theme.value === "dark" ? "light" : "dark"); +} + +applyTheme(theme.value); diff --git a/API Server/panel/vite.config.js b/API Server/panel/vite.config.js new file mode 100644 index 0000000..d5478de --- /dev/null +++ b/API Server/panel/vite.config.js @@ -0,0 +1,20 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import tailwindcss from "@tailwindcss/vite"; + +// Builds into internal/api/dist, which the Go server embeds via go:embed +// and serves at the server root. +export default defineConfig({ + plugins: [vue(), tailwindcss()], + build: { + outDir: "../internal/api/dist", + emptyOutDir: true, + }, + server: { + port: 5174, + proxy: { + // Dev-mode proxy to a locally running API Server. + "/api": "http://localhost:8080", + }, + }, +}); diff --git a/API Server/scripts/Run-ApiServer.ps1 b/API Server/scripts/Run-ApiServer.ps1 new file mode 100644 index 0000000..4c1adae --- /dev/null +++ b/API Server/scripts/Run-ApiServer.ps1 @@ -0,0 +1,25 @@ +# Runs the SMS Gateway API Server. +# Loads .env (if present), then starts the Go server. +# +# ./scripts/Run-ApiServer.ps1 + +$ErrorActionPreference = "Stop" +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$root = Split-Path -Parent $here # the "API Server" folder + +Push-Location $root +try { + # Ensure Go is on PATH for this session. + $goBin = "C:\Program Files\Go\bin" + if (Test-Path $goBin) { $env:Path = "$goBin;$env:Path" } + + if (-not (Get-Command go -ErrorAction SilentlyContinue)) { + throw "Go is not installed or not on PATH." + } + + Write-Host "Starting API Server (Ctrl+C to stop)..." -ForegroundColor Cyan + go run ./cmd/server +} +finally { + Pop-Location +} diff --git a/API Server/scripts/create-user.mjs b/API Server/scripts/create-user.mjs new file mode 100644 index 0000000..2d6ecfe --- /dev/null +++ b/API Server/scripts/create-user.mjs @@ -0,0 +1,50 @@ +// Creates a user in the PocketBase "users" collection to log in with. +// +// Usage (PowerShell): +// $env:POCKETBASE_URL="http://10.2.1.10:8028" +// $env:PB_ADMIN_EMAIL="admin@example.com" +// $env:PB_ADMIN_PASSWORD="admin-password" +// node scripts/create-user.mjs user@example.com "user-password" "Display Name" + +const BASE = (process.env.POCKETBASE_URL || "http://10.2.1.10:8028").replace(/\/$/, ""); +const EMAIL = process.env.PB_ADMIN_EMAIL; +const PASSWORD = process.env.PB_ADMIN_PASSWORD; + +const [, , userEmail, userPassword, userName] = process.argv; + +if (!EMAIL || !PASSWORD) { + console.error("Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD environment variables."); + process.exit(1); +} +if (!userEmail || !userPassword) { + console.error('Usage: node scripts/create-user.mjs ["name"]'); + process.exit(1); +} + +async function api(method, path, body, token) { + const res = await fetch(BASE + path, { + method, + headers: { "Content-Type": "application/json", ...(token ? { Authorization: token } : {}) }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + const json = text ? JSON.parse(text) : null; + if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${json?.message || text}`); + return json; +} + +const auth = await api("POST", "/api/collections/_superusers/auth-with-password", { + identity: EMAIL, + password: PASSWORD, +}); + +const user = await api("POST", "/api/collections/users/records", { + email: userEmail, + password: userPassword, + passwordConfirm: userPassword, + name: userName || "", + emailVisibility: true, + verified: true, +}, auth.token); + +console.log(`Created user ${user.email} (id: ${user.id}).`); diff --git a/API Server/scripts/setup-pocketbase.mjs b/API Server/scripts/setup-pocketbase.mjs new file mode 100644 index 0000000..b93e667 --- /dev/null +++ b/API Server/scripts/setup-pocketbase.mjs @@ -0,0 +1,194 @@ +// Sets up the PocketBase collections the API Server expects. +// +// Usage (PowerShell): +// $env:POCKETBASE_URL="http://10.2.1.10:8028" +// $env:PB_ADMIN_EMAIL="you@example.com" +// $env:PB_ADMIN_PASSWORD="your-password" +// node scripts/setup-pocketbase.mjs +// +// It is idempotent: existing collections are updated, missing ones created. +// Requires Node 18+ (uses global fetch). No npm install needed. + +const BASE = (process.env.POCKETBASE_URL || "http://10.2.1.10:8028").replace(/\/$/, ""); +const EMAIL = process.env.PB_ADMIN_EMAIL; +const PASSWORD = process.env.PB_ADMIN_PASSWORD; + +if (!EMAIL || !PASSWORD) { + console.error("Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD environment variables."); + process.exit(1); +} + +let token = ""; + +async function api(method, path, body) { + const res = await fetch(BASE + path, { + method, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: token } : {}), + }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + let json = null; + try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ } + if (!res.ok) { + const msg = json?.message || text || res.statusText; + throw new Error(`${method} ${path} -> ${res.status}: ${msg}`); + } + return json; +} + +async function authenticate() { + const res = await api("POST", "/api/collections/_superusers/auth-with-password", { + identity: EMAIL, + password: PASSWORD, + }); + token = res.token; + console.log("Authenticated as superuser."); +} + +async function getCollections() { + const res = await api("GET", "/api/collections?perPage=200"); + const byName = {}; + for (const c of res.items) byName[c.name] = c; + return byName; +} + +// Field builders for PocketBase v0.23+ (collections use a `fields` array). +const f = { + text: (name, opts = {}) => ({ name, type: "text", required: false, ...opts }), + number: (name, opts = {}) => ({ name, type: "number", required: false, ...opts }), + date: (name, opts = {}) => ({ name, type: "date", required: false, ...opts }), + json: (name, opts = {}) => ({ name, type: "json", required: false, maxSize: 2000000, ...opts }), + url: (name, opts = {}) => ({ name, type: "url", required: false, ...opts }), + select: (name, values, opts = {}) => ({ + name, type: "select", required: false, maxSelect: 1, values, ...opts, + }), + relation: (name, collectionId, opts = {}) => ({ + name, type: "relation", required: false, collectionId, + cascadeDelete: false, maxSelect: 1, minSelect: 0, ...opts, + }), + autodate: (name, opts) => ({ name, type: "autodate", onCreate: true, onUpdate: false, ...opts }), +}; + +// Superuser-only API rules: only the API Server (acting as superuser) reads or +// writes these collections. Clients never touch PocketBase directly. +const LOCKED = { listRule: null, viewRule: null, createRule: null, updateRule: null, deleteRule: null }; + +function definitions(ids) { + return [ + { + name: "devices", + type: "base", + ...LOCKED, + fields: [ + f.text("device_id", { required: true }), + f.text("name"), + f.text("platform"), + f.text("app_version"), + f.text("push_token"), + f.text("auth_token", { required: true }), + f.select("status", ["online", "offline"]), + f.date("last_seen_at"), + f.relation("owner", ids.users, { required: true, cascadeDelete: true }), + f.autodate("created", { onCreate: true, onUpdate: false }), + f.autodate("updated", { onCreate: true, onUpdate: true }), + ], + indexes: [ + "CREATE UNIQUE INDEX idx_devices_auth_token ON devices (auth_token)", + "CREATE UNIQUE INDEX idx_devices_owner_device ON devices (owner, device_id)", + ], + }, + { + name: "messages", + type: "base", + ...LOCKED, + fields: [ + f.json("phone_numbers", { required: true }), + f.text("text_message"), + f.select("type", ["sms", "call"]), + f.number("sim_number"), + f.select("status", ["Pending", "Processed", "Sent", "Delivered", "Failed"]), + f.text("error"), + f.date("schedule_at"), + f.date("sent_at"), + f.date("delivered_at"), + f.relation("device", ids.devices, { cascadeDelete: false }), + f.relation("owner", ids.users, { required: true, cascadeDelete: true }), + f.autodate("created", { onCreate: true, onUpdate: false }), + f.autodate("updated", { onCreate: true, onUpdate: true }), + ], + indexes: [ + "CREATE INDEX idx_messages_device_status ON messages (device, status)", + "CREATE INDEX idx_messages_owner ON messages (owner)", + ], + }, + { + name: "inbox", + type: "base", + ...LOCKED, + fields: [ + f.text("phone_number", { required: true }), + f.text("message"), + f.date("received_at"), + f.relation("device", ids.devices, { cascadeDelete: false }), + f.relation("owner", ids.users, { required: true, cascadeDelete: true }), + f.autodate("created", { onCreate: true, onUpdate: false }), + ], + indexes: ["CREATE INDEX idx_inbox_owner ON inbox (owner)"], + }, + { + name: "webhooks", + type: "base", + ...LOCKED, + fields: [ + f.select("event", ["sms:received", "sms:sent", "sms:delivered", "sms:failed"]), + f.url("url", { required: true }), + f.relation("device", ids.devices, { cascadeDelete: false }), + f.relation("owner", ids.users, { required: true, cascadeDelete: true }), + f.autodate("created", { onCreate: true, onUpdate: false }), + ], + indexes: ["CREATE INDEX idx_webhooks_owner_event ON webhooks (owner, event)"], + }, + ]; +} + +async function main() { + await authenticate(); + + let collections = await getCollections(); + if (!collections.users) { + throw new Error('The default "users" auth collection was not found in PocketBase.'); + } + + // Resolve collection ids needed for relation fields. devices must exist before + // messages/inbox/webhooks reference it, so create in order, refreshing ids. + const order = ["devices", "messages", "inbox", "webhooks"]; + for (const name of order) { + const ids = { + users: collections.users.id, + devices: collections.devices?.id, + }; + const def = definitions(ids).find((d) => d.name === name); + + if (collections[name]) { + await api("PATCH", `/api/collections/${collections[name].id}`, def); + console.log(`Updated collection: ${name}`); + } else { + await api("POST", "/api/collections", def); + console.log(`Created collection: ${name}`); + } + collections = await getCollections(); // refresh so later relations resolve + } + + console.log("\nPocketBase setup complete."); + console.log("Collections: users (existing), devices, messages, inbox, webhooks."); + console.log("\nNext: create a user to log in with, e.g. via the PocketBase admin UI,"); + console.log("or run scripts/create-user.mjs."); +} + +main().catch((err) => { + console.error("\nSetup failed:", err.message); + process.exit(1); +}); diff --git a/Home Assistant Plugin/README.md b/Home Assistant Plugin/README.md new file mode 100644 index 0000000..4e86f9a --- /dev/null +++ b/Home Assistant Plugin/README.md @@ -0,0 +1,133 @@ +# gsmnode — Home Assistant Integration + +A custom Home Assistant integration that sends SMS **and places phone calls** +through the gsmnode **API Server**. It can be added and configured entirely +from the Home Assistant UI. + +``` +Home Assistant ──► API Server (/api/messages, /api/calls) ──► your phone ──► SMS / call +``` + +Like every other client, it talks **only** to the API Server (never PocketBase). + +## What you get + +- **UI setup** (config flow) — add it under *Settings → Devices & Services*. +- **Services** — `gsmnode.send_sms` and `gsmnode.call`, with field + pickers in the automation editor and *Developer Tools → Actions*. +- **Sensor** — `binary_sensor` "API Server" (connectivity) so you can see and + automate on the gateway being up/down. + +## Install + +1. Copy the integration folder into your Home Assistant config directory so it + lands at: + + ``` + /custom_components/gsmnode/ + ``` + + (Copy this repo's `custom_components/gsmnode/` next to your + `configuration.yaml`. Use the Samba / File editor / SSH add-on, or the + mapped volume for Docker.) + +2. **Restart Home Assistant** (Settings → System → Restart). + +## Add it from the UI + +1. **Settings → Devices & Services → Add Integration**. +2. Search for **gsmnode**. +3. Fill in the form: + - **API Server URL** — e.g. `http://10.2.1.101:8080` (must be reachable from HA) + - **Email** / **Password** — a gateway user (create one with + `node "API Server/scripts/create-user.mjs" ha@local "pass" "Home Assistant"`) + - **Default device ID** *(optional)* — pin sends/calls to a specific phone + + The form validates by logging in; you'll get *Invalid auth* or *Cannot + connect* if something's wrong. + +A **gsmnode** device appears with an **API Server** connectivity sensor. + +## Use it + +### Send an SMS + +```yaml +action: gsmnode.send_sms +data: + phone_numbers: ["+15551234567"] + message: "Hello from Home Assistant" + device_id: my-phone # optional + sim_number: 1 # optional (dual-SIM) +``` + +### Place a call + +```yaml +action: gsmnode.call +data: + phone_number: "+15551234567" + device_id: my-phone # optional +``` + +### Example automation + +```yaml +automation: + - alias: "Water leak — text then call" + trigger: + - platform: state + entity_id: binary_sensor.basement_leak + to: "on" + action: + - action: gsmnode.send_sms + data: + phone_numbers: ["+15551234567"] + message: "Water leak detected in the basement!" + - action: gsmnode.call + data: + phone_number: "+15551234567" +``` + +### React to the gateway going offline + +```yaml +automation: + - alias: "Alert if SMS gateway API is down" + trigger: + - platform: state + entity_id: binary_sensor.gsmnode_api_server + to: "off" + for: "00:02:00" + action: + - action: persistent_notification.create + data: + title: "gsmnode" + message: "The API Server is unreachable." +``` + +## Receiving SMS in Home Assistant + +To **receive** incoming texts, register the API Server's `sms:received` webhook +against a HA webhook trigger. A ready-to-use snippet is in +[`configuration.example.yaml`](configuration.example.yaml). + +## Legacy YAML `notify` platform (optional) + +A `notify.gsmnode` service is still available for those who prefer YAML +(`notify.py`). It's independent of the UI integration — see +[`configuration.example.yaml`](configuration.example.yaml). For new setups, the +UI integration above is recommended. + +## How it works + +- On first send it logs in (`POST /api/auth/login`) and caches the JWT; on a + `401` it re-logs in once and retries. +- `send_sms` → `POST /api/messages`; `call` → `POST /api/calls`. +- The sensor polls `GET /api/health` every 30s. +- All HTTP uses Home Assistant's shared aiohttp session (fully async). + +## Notes / limitations + +- The API Server must be reachable from the Home Assistant host. +- No external dependencies (`requirements: []`). diff --git a/Home Assistant Plugin/configuration.example.yaml b/Home Assistant Plugin/configuration.example.yaml new file mode 100644 index 0000000..f45cc44 --- /dev/null +++ b/Home Assistant Plugin/configuration.example.yaml @@ -0,0 +1,49 @@ +# RECOMMENDED: add this integration from the UI instead +# (Settings -> Devices & Services -> Add Integration -> gsmnode). +# The blocks below are only for the OPTIONAL legacy `notify.gsmnode` +# YAML service and for receiving incoming SMS via a webhook. + +# --- Optional: legacy notify.gsmnode YAML service --------------------- +notify: + - platform: gsmnode + name: gsmnode # service becomes notify.gsmnode + api_base: http://10.2.1.10:8080 # the gsmnode API Server (reachable from HA) + email: !secret gsmnode_email + password: !secret gsmnode_password + device_id: my-phone # optional: pin to a specific device + +# secrets.yaml: +# gsmnode_email: you@example.com +# gsmnode_password: your-password + +# --- Usage ---------------------------------------------------------------- +# Send an SMS: +# service: notify.gsmnode +# data: { message: "Hello", target: ["+15551234567"] } +# +# Place a phone call (gateway phone dials the number; message is ignored): +# service: notify.gsmnode +# data: { message: "", target: ["+15551234567"], data: { type: call } } + +# --- Optional: receive incoming SMS as Home Assistant events --------------- +# The API Server can POST the `sms:received` webhook to a HA webhook trigger. +# +# 1. Create an automation with a webhook trigger (gives you a webhook_id): +# +# automation: +# - alias: "gsmnode - incoming SMS" +# trigger: +# - platform: webhook +# webhook_id: gsmnode_incoming +# allowed_methods: [POST] +# local_only: true +# action: +# - service: persistent_notification.create +# data: +# title: "SMS from {{ trigger.json.payload.phone_number }}" +# message: "{{ trigger.json.payload.message }}" +# +# 2. Register the webhook with the API Server (one-time), pointing at: +# http://:8123/api/webhook/gsmnode_incoming +# e.g. via the Web App "Webhooks" page or: +# POST /api/webhooks {"event":"sms:received","url":""} diff --git a/Home Assistant Plugin/custom_components/gsmnode/__init__.py b/Home Assistant Plugin/custom_components/gsmnode/__init__.py new file mode 100644 index 0000000..68986d8 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/__init__.py @@ -0,0 +1,105 @@ +"""The gsmnode integration. + +Sends SMS and places phone calls through the gsmnode API Server. Configured +from the UI (Settings → Devices & Services → Add Integration → gsmnode). + +Exposes two services — `gsmnode.send_sms` and `gsmnode.call` — and an +"API Server" connectivity binary sensor. A legacy `notify.gsmnode` platform +(YAML) is also available for backward compatibility (see notify.py / README). +""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv + +from .client import GsmNodeClient, GsmNodeConnectionError +from .const import ( + CONF_API_BASE, + CONF_DEVICE_ID, + DOMAIN, + SERVICE_CALL, + SERVICE_SEND_SMS, +) + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR] + +SEND_SMS_SCHEMA = vol.Schema( + { + vol.Required("phone_numbers"): vol.All(cv.ensure_list, [cv.string]), + vol.Required("message"): cv.string, + vol.Optional("device_id"): cv.string, + vol.Optional("sim_number"): vol.Coerce(int), + } +) + +CALL_SCHEMA = vol.Schema( + { + vol.Required("phone_number"): cv.string, + vol.Optional("device_id"): cv.string, + } +) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up gsmnode from a config entry.""" + client = GsmNodeClient( + hass, + entry.data[CONF_API_BASE], + entry.data[CONF_EMAIL], + entry.data[CONF_PASSWORD], + entry.data.get(CONF_DEVICE_ID), + ) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + _async_register_services(hass) + return True + + +def _async_register_services(hass: HomeAssistant) -> None: + """Register the send_sms / call services (once).""" + if hass.services.has_service(DOMAIN, SERVICE_SEND_SMS): + return + + def _first_client() -> GsmNodeClient | None: + for value in hass.data.get(DOMAIN, {}).values(): + if isinstance(value, GsmNodeClient): + return value + return None + + async def handle_send_sms(call: ServiceCall) -> None: + client = _first_client() + if client is None: + raise GsmNodeConnectionError("no gsmnode configured") + await client.send_sms( + call.data["phone_numbers"], + call.data["message"], + call.data.get("device_id"), + call.data.get("sim_number"), + ) + + async def handle_call(call: ServiceCall) -> None: + client = _first_client() + if client is None: + raise GsmNodeConnectionError("no gsmnode configured") + await client.place_call(call.data["phone_number"], call.data.get("device_id")) + + hass.services.async_register( + DOMAIN, SERVICE_SEND_SMS, handle_send_sms, schema=SEND_SMS_SCHEMA + ) + hass.services.async_register(DOMAIN, SERVICE_CALL, handle_call, schema=CALL_SCHEMA) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id, None) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_SEND_SMS) + hass.services.async_remove(DOMAIN, SERVICE_CALL) + return unload_ok diff --git a/Home Assistant Plugin/custom_components/gsmnode/__pycache__/__init__.cpython-313.pyc b/Home Assistant Plugin/custom_components/gsmnode/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d34599242d3988e764cd5aaab5d07ff3fcd99284 GIT binary patch literal 5943 zcmc&&Yit|G5#A$@)bXts>6c`$Wm`7$kP<%{%dr*Hlw6ClB!g!$Ca)u-eSykz9 zyG2<^Ygil*aZ*X+v0O4GYj|u_Rb-q>=5iV?j1sk&8HI06MC3>x7G*LnlMWnJm62?w zNaVD8gs6GOm*~UWM7dDNDj5xb^3#jBM;^~o!R>4sBWZUeoyL7Nbr`FuW*Btq{qdNp z!K}hL6>G4ZF>mq`K5a5}XrR5M<+amH`)W7U{51XqIO(R6io30*aT`vliXx{9+3{>) z0-wn$NiqTA6-|ZRL~u^dBvTXkw4xTWBNH9wN~h`36h>5%cW*l$88{rqSq&$1nu?R- z$!sopCMRPU#b=VKC(k8G8Wzcq!5N>)=IAQhG+D-`#IXt7iQ~PIo`GK2#d~z&h^blFruz=5c{!qK zS*?&%3c8105}aBT&I4pNz|P^Wr14}fn`TRWMjf5b`=OQ}1mlS2vNFsISxxh#RHc^? zm4qGU+HjiEfr!|v`%*c)ek(a7P$dcD;Wdk1?Yu(k94 z9B4RfDSlFS9XT91dT=N{AnHD5e3DYkpOJ~C*X3nROJ-!&Mhu2v=iRvye%Gd-nSpSH9gbUlxZvu3{#jz?TG2}>6 z&jkC#V1i_#WAJF$Pv~*zJQ!<|_<8K`x}_kz_GfAavupzywfB z@tYP;JF>E-uT5$bN=gDxrA{gtd@#K@rjaH(b+u%05IF-DDig`V9q>VZG^uI2gH6(% zDds8~>>Oc?^)0EQR#5X2^?LwMrJ!}1(9tPLStVPLq>ix(y}=OAmNb?pTR=i5{IrWu zeTF8{-+gQ61n->Sn|4pRFYPM{8!dWINmy^ui~6ViQ~pcOl!f*MkJIm*JUH)1yStz7 zyU}^w(lpO9DOj#o6mwE%3Ew}I{5ZxS{I``dAy#^6+dXLM>F08D22fZqA4}M{RA$Izoj@a4Eq2S`-pF3*}`PTgf>9o2wCtu4oj| zMsNNuWA?j`t#rX+HN#z5-imV^mfpKe?H~%ErBNV*0Ph_P@12AH4k~ID>b}*u?M8TixiNCX8=3PrPpebvoVR|uZ>sMlwd8HL z=xoW0O?rA@YT)wypdX;f7Z34HQ1}xU}jX`Xbt=zq2Z3uOoCmzb>nXK`jFTS*FfT7U|V|49`nv9 zqyFHxzGBLrpu*@K-|KoS{tYGnh8cd=b;Wh9qlCNO+4+9gyIt?^dw1U-*Oebll>Nsl z!m+Y&%(}dWNDFtpnTxFDuLqq#L%QwMQloM{2|(r~kYW(!e6y8Tv)lncjRzrBfbDOm zM?ry=&vzs``|c0QC)a%vJD*~^mmu!pP%4fK#r#73SW^*N zOG4{q`IpKslxyjm!j8E>)AYpD#2cYXfFV7ge8t-iA>iux&CbVediPWKU)1^b(>J|) z=RD0f_~y@js6m93nr`{o%~3n|Yc9(1vj^4z37HY|Olo3;Zjex;Hv!>{s^%=$WegN2 z$fHn&@6+ofDXpd?Nq0-qND+WWmLy__G2K0`=89v5qFU5+JJ5Q|DEvPFuNdS1fsUoG zBetO4^gk?Gq4w+|G)2@Km-7&rFtsR#>A|I=qjGKxUh;${)4DA+ZbWbDB}SlkfEKU> zHBR=PGw_Ytw*hj#1AT*$_;CraO&p3F&$NIfE&9I%w(b-e*6KoB{!uXtsfBKjKy>iH z-#YX>=-&;1$M6VLGP*D{M*sXu=5%LNl}Sq0?eL!%pf7yt*O`oFG|3EuGM$rQin{)Zy6)JV zob5{l)qFi#-(G3mQEJ_Bqh;r$>xJ$z+IhP%_}YCJ@4GE{roAtEUy{ngmKkmK+?(gF zk>5P?)-xa2{@}XqdgrO~*8Vw@<#<`xFw;1@=E@qYXSghEoY_9xb*0PdIZ+ncW}>sP zH)GedUk%N>91iD#y{^eN8JKTGfyU{xFP^nlJXIDp&zzafUddXMYvSe|b~@1!uuXct WsP}R8U*HYg6Wo_QoWKRy@%$H1c-lt* literal 0 HcmV?d00001 diff --git a/Home Assistant Plugin/custom_components/gsmnode/__pycache__/binary_sensor.cpython-313.pyc b/Home Assistant Plugin/custom_components/gsmnode/__pycache__/binary_sensor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22894fd694b67a69d42aee768c2bf8196ffae2fe GIT binary patch literal 3206 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 literal 0 HcmV?d00001 diff --git a/Home Assistant Plugin/custom_components/gsmnode/__pycache__/config_flow.cpython-313.pyc b/Home Assistant Plugin/custom_components/gsmnode/__pycache__/config_flow.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2e4fa2218f40bf612609e53821b86ab4900e461 GIT binary patch literal 3095 zcmaJDOKcn0@hx|`Tz<9sQ3_?r)Xx^YjP>AKMbH36&`azFhI`1thaS?R2leJ-92-6LAVvYmJfx_ApeTH!!2s@|ht6A)5^J_y ziL*2F=FQBTH=nKh{T>9*pRXjgekLIFJtbO?zkx9IKQIVAL=wVCVkAd`i94_(OmQ~B z#yQN{IG1o@9(c}#D=uIm?#6DL&nG-_FZRZL*k{wOgg+j@0l)>xo#={-Sd4e$?syOG zv3Z_EZ@drp#rtu8Jcxtw0Xz^!>wM}=%dMmL80DWmqFWg7`__Y!ch<2U`kxZs^KdHq z{2v+Lp-dokp&{K4!A>A`v7OVvZT?&*KXs{-C;4WXLo0mhN~Z*9>pzqTo#9$I4iBO+ zB>6)~3akt315(#4hetQKh`2KoGxVIc5zgg}55hSEhpkODykQn}Bddlr-BLHOVrhmR zo=Pu=P1PzDBheSsa)cpXMb{0>UN(Vb=k@Jm#6dg_l}n(Jh(~xzHA{KRUSCNr$%{Xp zUw)tXV-&wWFG)97QVXC&Fx9-8u?+lz#yr9j@jbJU1ZU<;*5)F{1}6R{CzjV#-6Fvj zY3Mra1Yd2j2-j3Te|=fLIxj7ffNg7G@y2p&QC?mk;=|Z;L*9ulZVd zE9K5pVR&?Q1f|(jSA%MY=bOYbw1-Uy={@Eln{@iX|JQv=b+_+p;F6 z6wm}C?1pM#nkK}VH#Rh#@YuAlRwTWOxvghp=sj{t*Y1{7s(wUP?ikpTwcHVtIs?^I z!fKZEG&W_hs#vNFZ31s&nzxuvD$>0$bisZ7bVi~=}PC>lCgHK#Wkt#rEKn{3x` zar@8%8ZBypjCOQ{Xw%7-$OM=%PeD1qKp&x+fW{_1;Saq1W$);B{bN}bgU24_ALKvNE8@hSI8pPVV<&!B zC=Z>h482<(diP0ef9OZ|g=#QV2}aAo=TzYNw`1P&pG=mFsNyc|Z|lP?3P`xf)hAN;6qqK+8< znd<2Deg6Kv&jR1|MkwQKb@c59qs9XDFLMoo_RD$Kf`@tP>5WaIr^8HalzTeDQGDzo z@V^Q%v2pIJp?MyNXQK=(JR4^yKH0V4VxOIJQ2asw@aG)GpYsgGT`b_ghcTFpaQHNs z+3C+}8EXxa_N_2X!rS4Whm(*5kjF4Jv5sk5jR-`LWnC$#vP?X(TrjewJjH#oe7B_J zn=4(ioYNrv^BN?P0n08~&Kent+*nLW%PUFzHfSULvTV}%n~@a@YUNJJQcYRLGq6^N zAHwuY2-A*c13F^N2!LOqZ@E(kfv%6&of>=G>8XaM?e~HEjaN>fYX|_QpCR^M z7=(P*C@7j98N;){rS9iSa58Wo(}_rU7?hg6f$2OUo_j{VRJ2M)$s{b`uTs`;XdO-yGXj#6M(Ic+nIQg?zOP+&J3 z+Fh_n0UaoWmm1wkNU3*A8tob^ND|+w`k^H?zd}e)I~B7V3=!LTtMGL|gw_`f5mt)y z6GX}5Q`C?hdwL&@NYeWt!llNwU%^Qr>3lZ-1OPIYVZKGPe??<|N5SvW;6Kpf-zfR! zD~@9vFA;!O9>gS=eH1_F4L+Lt&D??D{nYns-~HQrZ(S-2m!Fu`CgUgjLiq7`i@Lop zj6c3rnVkE2a<1y@tN2cS?K}C!t;*@8^690@>6J44`>xkn#|aN4gxhE9{OP_h@itN=oSC~ literal 0 HcmV?d00001 diff --git a/Home Assistant Plugin/custom_components/gsmnode/__pycache__/const.cpython-313.pyc b/Home Assistant Plugin/custom_components/gsmnode/__pycache__/const.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9007f2d18a96b1b5a74f9c945eb2a368c930871f GIT binary patch literal 423 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 JAi{R(e*u--9iIRI literal 0 HcmV?d00001 diff --git a/Home Assistant Plugin/custom_components/gsmnode/binary_sensor.py b/Home Assistant Plugin/custom_components/gsmnode/binary_sensor.py new file mode 100644 index 0000000..944bf49 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/binary_sensor.py @@ -0,0 +1,66 @@ +"""API Server connectivity binary sensor.""" +from __future__ import annotations + +import logging +from datetime import timedelta + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .client import GsmNodeClient +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=30) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the connectivity sensor for a config entry.""" + client: GsmNodeClient = hass.data[DOMAIN][entry.entry_id] + + coordinator: DataUpdateCoordinator[bool] = DataUpdateCoordinator( + hass, + _LOGGER, + name="gsmnode_health", + update_method=client.health, + update_interval=SCAN_INTERVAL, + ) + await coordinator.async_config_entry_first_refresh() + + async_add_entities([GsmNodeHealthSensor(coordinator, entry)]) + + +class GsmNodeHealthSensor(CoordinatorEntity[DataUpdateCoordinator[bool]], BinarySensorEntity): + """Reports whether the API Server is reachable.""" + + _attr_has_entity_name = True + _attr_name = "API Server" + _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY + + def __init__(self, coordinator: DataUpdateCoordinator[bool], entry: ConfigEntry) -> None: + super().__init__(coordinator) + self._attr_unique_id = f"{entry.entry_id}_api_health" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name="gsmnode", + manufacturer="gsmnode", + ) + + @property + def is_on(self) -> bool: + """True when the API Server responded OK on the last check.""" + return bool(self.coordinator.data) diff --git a/Home Assistant Plugin/custom_components/gsmnode/client.py b/Home Assistant Plugin/custom_components/gsmnode/client.py new file mode 100644 index 0000000..5e1e82a --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/client.py @@ -0,0 +1,105 @@ +"""Async client for the gsmnode API Server (shared by the UI integration).""" +from __future__ import annotations + +import aiohttp + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + + +class GsmNodeAuthError(Exception): + """Raised when the API Server rejects the credentials.""" + + +class GsmNodeConnectionError(Exception): + """Raised when the API Server can't be reached or returns an error.""" + + +class GsmNodeClient: + """Talks to the API Server: login, send SMS, place calls, health.""" + + def __init__( + self, + hass: HomeAssistant, + api_base: str, + email: str, + password: str, + device_id: str | None = None, + ) -> None: + self._session = async_get_clientsession(hass) + self._api_base = api_base.rstrip("/") + self._email = email + self._password = password + self.device_id = device_id + self._token: str | None = None + + async def login(self) -> None: + """Authenticate and cache the JWT.""" + try: + async with self._session.post( + f"{self._api_base}/api/auth/login", + json={"email": self._email, "password": self._password}, + ) as resp: + if resp.status == 401: + raise GsmNodeAuthError("invalid credentials") + if resp.status != 200: + raise GsmNodeConnectionError(f"HTTP {resp.status}") + data = await resp.json() + self._token = data.get("access_token") + if not self._token: + raise GsmNodeAuthError("no token in response") + except aiohttp.ClientError as err: + raise GsmNodeConnectionError(str(err)) from err + + async def _post(self, path: str, payload: dict) -> int: + headers = {"Authorization": f"Bearer {self._token}"} if self._token else {} + async with self._session.post( + f"{self._api_base}{path}", json=payload, headers=headers + ) as resp: + return resp.status + + async def _send(self, path: str, payload: dict) -> None: + """POST with auth, re-logging in once on a 401.""" + try: + if not self._token: + await self.login() + status = await self._post(path, payload) + if status == 401: + await self.login() + status = await self._post(path, payload) + except aiohttp.ClientError as err: + raise GsmNodeConnectionError(str(err)) from err + if status not in (200, 201, 202): + raise GsmNodeConnectionError(f"{path} -> HTTP {status}") + + async def send_sms( + self, + phone_numbers: list[str], + message: str, + device_id: str | None = None, + sim_number: int | None = None, + ) -> None: + """Queue an outbound SMS.""" + payload: dict = {"phone_numbers": phone_numbers, "text_message": message} + dev = device_id or self.device_id + if dev: + payload["device_id"] = dev + if sim_number is not None: + payload["sim_number"] = sim_number + await self._send("/api/messages", payload) + + async def place_call(self, phone_number: str, device_id: str | None = None) -> None: + """Queue an outbound phone call.""" + payload: dict = {"phone_number": phone_number} + dev = device_id or self.device_id + if dev: + payload["device_id"] = dev + await self._send("/api/calls", payload) + + async def health(self) -> bool: + """Return True if the API Server's health endpoint responds OK.""" + try: + async with self._session.get(f"{self._api_base}/api/health") as resp: + return resp.status == 200 + except aiohttp.ClientError: + return False diff --git a/Home Assistant Plugin/custom_components/gsmnode/config_flow.py b/Home Assistant Plugin/custom_components/gsmnode/config_flow.py new file mode 100644 index 0000000..15ab690 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/config_flow.py @@ -0,0 +1,67 @@ +"""Config flow for the gsmnode integration (UI setup).""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers import selector + +from .client import ( + GsmNodeAuthError, + GsmNodeClient, + GsmNodeConnectionError, +) +from .const import CONF_API_BASE, CONF_DEVICE_ID, DEFAULT_API_BASE, DOMAIN + +STEP_USER_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_BASE, default=DEFAULT_API_BASE): str, + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + vol.Optional(CONF_DEVICE_ID): str, + } +) + + +class GsmNodeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle the UI configuration flow.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + + if user_input is not None: + client = GsmNodeClient( + self.hass, + user_input[CONF_API_BASE], + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + user_input.get(CONF_DEVICE_ID), + ) + try: + await client.login() + except GsmNodeAuthError: + errors["base"] = "invalid_auth" + except GsmNodeConnectionError: + errors["base"] = "cannot_connect" + else: + api_base = user_input[CONF_API_BASE].rstrip("/") + await self.async_set_unique_id(f"{api_base}::{user_input[CONF_EMAIL]}") + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{user_input[CONF_EMAIL]} ({api_base})", + data=user_input, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors + ) diff --git a/Home Assistant Plugin/custom_components/gsmnode/const.py b/Home Assistant Plugin/custom_components/gsmnode/const.py new file mode 100644 index 0000000..7dda479 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/const.py @@ -0,0 +1,13 @@ +"""Constants for the gsmnode integration.""" + +DOMAIN = "gsmnode" + +CONF_API_BASE = "api_base" +CONF_DEVICE_ID = "device_id" + +DEFAULT_API_BASE = "http://localhost:8080" +DEFAULT_NAME = "gsmnode" + +# Service names registered by the integration. +SERVICE_SEND_SMS = "send_sms" +SERVICE_CALL = "call" diff --git a/Home Assistant Plugin/custom_components/gsmnode/manifest.json b/Home Assistant Plugin/custom_components/gsmnode/manifest.json new file mode 100644 index 0000000..0f4ebb5 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "gsmnode", + "name": "gsmnode", + "version": "1.2.0", + "config_flow": true, + "documentation": "https://github.com/your-org/sms-gateway", + "issue_tracker": "https://github.com/your-org/sms-gateway/issues", + "dependencies": [], + "codeowners": [], + "requirements": [], + "integration_type": "service", + "iot_class": "local_polling" +} diff --git a/Home Assistant Plugin/custom_components/gsmnode/notify.py b/Home Assistant Plugin/custom_components/gsmnode/notify.py new file mode 100644 index 0000000..cf8c3a3 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/notify.py @@ -0,0 +1,150 @@ +"""gsmnode notify platform. + +Sends SMS through the gsmnode API Server's `/api/messages` endpoint. The +API Server is the only thing that talks to PocketBase, so this integration only +needs the API Server's URL and a user login. +""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import ( + CONF_API_BASE, + CONF_DEVICE_ID, + DEFAULT_API_BASE, + DEFAULT_NAME, +) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_BASE, default=DEFAULT_API_BASE): cv.string, + vol.Required(CONF_EMAIL): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEVICE_ID): cv.string, + } +) + + +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> "GsmNodeNotificationService": + """Return the gsmnode notification service.""" + return GsmNodeNotificationService( + hass, + config[CONF_API_BASE], + config[CONF_EMAIL], + config[CONF_PASSWORD], + config.get(CONF_DEVICE_ID), + ) + + +class GsmNodeNotificationService(BaseNotificationService): + """Implement the notification service for gsmnode.""" + + def __init__( + self, + hass: HomeAssistant, + api_base: str, + email: str, + password: str, + device_id: str | None, + ) -> None: + """Initialize the service.""" + self._hass = hass + self._api_base = api_base.rstrip("/") + self._email = email + self._password = password + self._device_id = device_id + self._token: str | None = None + + @property + def _session(self): + return async_get_clientsession(self._hass) + + async def _login(self) -> None: + """Authenticate against the API Server and cache the JWT.""" + url = f"{self._api_base}/api/auth/login" + async with self._session.post( + url, json={"email": self._email, "password": self._password} + ) as resp: + if resp.status != 200: + raise RuntimeError(f"login failed: HTTP {resp.status}") + data = await resp.json() + self._token = data.get("access_token") + if not self._token: + raise RuntimeError("login response did not contain a token") + + async def _post(self, path: str, payload: dict[str, Any]) -> int: + """POST to an API path; returns the HTTP status code.""" + url = f"{self._api_base}{path}" + headers = {"Authorization": f"Bearer {self._token}"} + async with self._session.post(url, json=payload, headers=headers) as resp: + return resp.status + + async def _send(self, path: str, payload: dict[str, Any]) -> None: + """Authenticate (if needed) and POST, retrying once on a 401.""" + try: + if not self._token: + await self._login() + + status = await self._post(path, payload) + if status == 401: # token expired — re-login once and retry + await self._login() + status = await self._post(path, payload) + + if status not in (200, 201, 202): + _LOGGER.error("gsmnode: %s failed with HTTP %s", path, status) + except Exception as err: # noqa: BLE001 - surface any transport error + _LOGGER.error("gsmnode: error calling %s: %s", path, err) + + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send an SMS, or place a phone call when `data.type` is `call`. + + Recipient numbers come from the `target` field. Optional data overrides: + `device_id` (which device), `sim_number` (SMS only), and `type: call` to + dial the target(s) instead of texting. + """ + targets = kwargs.get(ATTR_TARGET) + if not targets: + _LOGGER.error("gsmnode: no target phone number(s) provided") + return + + data = kwargs.get(ATTR_DATA) or {} + device_id = data.get("device_id", self._device_id) + is_call = str(data.get("type", "")).lower() == "call" + + if is_call: + # One call per target number (a call has a single recipient). + for number in targets: + payload: dict[str, Any] = {"phone_number": number} + if device_id: + payload["device_id"] = device_id + await self._send("/api/calls", payload) + return + + payload = {"phone_numbers": list(targets), "text_message": message} + if device_id: + payload["device_id"] = device_id + if "sim_number" in data: + payload["sim_number"] = data["sim_number"] + await self._send("/api/messages", payload) diff --git a/Home Assistant Plugin/custom_components/gsmnode/services.yaml b/Home Assistant Plugin/custom_components/gsmnode/services.yaml new file mode 100644 index 0000000..59427d8 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/services.yaml @@ -0,0 +1,53 @@ +send_sms: + name: Send SMS + description: Send an SMS through the gateway. + fields: + phone_numbers: + name: Phone numbers + description: One or more recipient phone numbers. + required: true + example: '["+15551234567"]' + selector: + text: + multiple: true + message: + name: Message + description: The text to send. + required: true + example: Hello from Home Assistant + selector: + text: + multiline: true + device_id: + name: Device + description: Device to send from (defaults to the configured / most recent device). + example: my-phone + selector: + text: + sim_number: + name: SIM number + description: Which SIM to use on a dual-SIM phone. + example: 1 + selector: + number: + min: 1 + max: 4 + mode: box + +call: + name: Place call + description: Tell a gateway device to place an outbound phone call. + fields: + phone_number: + name: Phone number + description: The number to call. + required: true + example: "+15551234567" + selector: + text: + device_id: + name: Device + description: Device to call from (defaults to the configured / most recent device). + example: my-phone + selector: + text: diff --git a/Home Assistant Plugin/custom_components/gsmnode/strings.json b/Home Assistant Plugin/custom_components/gsmnode/strings.json new file mode 100644 index 0000000..b11d469 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/strings.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "user": { + "title": "gsmnode", + "description": "Connect to your gsmnode API Server.", + "data": { + "api_base": "API Server URL", + "email": "Email", + "password": "Password", + "device_id": "Default device ID (optional)" + } + } + }, + "error": { + "cannot_connect": "Failed to connect to the API Server. Check the URL and that it is reachable.", + "invalid_auth": "Invalid email or password." + }, + "abort": { + "already_configured": "This gateway is already configured." + } + }, + "services": { + "send_sms": { + "name": "Send SMS", + "description": "Send an SMS through the gateway.", + "fields": { + "phone_numbers": { "name": "Phone numbers", "description": "One or more recipient phone numbers." }, + "message": { "name": "Message", "description": "The text to send." }, + "device_id": { "name": "Device", "description": "Device to send from." }, + "sim_number": { "name": "SIM number", "description": "Which SIM to use on a dual-SIM phone." } + } + }, + "call": { + "name": "Place call", + "description": "Tell a gateway device to place an outbound phone call.", + "fields": { + "phone_number": { "name": "Phone number", "description": "The number to call." }, + "device_id": { "name": "Device", "description": "Device to call from." } + } + } + } +} diff --git a/Home Assistant Plugin/custom_components/gsmnode/translations/en.json b/Home Assistant Plugin/custom_components/gsmnode/translations/en.json new file mode 100644 index 0000000..b11d469 --- /dev/null +++ b/Home Assistant Plugin/custom_components/gsmnode/translations/en.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "user": { + "title": "gsmnode", + "description": "Connect to your gsmnode API Server.", + "data": { + "api_base": "API Server URL", + "email": "Email", + "password": "Password", + "device_id": "Default device ID (optional)" + } + } + }, + "error": { + "cannot_connect": "Failed to connect to the API Server. Check the URL and that it is reachable.", + "invalid_auth": "Invalid email or password." + }, + "abort": { + "already_configured": "This gateway is already configured." + } + }, + "services": { + "send_sms": { + "name": "Send SMS", + "description": "Send an SMS through the gateway.", + "fields": { + "phone_numbers": { "name": "Phone numbers", "description": "One or more recipient phone numbers." }, + "message": { "name": "Message", "description": "The text to send." }, + "device_id": { "name": "Device", "description": "Device to send from." }, + "sim_number": { "name": "SIM number", "description": "Which SIM to use on a dual-SIM phone." } + } + }, + "call": { + "name": "Place call", + "description": "Tell a gateway device to place an outbound phone call.", + "fields": { + "phone_number": { "name": "Phone number", "description": "The number to call." }, + "device_id": { "name": "Device", "description": "Device to call from." } + } + } + } +} diff --git a/Phone App/.gitignore b/Phone App/.gitignore new file mode 100644 index 0000000..740ffb4 --- /dev/null +++ b/Phone App/.gitignore @@ -0,0 +1,19 @@ +# Flutter / Dart +.dart_tool/ +.packages +build/ +.flutter-plugins +.flutter-plugins-dependencies +pubspec.lock + +# Android +android/.gradle/ +android/local.properties +android/key.properties +*.keystore +*.jks + +# IDE +.idea/ +*.iml +.vscode/ diff --git a/Phone App/.metadata b/Phone App/.metadata new file mode 100644 index 0000000..9c3a509 --- /dev/null +++ b/Phone App/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: android + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/Phone App/README.md b/Phone App/README.md new file mode 100644 index 0000000..a32df2c --- /dev/null +++ b/Phone App/README.md @@ -0,0 +1,124 @@ +# gsmnode — Phone App (Flutter / Android) + +Turns an Android phone into the SMS gateway endpoint. It registers with the API +Server, polls for pending outbound messages, sends them over the radio, reports +delivery state, and forwards incoming SMS to the server's inbox. + +``` +API Server ──(pending messages)──► Phone App ──► SmsManager (send) + ▲ │ + └──(status reports / inbox)──────────┘ ◄── BroadcastReceiver (incoming) +``` + +> The Phone App talks **only** to the API Server (never to PocketBase directly), +> using the device token issued at registration. + +## Status: source scaffold + +This folder contains the complete Dart app and the native Android (Kotlin) SMS +bridge, but **not** the generated Gradle/platform scaffolding. You generate that +once with `flutter create` after installing the toolchain (below). + +## Prerequisites + +1. **Flutter SDK** (stable) — https://docs.flutter.dev/get-started/install/windows +2. **JDK 17** (this machine currently has only JDK 8 — Android Gradle needs 17+) +3. **Android SDK** (via Android Studio or `flutter doctor --android-licenses`) +4. A **physical Android phone** with a SIM (the emulator can't send real SMS) + +Verify with `flutter doctor` — resolve anything it flags before continuing. + +## Generate platform scaffolding & wire in the native code + +From this `Phone App/` folder: + +```powershell +# 1. Generate the android/ Gradle project (keeps lib/ and pubspec.yaml) +flutter create . --org app.smsgateway --project-name sms_gateway_phone --platforms=android + +# 2. Overlay the SMS-enabled manifest + Kotlin (overwrites the generated stubs) +Copy-Item -Recurse -Force android_overlay/* android/ + +# 3. Fetch packages +flutter pub get +``` + +`android_overlay/` mirrors the real `android/` paths, so step 2 drops: +- `app/src/main/AndroidManifest.xml` — SMS permissions + the `SmsReceiver` +- `app/src/main/kotlin/app/smsgateway/sms_gateway_phone/MainActivity.kt` — send bridge +- `app/src/main/kotlin/app/smsgateway/sms_gateway_phone/SmsReceiver.kt` — incoming bridge + +If Gradle complains about SDK levels, set `minSdkVersion 23` (or higher) in +`android/app/build.gradle`. + +## Run + +```powershell +flutter devices # confirm your phone is listed +flutter run # build & install on the connected phone +``` + +1. On first launch, enter the **API Server URL**, your **email/password**, and a + **device name**, then tap *Sign in & register device*. + - Emulator → host: use `http://10.0.2.2:8080`. + - Physical phone → use the host's LAN IP, e.g. `http://10.2.1.x:8080` + (the same network as the phone; make sure the API Server is reachable). +2. On the home screen, **grant SMS & phone permissions**, then **Start gateway**. +3. Send a test message from the Web App → it appears in the activity log and is + delivered via the phone. Texts received by the phone show up in the Web App + **Inbox**. + +## How it maps to the API + +| Action | Endpoint | +|---|---| +| Login | `POST /api/auth/login` (JWT) | +| Register device | `POST /api/mobile/v1/device` → device token | +| Poll pending | `GET /api/mobile/v1/messages` (marks them `Processed`) | +| Report state | `PATCH /api/mobile/v1/messages/{id}` (`Sent`/`Failed`) | +| Incoming SMS | `POST /api/mobile/v1/inbox` | +| Heartbeat | `POST /api/mobile/v1/ping` | + +Pulled items carry a `type` of `sms` or `call`. For `call` the app places a +native phone call via `TelecomManager.placeCall` (needs `CALL_PHONE` — covered by +the phone permission) instead of sending SMS, then reports `Sent`. +`TelecomManager.placeCall` is used rather than `startActivity(ACTION_CALL)` so the +call still goes through when the screen is locked / the app is backgrounded +(Android blocks background activity starts, but not telecom-routed calls). + +## Code layout + +``` +lib/ + main.dart entry, bootstraps services, picks first screen + config.dart default API base + poll/ping intervals + models/message.dart outbound message model + services/ + storage.dart persisted settings/tokens (shared_preferences) + api_client.dart API Server HTTP client + sms_service.dart platform-channel bridge (send + incoming stream) + gateway_service.dart the poll → send → report loop + inbox forwarding + screens/ + login_screen.dart login + device registration + home_screen.dart start/stop, permissions, activity log +android_overlay/ native Android files to copy after `flutter create` +``` + +## Background & delivery reports (implemented) + +- **Foreground service** (`GatewayForegroundService.kt`): starting the gateway + launches an ongoing-notification foreground service + partial wakelock, so the + poll/send loop keeps running while the screen is off or the app is backgrounded. +- **Delivery reports**: `MainActivity.sendSms` attaches `sent`/`delivered` + PendingIntents; `SmsStatusReceiver` forwards the outcome (tagged with the + message id) to Dart, which reports `Delivered`/`Failed` to the API Server. + +## Known next steps (not yet implemented) + +- **Survive full task removal / long Doze**: the foreground service covers + screen-lock, but surviving the user swiping the app away or hours of Doze would + need a dedicated background Dart isolate (e.g. `flutter_background_service`). +- **Push wake-up (FCM)**: register an FCM token at registration so the server can + wake the device instead of polling. Requires a Firebase project + + `google-services.json` + server-side FCM sending in the API Server. +- **MMS / data SMS**: only text SMS is implemented. diff --git a/Phone App/analysis_options.yaml b/Phone App/analysis_options.yaml new file mode 100644 index 0000000..c4d5f50 --- /dev/null +++ b/Phone App/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:flutter_lints/flutter.yaml + +linter: + rules: + prefer_const_constructors: true + avoid_print: false diff --git a/Phone App/android/.gitignore b/Phone App/android/.gitignore new file mode 100644 index 0000000..55afd91 --- /dev/null +++ b/Phone App/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/Phone App/android/app/build.gradle b/Phone App/android/app/build.gradle new file mode 100644 index 0000000..6545a7c --- /dev/null +++ b/Phone App/android/app/build.gradle @@ -0,0 +1,43 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +android { + namespace = "app.gsmnode.phone" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId = "app.gsmnode.phone" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.debug + } + } +} + +flutter { + source = "../.." +} diff --git a/Phone App/android/app/src/debug/AndroidManifest.xml b/Phone App/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Phone App/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Phone App/android/app/src/main/AndroidManifest.xml b/Phone App/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dabd850 --- /dev/null +++ b/Phone App/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt new file mode 100644 index 0000000..fa22bb6 --- /dev/null +++ b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt @@ -0,0 +1,99 @@ +package app.gsmnode.phone + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.os.PowerManager +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat + +/// Foreground service that keeps the app process alive (and the CPU awake via a +/// partial wakelock) so the Dart gateway loop keeps polling/sending while the +/// screen is off or the app is in the background. +/// +/// Note: this closes the "survives screen lock" gap. Surviving a full task +/// removal or Doze for hours would additionally need a background Dart isolate +/// (e.g. flutter_background_service) — documented as a further step. +class GatewayForegroundService : Service() { + + private var wakeLock: PowerManager.WakeLock? = null + + companion object { + private const val CHANNEL_ID = "sms_gateway_service" + private const val NOTIFICATION_ID = 4711 + + fun start(context: Context) { + val intent = Intent(context, GatewayForegroundService::class.java) + ContextCompat.startForegroundService(context, intent) + } + + fun stop(context: Context) { + context.stopService(Intent(context, GatewayForegroundService::class.java)) + } + } + + override fun onCreate() { + super.onCreate() + createChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForegroundCompat() + acquireWakeLock() + // Restart if the system kills the service. + return START_STICKY + } + + private fun startForegroundCompat() { + val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("gsmnode") + .setContentText("Gateway is running") + .setSmallIcon(android.R.drawable.stat_sys_upload) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground( + NOTIFICATION_ID, notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + @Suppress("WakelockTimeout") + private fun acquireWakeLock() { + if (wakeLock?.isHeld == true) return + val pm = getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = pm.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, "gsmnode:gateway" + ).also { it.acquire() } + } + + private fun createChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, "gsmnode", + NotificationManager.IMPORTANCE_LOW + ).apply { description = "Keeps the SMS gateway running" } + getSystemService(NotificationManager::class.java) + .createNotificationChannel(channel) + } + } + + override fun onDestroy() { + wakeLock?.let { if (it.isHeld) it.release() } + wakeLock = null + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null +} diff --git a/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt new file mode 100644 index 0000000..f3e1ecb --- /dev/null +++ b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt @@ -0,0 +1,187 @@ +package app.gsmnode.phone + +import android.Manifest +import android.app.PendingIntent +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.TelecomManager +import android.telephony.SmsManager +import android.telephony.SubscriptionManager +import androidx.core.content.ContextCompat +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity() { + + companion object { + const val METHOD_CHANNEL = "app.gsmnode/sms" + const val EVENT_CHANNEL = "app.gsmnode/sms_incoming" + const val STATUS_CHANNEL = "app.gsmnode/sms_status" + const val STATUS_ACTION = "app.gsmnode.SMS_STATUS" + + // Sinks used by the broadcast receivers to push events into Dart. + @Volatile var incomingSink: EventChannel.EventSink? = null + @Volatile var statusSink: EventChannel.EventSink? = null + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + val messenger = flutterEngine.dartExecutor.binaryMessenger + + MethodChannel(messenger, METHOD_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "sendSms" -> { + val phone = call.argument("phone") + val message = call.argument("message") + val simSlot = call.argument("simSlot") + val messageId = call.argument("messageId") + if (phone.isNullOrBlank() || message == null) { + result.error("BAD_ARGS", "phone and message are required", null) + return@setMethodCallHandler + } + try { + sendSms(phone, message, simSlot, messageId) + result.success(true) + } catch (e: Exception) { + result.error("SEND_FAILED", e.message, null) + } + } + "placeCall" -> { + val phone = call.argument("phone") + if (phone.isNullOrBlank()) { + result.error("BAD_ARGS", "phone is required", null) + return@setMethodCallHandler + } + try { + placeCall(phone) + result.success(true) + } catch (e: Exception) { + result.error("CALL_FAILED", e.message, null) + } + } + "startService" -> { + GatewayForegroundService.start(this) + result.success(true) + } + "stopService" -> { + GatewayForegroundService.stop(this) + result.success(true) + } + else -> result.notImplemented() + } + } + + EventChannel(messenger, EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(args: Any?, sink: EventChannel.EventSink?) { + incomingSink = sink + } + override fun onCancel(args: Any?) { + incomingSink = null + } + }) + + EventChannel(messenger, STATUS_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(args: Any?, sink: EventChannel.EventSink?) { + statusSink = sink + } + override fun onCancel(args: Any?) { + statusSink = null + } + }) + } + + private fun smsManagerFor(simSlot: Int?): SmsManager { + if (simSlot == null) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + getSystemService(SmsManager::class.java) + else + @Suppress("DEPRECATION") SmsManager.getDefault() + } + val hasPerm = ContextCompat.checkSelfPermission( + this, Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_GRANTED + if (hasPerm) { + val sm = getSystemService(SubscriptionManager::class.java) + val info = sm?.getActiveSubscriptionInfoForSimSlotIndex(simSlot) + if (info != null) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + getSystemService(SmsManager::class.java) + .createForSubscriptionId(info.subscriptionId) + else + @Suppress("DEPRECATION") + SmsManager.getSmsManagerForSubscriptionId(info.subscriptionId) + } + } + return smsManagerFor(null) + } + + /// Builds a PendingIntent that, when fired by the radio, broadcasts the + /// send/delivery outcome to SmsStatusReceiver (tagged with the message id). + private fun statusPendingIntent(messageId: String?, phone: String, kind: String): PendingIntent { + val intent = Intent(this, SmsStatusReceiver::class.java).apply { + action = STATUS_ACTION + putExtra("messageId", messageId) + putExtra("phone", phone) + putExtra("kind", kind) + } + val requestCode = (messageId.orEmpty() + phone + kind).hashCode() + return PendingIntent.getBroadcast( + this, requestCode, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + /// Places a phone call. Requires the CALL_PHONE permission (requested in Dart + /// via the phone permission group). + /// + /// Uses TelecomManager.placeCall, which routes through the system telecom + /// service rather than starting the dialer activity ourselves. This is what + /// lets the call go through when the screen is locked / the app is in the + /// background — a plain startActivity(ACTION_CALL) is blocked by Android's + /// background-activity-start restrictions in that state. Falls back to + /// ACTION_CALL only on very old devices without TelecomManager.placeCall. + private fun placeCall(phone: String) { + val uri = Uri.fromParts("tel", phone, null) + val hasPerm = ContextCompat.checkSelfPermission( + this, Manifest.permission.CALL_PHONE + ) == PackageManager.PERMISSION_GRANTED + if (!hasPerm) { + throw SecurityException("CALL_PHONE permission not granted") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val telecom = getSystemService(TelecomManager::class.java) + if (telecom != null) { + telecom.placeCall(uri, Bundle()) + return + } + } + val intent = Intent(Intent.ACTION_CALL, uri) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } + + private fun sendSms(phone: String, message: String, simSlot: Int?, messageId: String?) { + val sms = smsManagerFor(simSlot) + val parts = sms.divideMessage(message) + val sentPI = statusPendingIntent(messageId, phone, "sent") + val deliveredPI = statusPendingIntent(messageId, phone, "delivered") + if (parts.size > 1) { + val sentList = ArrayList(parts.size) + val deliveredList = ArrayList(parts.size) + for (i in parts.indices) { + sentList.add(sentPI) + deliveredList.add(deliveredPI) + } + sms.sendMultipartTextMessage(phone, null, parts, sentList, deliveredList) + } else { + sms.sendTextMessage(phone, null, message, sentPI, deliveredPI) + } + } +} diff --git a/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt new file mode 100644 index 0000000..c3df656 --- /dev/null +++ b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt @@ -0,0 +1,40 @@ +package app.gsmnode.phone + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.provider.Telephony + +/// Receives incoming SMS and forwards each message to Dart via the EventChannel +/// sink held by MainActivity. Works while the app process is alive. +class SmsReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return + + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) ?: return + if (messages.isEmpty()) return + + // Multipart SMS arrive as several PDUs from the same sender; concatenate. + val from = messages[0].displayOriginatingAddress ?: "" + val body = StringBuilder() + var timestamp = System.currentTimeMillis() + for (m in messages) { + body.append(m.displayMessageBody ?: "") + timestamp = m.timestampMillis + } + + val payload = mapOf( + "from" to from, + "body" to body.toString(), + "timestamp" to timestamp, + ) + + // EventSink must be touched on the main thread. + Handler(Looper.getMainLooper()).post { + MainActivity.incomingSink?.success(payload) + } + } +} diff --git a/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt new file mode 100644 index 0000000..0d87a2d --- /dev/null +++ b/Phone App/android/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt @@ -0,0 +1,33 @@ +package app.gsmnode.phone + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper + +/// Receives SMS send/delivery outcomes from the PendingIntents created in +/// MainActivity.sendSms and forwards them to Dart, tagged with the message id +/// so the gateway loop can report Sent / Delivered / Failed to the API Server. +class SmsStatusReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val messageId = intent.getStringExtra("messageId") + val kind = intent.getStringExtra("kind") ?: return + // For "sent": RESULT_OK means the radio accepted the message. + // For "delivered": RESULT_OK means a positive delivery report arrived. + val success = resultCode == Activity.RESULT_OK + + val payload = mapOf( + "messageId" to messageId, + "kind" to kind, + "success" to success, + "resultCode" to resultCode, + ) + + Handler(Looper.getMainLooper()).post { + MainActivity.statusSink?.success(payload) + } + } +} diff --git a/Phone App/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/Phone App/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..5b271c08f6c79daf53f74a112b9f9454b58059e8 GIT binary patch literal 4107 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 bKEL{s%g~y;ns0~N1^#twc literal 0 HcmV?d00001 diff --git a/Phone App/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/Phone App/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..c517dafbc288b9d9da88e4122b8564bcf784f57d GIT binary patch literal 2682 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 L9IYBHeUttLs|^K~ literal 0 HcmV?d00001 diff --git a/Phone App/android/app/src/main/res/drawable-v21/launch_background.xml b/Phone App/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/Phone App/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Phone App/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/Phone App/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..77557e1e6bbce2756fa5734d3912682cddd1452b GIT binary patch literal 5258 zcmdT|XG2q6vqeG^DFV_(5b4r;SBlaEB=jDTBE19%63P>L$AEMUuL|;Dq#Ht{By^A_ z%>V)cktQV|H57T{{cwN7y&ujwAI_XTXJ+rUX3eBMH8WtO=b@*dpkOpI)CZEEGk-5S z8uC3&Kx&7A;+BPxzK&I7+0Fv=sU>8we;@qKr9hE;Hor!`b?Lx=kmmf*y^-b{a~oHM4VG5J`}zHQ|Y9JS(3tt21_!Bj|D=sHsCL0Z4HM0`&lG(%T7`t#50 zn(Co^5$jXxTane2SpIb(Qk&OB6 zi0OhTZrzooNWaNN@raJ$A3-Xbhb;f+UOVL(*L;R-<_hOrjqyQ-=@=MJkKX7N$OI^F z!3zq$sq2+413K`c9&`+~UkGvO=FcoGtDUrCxVvSyldkTt3YK%Q3dX5wYsaE`Ln8}C zbPirE1VZ*&9o%$5!H=$8yVgPb;9^U|y+QTox{GL`dECeDPbb;l2z$05f0aADSoY7f zR5Z4SUyyY_o$uG)pacMjfxiA1vRYbtmG~{fJFNN%47ViT2%6QelvjVD@7g2(<8AI+ z^u>jUFgr(}?|+(ART9Vs%gf6LT*>gv!!5gHmYT7Vn!G`eNqGfZEn={$Q<%+$_DwD) z5j&Cd?&sJ7Pi|$Myq78~2=1o50%4g3J4x4E=hsjjP_QstpZY_Id~J{U$wfZC%Cct- zb{zsuk|%#XM6_OlS;0cAgG7xdP-Y=tR%;Q zPZfOo1|X4m6hA7-@UrCo-<=rnkmt+D1}<9K zbW~=Y3lvW|CFwR?yj*>$=%tg{=hYPR$hY?CUD=iowY9Zl_wKEYe5gTI7Yq$vfZ?S5 z(VTy1uOfc#^*aQdJ!3yhZ?9ORXQ50e?_KEORg#Iau(Vvc@7B~Ue?1#v1q2=zV6kyd zfBG(L;=$C^uudhl+u2|Xi*Y19{SEE$_cfAUcE=*}d9%EsjreKvGF7>ln!o_H;^MA& z+|uU|mcu(cS~gElfq8~PCHfv>LyHyD?*lBvbk}`ZSuy=C9m4Xq0ynubQ(fu^kWVKY zmm0_qzp75ni*=q$-Y*YMSPmSA?})0l`uuXr z>A?f6&&WtC^F@fMO;~vNksA<9O2mPzl9Q8Ti%1VQ-UTFoW8Iz5sK&6Sc^KQzavTKc zptaiX2I;X^W1@9HvcB^vy>ONTn_HZmF=b`Bhw_l|_yJG>%VwB;zI^fa@vB#_!n;S^ z&@tQO_=_s5yl|Qui^gf=StqG7Cfx%$( zKyXYo`m)ZC}qLVEfSn0{-+;Y**foHHn~6*8)T z#Cy!{9J{fCeRM$&O^uAIm$&M$*%P)l+n(HypSX*l*HeJiPx6c_C*Qt%%XxUh&l%#1i-E8=N6mlEUg` z0y62D3nW(vTqc6Qy*+8Oz^{S)xoN#CD_Y}%n0GfH4d4$SV4}A zVxv3}BT`h{p61<9%u+{rM1F{O2fXftmieb37*A)^My;%<*rT<(hcOgloxcjl zX&ue(M0DVT#a-fP6pEifr+k$t0fOop8d=eG%fL1ooA##9Pbz^v_65D3QCw`C-3(jk zEy!MbHaP}6XzTvU33O40(K^wWc+KZ(?f&*5vG16 ze3&Bpf_awC?}}G3>i0xBKqqW`mAslTIX|6vsJOXWu-j$+ zreLmw7 z?>UKBdc#F@nv&1UUMN$m6cUD!z8`rks!->1~)lV{^MN+^0%4#eR9D`6tK?l!NEb7SFiJ&M`8-d{|$vg z-R6IsqJ+HZuT!?tBnciilWuTmDr-n?YunfRf0*3VJlYg3kDdDZ^{bVmBVJo`yQ50R zg_)n9zqz79^<+FTah)r93Bf77_OU~^yxr!+|87GuWg@P&%iw4CFLaP=9T52^Y`>h$ zz3yFJDI-g<@hT^htt`y)|vQET7EveM$067+-~?a*fr*oCwxRLS+G~ph%YY}mVH7rZ!mUxx}6}zk>UNa z>^mOs&2a`}6uMh)l9-fq32~088N}mHnzvrG=M0F>fMl)awReuW&XBwhWc;J==7c%f zAdj3Os-dYuR1qxs#^GZTm(4c2ioNjYg{f-T4aJhgb6>wLsux}ThJ*2*Z13_d#HkDr zenCNT&qfsd8r#IBX<(NrW`*onZ5edD#-k8L*Q?;j(|ZV3yU56vT+bOt!{Hyt!@=lh zu13GlaFmpk(Jn8JR!zspjdumoErnc8JTJN~?!s*s9qsgdb8UiWH%uR>VETSE0@!r= z&F9zl*UiS!%pd37p3fC-2~|~iFl%U?15USA!(pD@S1sHo?A^KN?PT}IWGX5$99b!y z^>5~Hw9C93N;1@r3RY59u5~`vpAbnm0c*Ih+l7Q!JPtLU7)p#IBp_|I*vLv~{;U&c zqJ@MW7c)&R(sCt}Mo=zWVd~~>$8&zwysiU5eC5*^<~q73WQb19H?|fXni-f?9U!M( zO;MBaeNSke9!7Th$UvMpYwwKuJ~l=~Aln1Up}x`25?S=$CH0@ye$|SM2FGGB5lOn#bsRcTVFsr=%%huTo1dthPJNr3S{n`2BORt`WZ~v~hG6BeWjBeo%zlZQB|(s*#d{l2y#zvs7v{w~4;K zM~{Jl_F4K*r#+@SEL>>rA-_7TgACj-)*v*{wfwSuAa$UZtS<@VHBN%haiIsHg}=h= zJsG+$a(_BC%{tZE^!lcxq=a1Oc{~yTV9V&miV++TbvDSb#GF#T`4+jW&5JBjIF)yN zVdxO90$APh!Pj8L8A4!x6k0pf%g`s44V`(ect=o;v#szuwMgMmoTFtcQi)8iAJD|i z6~ou?rgxRwzad|h0iXO!3D z<>x2EErOijz*ZNmG}JlbP4}{ZGRG2SC#?v%qNW?iz=73Mxn*0Sa_i|C=|FtVP_2L( z=aPFo)9_f(#E6oT(rUVWdS!)o)5k5V$6jX+yPTV|vVY1(mnYi|py$I{TF&7p)HKJ? zvJAecYf&=a_zKiC7m2wv4Ti}{dr=ZhD8@p&6R)f6A1_9? z?@!O{M6_z^kDkcmz-5;|GUDt>QQ%H$uV34WAErJiMb`l|^;}&Eu3|ldRscQ{MF{G3AK3qw`tUkWjf(bn!d6J13MWMcE3{TTK42_^!SANA6oMxS?Y4u+=e5rTm$Bie$>I8IN0rU?i^Sz?KV|+`(7U z9`OB}89AF2KrjFRxTATx?0O=8t7&cSqq07Yev32c_OsUgnKCtSmC6(z|59@UrcptB z4N+&-ME%%|d^dgy0fIZB7*Ejc-5~BH_ixb?2J9pf$<)u+SJe*qSq)&o-f4#k0F{wj z(58l8GuG5e)aB17Z8+p4p$hO$NobQzY4anIyoFk$uOF8eQwE&&)C2iusOJfa2HCes zjxS!!ey1}3umIwwuPhP$*P~IBQy})+cdt zUv%DjY-;LQJstTQw*D}spQ>+D?&LNje5?iAJvH9r%r2x<*qTv+s##443+NNvl@O`M zERi%N$QzO}am55r8Ai_8qg<*pqYBQ;GJ2eL!RVdu=ytZ-l8%T%w3}#JG1*$0Y~7o~ z|AV7ntMGN+rIA_0v01Y4C&p_-7xE^BsAqGZd>UW9np$r$@Jk)0&RK=SoL^$)u*cuu zydkMD7ylO4*NZZYBt|tWOQE0f>+F^+%iSBmos` z@#}l(I8^{Z)J)3ZaDa_H|BT$M$Y)JC@x#YHIV-hF1eHgeQvww_!I7rqr7pFLDoqfE zuGl{q^9cn7Ww`oq(61>R={i|tZ24~e(RIHQ$y7(5sTP+sjtX=>UOORN9ncEpyklf$ z&LK-Y@wyw^lnI@|vhwbC(CY@gX@KbjPkDb5W8wZBuIOr;B!?yw@8n z6L8_?!BIdY3PACmya(_*Ghx_ob+B~jW7(kKs*u>625);!nYG3*A z=x9;OGe|MmT*bA&F#5pbS6;?#CRzk%!jM4y{iWlTqDs2b*>B$*#BUVzy6l9TRZSw` z2=4ET*_RqiUvAD=&+Ht(v2W_)5D^va6?)DJT>ZTX=1P9O5sER3J6V7j@bd7K(BDvI z52;&$)(AM!ZF{S}Hrw@DR>8QsHxxlX+ecrV?oX2CoFc&waPkkXmJL#B4S}f0(E22t j|GyZ9{qLM^{OVdRZ;wW0{(d=m#z{5YJc?2a#An<+wSxWKBiJ?r_x3?`>M5}32rlyI7V0@f!NzVK0b>F&1cM%iaF;c z*D35Bx`u{W(a?#BDc^v(yq-P(i|Q(hI9;}w@06_v!%Z3)6MR=6z$=j)mk*+gJ_SN| zJId?rhw>=nd#+M2BpwYQc6Wjqqh=!k6YuU|MMz)rp%aZt(8iDGCghHJ{nYH(*jRoR zNh$>a2CZ12L#ikFd?m{|1-?)QI~?l~%e!)kT( z+t##H=!41S)JCyR%})pVxeW{q&C9mVV2=LWV==6^qrZ4N;{2E>j?%nk^$nPePL6R; zE58&tx^mv5f4))g;SJ1;PXD<^qjrZ|j$K8C7D-e@?6O95rPY^dvOgiRs?k6nx>!Ch zyG=|SI7tTPx9ig;BT|_9dmu}SSS{(MwOHzkty?XJp5k4eIrK|G%X(4gaQTYtTEp$oo8Edk}e%$GNv2!U76ItIQyX8L3Pf3v!>ymvYQpeDqA&+ zK}`{pN;H;tqr%(BEzRyFDihZS`i5};9Vi#gce=V9bh1Z!+KG)AQ0r1t5Z?99sBZD? ziHkDFNVla>DlZA9{VX1({C$X?nyeW(TaNSWPeX8ZK6>=1b#isDX87!s5ZF)XavsdE zI$zQYJF$|s654-^i7R9*8bZ(TmKqFt8^LJng?Q>g?0;#ZqN97}S`I?S78g(Q%V=^9 zd98`Ehs5kh^2|pj1v=Nqj<%-yYzK8fH>oj*dt#q2^M#+9w4-1M9g-KDd=Si~B56BW zVem8x%92NusG2_nOyDi+ZH}cqf1{aB+tAMT_Bo!_AQj3ccOyaC#A7~pqc)ZdAthO0 z!Jwg5?!of z4zkHSy?0jpUMmkh;k+aIQVVr_iZu;;qUj_6^Vsy`Ei+oE+Bze~8}{GaI$cax+M{6?Ox)oGf$%+lz`l z84`t#4P;sZRqCzHsQTd&d9bRnAoW~DMUtbionYrrDso~f)kdM_lv*C(TGPGmxKmnD z_r?wyx@dBM69PgODQqmXmz*4PtvfW;d&%o4m0u?aE(!(@BfVTf?;<+kji#ePtN!}> z)`z&}#Yd?5xBz-(&2h%zx-sJ<_Pzj`@kbpl^_mjTj$eF7(S_0~o|dK;k@+`UEf9>_ z)iW*5%{6>4X`#j7j>&9B@YkLIhhA*YGi1_K72?Q83o|%Y$E4g>sQLtv^Ep|NX?tfG z1U=@HXV#6=h^w8cw?FZ2?z66;95@&E*a!j}7%<%@C*(kaQDBv6rR86F82E7BgZQ32 zxQrI}dJtG?`E~ZZYp2dJqr9wsBpu5RsZXBAS(b?HNZK<=CgjY_j9%GzK>W7kTgBJ) z7=A}5r%A&qhZs~Im%gV|jHr*s&c8!(0X&S3sS)g{0%Jt~z`%%J`~-d71|DgwCEcRa z8&?F*X%CknqrtuA48A}A)uBcQDRk4U`OIU_RuL2~QA*eQr%nxzbo7FE<33E<0%L8X zJWS-V`R@L6<8BISsa0!^t&=VrjbWm_a(v>2DXddn9}wMFM~q6(xl^d4tal8OXB&eu z_0K|Kk_^bpFPBe6HP5qdCSC8_qmlJXFE@3S?15D*bi+ z$K$$<|L&-q<5z&1i@D3qW)%2K?H#uIOJCJ&OA2$9MNDHb7=yM3*K)xG7E~S~1TWOn zsBJezMwxrv8~py+rFHFW5n;rSMUcvi6q&mzBf#}R_?U;P9W!R7k1ITJnWa5lZP}oO zqUYwQh2SRrdBiBSRQ2K>^vd0zc^u=OvN7EGBh3fD3Y7E_2!>D#Gd<6D>>!R5?%IZi zf3zE0_V7-KKsZG9l4;v7RekMw*?9TXs6Q{E%K$PSaEciHL~A&V#F)Dags&*&L&U-m%`k7unWj)Mn--PEw$5+5}`b!hQs?3ir8s zr3?<^(=#ymciAy(UK1eq-p9JwGA6lj6jjaK?AMrZ&?gLlPi*(HgeAa+G%Gg0n#_Qs z_OOUyQFP6#(a#z)RtYx^XyrtJ}Pa+%Fg3K@XX3YtTjUWn}MDG=Lm`jc=71&6aW9y&SVc2@z1LiyUxyx`po2PA+G1 zxPX+%#l^%d=z+4Op3O!K6p}}yMHuW!aZXVh@#{xoClOLQ5l*cs#L=sBad)E zh)GgY$l8IFre^q4CysIr#>t$bEv0~O@0cN+^aQD&G(Xpvv;24XCVAG}{VDl)EbDEx z9{?b=1^S%tYs&@{#!#)lu)HX#+zLaekh#r4HZCpzey970m$#l!=|^?NW=dOmFD{oM z@pCyh6Ud6f%dtje+xI+xX8rzs65F}x*(TR>sFIS>wMf3eJr>?UI0}I0=)xGc0R&st z3)%6BX`5bm^(pexC{`>TfI4sH#}Fzt0W*pPpreE9>;ch{3yyBsp7KS(aFSLi=e=Ny zh2m4E(Zu-DrrK8fTAj!S(d#pv{ep^qdNObGP;H>67*;IEOXInd)0QT`%fcfzB)m33 z4~ZV=@2}zI^=fT!(01vLi+U}bHMm)II=JqsX*}8SrVTu1<)F--`XTX-Ca;e<{E6Zf zdyM3FU@JOnPreLQR9yW=bv~EGtRtE+KvDd@dd$xer?xx%be7*(um0;st=tFSM%>lq z7rW%y0g7&Nwis)(fPhoSEp#nR-eB@i(P?1WcuPZD*|}*wfA0FT5DG(MZ?w+vSD5w zU#4BzDw8`h5GTe8SW>sqfCkX2VcMC8p}2>Q6h!9+N&;p;%iG=)nkO4h5H3-Z}Cf7y1S={yRsD2 zmD|3r5B$isknF4G>S+YKeq6b+LPg*~{>po`u&%VU*GDY-aZ%?#Y86K7dd3q}_Q*CB5=5|$e-kjINYF&cV2YzWU z^7k>N2%9ubQQ^IKrF+2rx~cKeMZeFS^C}_z`Sk%7!}{riJ2X#_#G^Z?#CBz722ddZ z7Z+EV_lft31fvROA+7K(ITEx2;=C7ImaAoN9L4NbwfNam_pDsHQIk~vtzWwoMeoEda7M^K|Ek(E+eJyCeC{$&OEbhVU*C2Cy^*hze zcc*P*$H0k4`|9e1_fh|SK+Q#gcjdmwVJkbPR<%p?cFX(}i^&uQKgPwykuZqWELK)* z-nAL}2wL}JPWJ2lF7NzmioC_u-~Sa~yXA7?4z0L76JBNUgIJ|&7#c=Q63BM>-W9x?0bU( z42DnUXp;LDhp`ECGU1ta`8dJ!Amh`Tc>ea^6)zvDkc)gQ=|Rtqtz?@4)TIE(b3Wc1 zZ7$&w&ffd2-etDQC@L!I3_mOhH|LiI16e&NfkiJn00-^s*SInD@==f4ci}3IUmo%x z5BXz8b(ZiVewYhe`#b#o#UXdGJm=({`}pDaQTCXkiK!{6vKtYU z1^>YF{;V4w5}3^_L@@T2HL1(~DY?qAFI`9~C5F}*Gdkompa@sP?bGQN|~9OW-}FL7?$1GWnn!++aFQP_g*Z0_`n-*g{L6^mqxVqjpX zB`1&~DjvdHq9SBa_KIF!_y#+s?}DU?MzE(xOfsL)02;#9SOo9iHK}lnt*vd7j>K0} zJ93OAC_#qsKJbMlgnFg`vNBs|Cm6X`P(hn}*cBfgXfuSDkX{Jcb(?)~TBL*2NL4=^ zEu+%Dh6!gqu^IWGMM$NbJ>;cm2|3{`#{6wr&ab!GI`?G#qV#ux(lfPnyv;v<-@OG1E=;!?xb zc2e7RMA#&N3EA}26b^~|_Y~uW_pa-YH@|O&q!HIjEZSy7_(1>za%E+ON~0`&3YCWs zfy7Dd^`XsHwUAVP1I)Hh_QhfV$SqN%^u77m4k-5xXJ_jK9(E%NN7k>88#RXBU{7T8Y&%$-v zGm%|+_-bZgmUU;YWor*H!^HwBK+U|pLtQ+5btwrGGFc$Q`Qk?nFaI*w7r6;qE# zhYQge+mfohwGq>lzk{%?TeQx+ z4%3^P0n2 zFc>&Zvcx-y@W!UIA>2e6>P9l)e|c5eHgoxQTQbRTfmkFh5uQBe(M+Bo+0U78_{(kSoCrYM?3 zvlq!``!eHRJy? z;L_@M_lZ5tf!lA>VfO7ei@=%Xfy4<`*4!|qP2If?4EXRVU zaPi{NhqJFC#+Tp9mnVLdVx!CzZ9;!k6~>SZ4QVFxz52Y=(f{Xz_*UzcLX$lnDZRo) zct%FXRRr7y$HY%(14YBo`m+AH`w!+=CUUG2&P1KvOP$Y-SUCuUKv7@ zD=PGD20vD89tM9qUtvz(*mvNRf&g&<0TZB%RgPNO^A7;-v7LdB*<))W6-xVeN6{)bhK<%C|O<`3>w(sv&cm1GTi``|=$G2c) zZORMocYUkor<+c-9St^nD?Y-qJ7pkCGC#unn4Y}9ZI$y^FCQI++$`x9P$%RrH)za> zDWs3Gbs!DhK@YPpNzzvb1_U}TV|h-X3)8=zpo5;}U(&z5Pz9@fzrgnXFJ=lH{!2cP z5=kk%NCbGQqV=_%Zq=v9bo3&bZp5U1gw~@DBe(q;&n@Xi@Bqr56n+}@$BR49O9{ew z@vYI+!|b?BN6WnhD>L2EddYA!uqkeP9mIMKMiwo3z-GHG*OlN{QuPN4#trVvf!Quy z5&-XZRat^iB8%31VNwW0_LUz%5+1N2vy55fDf=7|O&(+Q@9ieh0+kGO)7K_b_IUdQ z3NP>hx{bzaX7ijJpSbEWQPBG}K>$PE2)FRfKG52F^#j&KghT{SGMTkBwneDyP2eQW za?P)K?26GtB}muK$vKNp#)pNqn&YeNbo#f9s^D={JLyNyz{!0bi9^`XdaF_I5dk&} zFd?Bva8E0r#xjwu4SdN2R=c--xR%{Ntuye8{he?_Y>Qc?((=+5Y>COeVPemF3WF)~ zA6NI@Ye@y)4*D3V$2Qd&%Ju<8Q1-BYg3Mu1^=aX!d% zSK4EfaO3*>`{yOHX+roZydbNn9;+`K0v(^)`*%;~YXB#g+uC|Kl7uSc0yPz#Uz=mm zC_HPA-xA4c7uG1W;j2RL_p4-Ro^zGU+tJane*Di|raI@LbN-z*((p_b*XDCe_`s3p zC*Foxvp*k(>Sn3+2SsYzW%_Fa4G<2B+sLjVp0GnS@8(G%#VIdBGk_WpbrZh)&a z0URc`;o)9J+njkjxuX<`!{^wM+n#q7(jDC0{|*68D7vAsai^l(xQ^?oKtbXl%dIs? zxCBuZAm_Kr{pW&6E4xPfgS}!}RI3UK_7n~P_X{T{m0JQgeEA~V8Sp5Lb<N%GVq@t8X|Wf5I%V|#YehcX=_uIZt?SP&o&p;iVM-X6UASlEygv)uWk+7 z3kk#~e>GDjK86UKJgHfL4IB%B7xW<5k!zG#byY^nCriX{Xc6E zi|<|-oDRLfmpfq{a|e2q^pcL!{!cX`eJ!WzGUO>Z&8lANi!n#YGF&Fh)WKXoylDZU zPMwkHvvm@8s&I7eQ#ng_?3g~=^O~fW%iN%qH}6X8FoqhZJ{_7AC)mgEQ7|GNFStoX zO>XYk`pB?14Wio?wNF+Yr2QkC#zu{+KcUKh_q>~rtG|{Uh9HvU*3;ADb@Om93h$Ux z3O!_|Rxz7^`b-NrI#LPTBUxB#6;jMQ-@}?`f$w=IK4l##Mneee(){*^y#l}cg610J`wc0rNRX&Z0&7CC@ zD_HlUGFOb}ErRh+W+Ia4Sf+b#V8al;^LCxi)4WfwIoL%6mnh- zd{*L7c>k`TE`#p-0Cs9~G+~B!u{T}Xvs>4c;Sj`=cI@Jh)cXX3k3)SauwlzAs;q7q ztBk&aCb>bT^ED_hJ z4-ZfLu*%4{74LoLX3USmJ2f|CX+$7=<-wA6gVUIpAiB@5TwMGd2>1zx`(k1x1~TMf zAeQ-J&?mL6d)x7GZw{}AGwfx;4__y`2qjDb8~jpbGLW6QsU3Fn+TzQsZ-jiTIc6ut zR?Myt7gV4E&i5yGGx2n7e{!y=7~auQJGFjjVg9$2f5E`S+GuzMwP#^>zJfQtAiv#L zuaVAw;aAGtn|t-y*%RR_c>0gKJ3Gf31oo$F#AG*k?smhK2_Yx3Tdmb>_CDsg?X9tx ztRF0>MGL$0V~p)A<%Lo;ZIbh~W)|Wqgtf!oebZVRdHc}9NJYhFaeb;@PxCuB@D=R} z_m$z1(9n=|m)fZiyxJbu1 zTop6e>b8cCBq@vjd<*wj1oPR?L!-Hh4nQ$Kh?%7aQSGN#r;r!Pd z8z{1Q?szZBddzhXd`VrU{R{Ado!so*gV?bH!-I^i%9}AW47wZNS^+9jKbd{pLP{E( zcyQ9b`^34e7s<<4h;w_W?d#|AcxlYS`Rt0lo!z>DEIDl`2oBo;TU}j)k>f#;4`+WR zSiZH^tm-;Ti9G0CDLl^qX|YuLfT5f9!sTYpd(U9adU%ve;#Ys3oc&n5tXe~W5PEN| zMniUP=brF5^t)-`jt?3o7v~2${sg3oCMRv@&pOBoG7bls!mRlM5fSqQY z2OM;kUPC%tx?4qAnLS$DSA~%A%DU$V#=@;*cM13~|6*Tyy%K?)m6@2#Lc#Fv${OW@ zsYp|K!AyJWGY`EDH4S#3;j~Kjd&lhu*3GQi7&7uG1W}U6jLRr>Xp6dZy_BU~>E5D3 zShk+C6b)ZnjDPm)-+zvM&#FhnCm+bkWr#<8c=QIGEvT@#0An*!vEUdHL6v&#$|qgS zA|mJbO>WH_!c!GoP1vQdgfAI^zkr{`ea_7#)S7QhdVAQ|*r?1Z*Q55h<2KyP?u%BO zr26@tj8vh~kp;NGKj6mHXjiF}g}n&xxSsX3<=mV0&T3f}2%m86YZH6e40(!*>T6S0 zuHl$*lPcB5@FrjBZ)4v#(dhF=$}6F8fhWL3d5UevU4;fkhNr1rU12L?k_^%%<>js` z$6aAi{zU_vh@9Lh8n3o504(%`8#iNMY6`3Gp0AUy;uPZ!cKLFC@2u$?ZsLT!J-_-! z|JO>hnz1O}nK-aJzUxTE-Kx~JkI$4_TxKf`&krKW;Nx}DLc$)kc=S(P?EO9iaiJr$ zDbxfUetu7Ww8>i8X8UJbg(PM>2k6vsxH#g%aC+CFqFuZ&K zamy{;VD>k(Y{+_^V__9k01Nf6MOh_#5B7AEkw3N+swtG+6qE-0YNhvRoOX7A)tZdJ z&trF^^VWsS4~_2#?@fO7b33|wpt0Dm>#X)Da$>FSwOhwhJ*f@-WFGt}`h4|-n(sb- zrlxD#r`7?5G_x0g@jAP^p$|msPN3Jf`A_EN=1L22am(jtsrK1(dXeIo9y2pzahq;D zZ9U2=M%%Y<*-hxQw`ol(*TLyvQk8ov68|ju1A<7^2U&v>E`lztEE&?$YiE1`Xa6@z zhaN&TUbU#EPCc&l`g+$8LS)rx#wAC+rLfZN_^`>GgMtUATJ-m~)fLi}(;ZnfR3mVSOe8`5EO_Kczn6QR0{V<!=pSPK9l0(EW^z)iZ+P$bcCRlV>_pO|kn7~NgebtZ~- z&ly@akrb7hy>sWbKv-MRnZB$lgAqsBb^e6Jdeim#V47A+gsAp==bgR16PQ(QsSI;O zyWYmYBYpCo`8RBYxHf~Ls|q=1(ut5)4Ut$FHUh+vE?8W3*YvUNP2GRaIq_!qw}# z#}VnC>)wm1oQ*|ra)@t=V@Ln?VZ*^UoP{nS!7ZVh2(};{rt1troNVfOt6v;_YvtJN zD>p5GWr0{U&Cb5U(w;BTL%%!1EinO!&I6qA(Av>49nbVyke?(WGgCNNuu-`Ql2iEA z5vH7}s88l_xU(z@J*y%486>zb0=s5(pd`ae-4RSzdXcm7Gn^8#O78hZlZ&5ZH|vZW z1Q&KcprHy%2ysiSCSs}dh>&9S*1$?bEX>hz2fyy2By#GDs0`f}`)pVv8<_uj(B9te zfx<qmB~?uM$T5la~k_q#G(;R0>uYkdqUL~b29 zBe*bJ4BbQFV$Nq?baZ^X(5{@V@T9X61bWC=ufzQnA2{4h3-kE= z`V9qkHD0`UZe(K(am_Q&^xz6$~>(HPyJUdFv7Ar)bKe3JOY=#gE&!{hJ2v1WBq8*dfV%{<%{oW@-)8 zR76K~_blXIll8z#8{TZDeV%=(2@<%s$7COaw(lc~m~1!)mP4S%8ZTD=#lU z{QmCqgzm;qHX`-sv(&pYF8lJpE&r;8yy#&-oA40!snvXKZ@($dpM2bK3j$z^Cv?bF z*VA+FTBzlMsB9y~xrzSr?pax0-GV<>U6d4zU<+-z>*wai?ONKb0}vP~m?0>o4> z;@fUd3=+2S-DBlPmDAHND5cE_3A!Jmn%w%254IVgcUK&-P*H&SpCZ0}`*|N={S-Z# zY+Ceq$a~}o_VK@{Ilz`}@PO5uA2pCop`g|H#mA`pS5km*I^LI%SiH|f)uQmj5{w9T41XTg0rPludkc`pF$6$x zb)iIic=o_3Z_rOJv+rngL&KUMDh+wmAQyQg2s>xicx_&{526_Yv%Baw6pKf@#_{9g z-4$jAvb?DvH^1ho;?!Nd<2rRShu%DMwXw00L*HP;?F)NsgxfwT@eRs--^Mwr>}7^N z-^E~5O%9hU&MIXAYu)^(mpKf~@8apJ2RT7pjg$5KDSd>jp?Omk7c2Dys^--_kf;l+ zx>Gb?J2)_c-1-Hse#e0`1n|c~!EP?Ib86oO3M(`GHatKmN79&(KsP#pcC06KB%b{- z?ykV;$&!H_L40s<@MgYOVrt2we&t^?cyhJGoEIS)Sm@t?b23tchON0DXynsxK9M1t zM+$28ZO40~znHz#1o+ zs|mnk!8QN+^T(}6wrMpKq(C+-w9FZ>yc}5V6OEmt2s6%|_XYy=P^h6+l`4Unm63=< z>6mE8G6r_4O-q6X+>mONA#)B?ZN`NnXprV6GN&yQHL~ufuUd z?tFtTxLjet*}gkWejT`<|H`9OsQ+(QXF0i@ogKkz@Sa>C;)Q}_dgN>BP#C(j%O{NH zrM3TQr@5=-R%7fwbGhRLdeL(O$Hr3GUUak$t$gk4d+dnyxw|P-fWizpVb-i$W?W8nDK>464j;6&wFMZMm>i!5IEkfr z$lsN5vs71~kXPoqs*pfrA~X`i!ZGIJDZ^f>hvDHtdtr)YwXDg;GxTLiLqicW(ZIzs z2VRLe$s{>5sBv2N@VQQlfBi<76Nh;#+1sy_bebYN|F}uz2a5->M|RBd0MjhhtgWqG zFBWv{FJOPyqSEa0lU1YR`P9^u!GkSp=FrnI_GMc!=FB%zY*bWK5uR%}jL&DAm$5uf zlvZd`cn;0EBfs?Z-D~P;D%%Q8u)>tUDQ}SxwLEf_WF1mbQ4zn$sqtWP1XNwQgV2Mh z1K!2%dm8fF9K6z;Od;Gb7QT74Nd)Y!elHFp!P$cx6_XUnV&O5FxrVzoN60SX!B_^BMK8Td~ zkY_Hroz8&5amnqoRnteM_lt{SGQJ#}3DDP9=H=~@@msGwmnE~KDHBMP+`up4NB=2| zw(e#c$U=@RCeYE#n`Et~mc!U!Ql&!O4TSLpfpEZ)e4kiawjgL`6|qpcZ|FG`uSMqI zjQG>dtgIjRGu~X;Bqr+CPd80ZJZ!*aQu0OWW9D^0JWdck;IHlrB*#@wpm}%2Uy{4~ zab{aX?Vs6GMm_pQPGOkP+25bY@7%boTr*)>3h(xB7#$jFnfgAY)nAukeOkXagIpxSD!Dq{{)}^a`RRxk(on2@qhu()rxS_#>ZB1VE8@A{V8?Hb$(lV> zHNPOB&Ydy!>(?(v7pZhCs)qGP-m}!<0vy5*>OO%YDydwck~^m>M-FC>rJV?g2KJ^1Asmea77779rcq`asGFq zt5aL8Ki`d~)DhzR1#QC-R+5Q@e-r~M2c(0W?B!m?{3w&B569%f$#?rNR|$Gvsq?=b zbR4eM@L_6X#M9f|ooe$t(bclcA5&A04&IWJCrGj0V^M9v#?T3TFXDk1Q)yI>LbIrDK*01Akn+;;Yz5KUa%SdDJ1S|g zr7XY=N&rvLqlxMW5Q`~g32!@GDK*1WhPL=p)9`UxFtx7`{Iib?O!vmorYMk0z@j%n zu_;dx+S00i6BKS{>w7E{{0$!qb7(qPafM^fMwY|HpiqOhcXS{bw3VAhRsT)=t`lfY zAlY0mw=K#pE3?hQvq^gWE2m5jO-&ItKH|dUIR7MG*~~qyE_OWd1R7?5{I_!%(J*B6 ztLt3a1D?u{GA%1wqiC-$d1w!XCeq*nP{LPwk5w!jx97FD9$4w20@58PHq`h=uvj6J z$)Ta`u0JJ;d0ZAupDGJ2JB?s{##x=y{RYIvZwrqO59^DP+G?ooJycg;Q3u#U2^2Az z(wZ$FN)-L^n$PCmR&G46uM6$bm9gE7w6wGs_Z2vr{OGU_?gJ0Vc9X2iZ()#G4eg$n zdNX&EWgW=3#tlY~M`sC$*9ocC6JPLrNnRLPj3xL<{2KQC+wD45VHLn0)pWF|@MpER zv(p=sn;RUZYN2pd_Vee@uEWAa zGfSg1PogBl3r}gOL+9R02e@{G&CbrAXPH)dAD=)>xV=4oT~C(up*5EUvuc{BBgfQf z8J$Amc1c2leqTN0DKl}x!V`7%`L2%=?#tJNp%mRZa;4nYkPU!ZAbOOW2a{$bo=~+I z^5wES55HHkypnPbp_I+KH!NPG>gx+j2BEe3gh*X*i4vl$wXHe}%eolShr8hM?JCY4 zHc#XV10W1`4h{}{o5Uhm3Pa}P`}V`-HPelU^fry#ckU0 zeL$RW%fxAf;c?c_PeO~P3Ztb}sonkepPfS=WDJSOeS4mW2GP4{YEH#*d9pTin@_@Z z0C;t~WI$m4f+M-%b5*46+M+1{RT72w*?=3jjgA`ny?8X>i1GwIARSv55x0o$dApIB z26c+79L!-RfZNgtLtei{@cnj?iezSF{3@J?s5kh2O-`=biZXL4y#G(y8!E%zzbDuYe+(*}eKBHj zrOPcz)3#!`EVYJNB<~8kx_X)>LfQNBB?M^ixKhZJs~j ztQBgsc}3yK1IlrIiqQ*rj-~psWjC{2xXKfUqxs$4`Nit0 zD!F|4B?#*!VsRqiQ!wUKX8omW!V~lxN!Ale*3H~zkSe8S>bQ}`-i_7O)p+f+0=POV zrz&7$Pr?-Z3Nna)$IT{GuY6qUOE0~|XSTXVC3i5muYMC+Tu!o~1<9Kz^8LhY8nBmJActzqL6MZGV2)%&L#!GX(=J_Ng+bosYFCW=SpV zR5?#koz6w(E(1#PN2hyD0rl4_PYnpSuoR<@E>VS~OgbeNz{wG-H-q;Fdy;vJS=pw{ z479X%YV>>HP{OT`VAv9Vy&=tuNJ}$g_h;Z0+*Yx3`i>=5U6;#(?@g_VdGQW>;#{8y zDrGyX?k1PGynb7lze$}yuSa1RCN5Vov+E^uY6YtI-#1HQV^`8O7E?fZtb&Dt806c> z%DaDTpWEC(;fNraH`+$%g;0f#4z_g$1iI@KhL`Rv7?_=XJK%%lByRUfZzyn5Fz$X^ zA+Yo2xDxM9=8~+Dg}7L1icwBfaS3K&*~a5o2^1VU07tDWgZEqhYW-=? z6f$~KdA;)eNGKRqA+R!)9VKP`!p5Lu$gFfjneX82T62(h2mS6ou+Q<*O%@K*N^3_E z`bwO(?47BU|?!6ow)&qmx*uQC{o78@|pV-7nt!|>uTA>Yskw@ zS~^f44NuPz3=m*}=>6f|bnV|_jC025IP({f^S~CUWKEJOPQvsYYJYL{8p5QOw7i0poKf1t2W_~ z8%)IFNYH7UXV+8%0<%EwZLNQ`@c+5{sM}A3AmOx#6A-2Io|}aA2{xVVVkM-v)9-KC za7FO+wSipRKde78d?D}=Zj$JR7%cPZ_k; zey2AwiUOo1$dN*G-p}vj!4gFc2YJG^Q%+RuZ*SzMgPOGiyfqQ%*6FAbNOL}TC0&HF zqvkr9M@q7fnO0sPQ9;>1*? zBc&{QOA(~iXi(j%D|>vP`yYaMAmky9h&vn(_pgC1_UTC1OQ&xAUj*?h5E_wSfo?eF z%cmbp>+2PFFdywf@99a`T!~@B*Rx+kn)N}jyAagXGBGVv)1j;5G!>>iB|bwZkTJgECPVAE)=XQV?gY4R&higa@Z}fc;)2c<K`;d*VS^v;2%IlIf&F@NS~|BAK}; z`Z~Jy;xp5|gn)}fM6VDHsm`i(f_`TkkPp;ZBa^$YX=>|ZmJvDohrdI9R{Ht*CF-4h z+9krmAg}0&^O!)jWa?u5DD?%SG(nOfZs-m_=8Tp3@=106B&-sDA0kRlG^9Ig((}Le zI=TDW^w<54iia-s$!y2k{(@@~Za_f8lNqQ~>?joK5Itt?(UTODy2}9vB8U68)6-J@ zp3<+QVdw2mqE8aUd9`~S-Pfj!Db?$1KG^7vVza2CBrCdQ=I>x% zUtd!H;z?~Z7iK!^c55c+{UT^~2P3P~Jz+L$K^{iC{mkzr$DMa68Ln;b)&LI!5PtF6+Y)c7O^tTf00 z1_9EwiN~i*m6&-B>q`Nw79s~a%H%eT;tu>TvsI_%z`3)2WI;>Anyel9P zIr79IdrDj_!gO?W^j9ulF6!%*J=NBec6iZIoMcr{CkOGP5jj?yEf=__nc_Y6+rqYI z-lLi~BS!|yk`pbwv%7mbqHE58@CWKlcEcoWZg*g<NnWiiU@ z9K3f}S2w;0zEIHTY`v)CKBZSv47Wg)kUj*$=ux~^bIxnDyK+cqV3wBc_Ai-)He9{O z+IU816E))L!)BL<=e=Oc75YOdso|jDcwUW#>JYA$k+zq^sd3&^kmNuFXTkrWzewvB zVJP6?a}s-)dh5`g=#>z0NIX!2MnF6L!}_{VA*h=?xXhtX4ph(a1?l??k{pGY`2@a3 zaAWkJQ)&@$iq)oLUtdwOEZ6dZ`mD7UkwibEV5Cm@uU2A%_q?D+{#<_Op;Wn{L)&0s zqIgit43drF20x7mYpkLh-fksf(s#Qnoo783LAUia{_G|6cu|Xc@HgL{+f%$Mged|D zn4m4%8k(_@0gAGwG@td_>w1)^KA5C6JtZgG#Lon|B{@XS1B~RiPz+*$&UOvKpMkJ69M!I zxyV2rIt`?VvztNt9Opekq5uB04Kxw{KmOM5@XV;AA-=gw3sVQkFmMB=f`wv82 B2OIzZ literal 0 HcmV?d00001 diff --git a/Phone App/android/app/src/main/res/drawable/launch_background.xml b/Phone App/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/Phone App/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/Phone App/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Phone App/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c79c58a --- /dev/null +++ b/Phone App/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Phone App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Phone App/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..69a543e2c34225cf899d9bae24d1ed1a2668967f GIT binary patch literal 2170 zcmV-=2!;2FP)3n6 zoNs^kp6`B~^E=;n?s7<&AdpNVjmt@0smV-Psk9gt%ZzdJBr3T907!%oVt`>r@L{Aa zy3HGh0gCnqdjodQWna7F{6O>78qbB+D%uzFPvp7Dm;}8_Z(P0T*%|p6PZCOL;`N*q zEfA1|Qf5+F3`t6hK0o0nnOm4(xX*c}=}mj(xxJKQ&@tZS*AEavpkI|!YFd}Khme!; zT=*^&kW8Y}7c3}CSep5i{aC|`_G9P&2w|EYi#uk3BpQXz{7liinv7X%r_^YTY!n%3vNJTxc8pbJ8B3`KYU&UL2wgP^UigAI^z&B8K5PR_tnOH8^T z6bF%n(o~$c)mm|)ltfaAbVh#q6VZ{p-Jx4FcVm~j_E}P!WnMujWqP3<5*v_|P-(L* zMWl9t`F^3D5i2yA$;(M)sxdc~c!@zch2SX7`h}$2q?D5Nv+GA|X{5fMYmZgxdm#jFdwau5J1ra3Q0PRt#}+a1#Zih=0>#Q-TPQ4}F( zTBKuero9O{d|nmrnB9x99_;SH?j>$^!QHC$d&TvNjQR)gOisP4Uvd z=r!n?&jU5qMzq45&9;kD7s6~!BHDW9J#hFuocl8+H0Xsd-e=l9c6faL3c#6vR-&1C zYt^seH~%~c7kfLVHHZcVDlS$-tH%!8?s*tw1PKWW4g9IF6n57ihU)gOIP05}06mzN z2lpk<2LeWo;9G{%?}a4QcW=URptdh5VcQ3rPEC4Wz(s%(Tm zIig3|ZkAJ;T?9w3oCRmVJK2vj`6BHN`JuDF7tC=6h~6m}bLU_ZINe?+?iZnm5i44e z4$>4nTD2e2bSCUDGvYG*ZJlU;M%u>ZC`c(hw|G4=YYoGkZ{1x`R{J&z($v%uX@58f zP0mhWBAylhb(yGVkG74+F)DSiciAt%5@#HqVy8PV!HabtfItbvmBrNjA)ZO{$npC4pbpNVQmlhexnK*W^d3!DWA zPo*9CKZhUBOdqy&4Y=T?y1#*+3POrz^1cp$S2^22AB336ukbk&>HW)}ge<*z*oG(e zY)t|>u=45Aw;I1aAx=lX13gyt8asV&*5|QooQSkSB8AL^WW+?&1nig1qiTd`+pTa? zGJfHaX!Z=cCcQuWNcBDcg*MNqZ}HwTIMbkanofbk z*9Xt%J_J%i!rIhQIUH)Ph0m=2VcR#7N#6)(GQnx0c)YCvwR;?}>)uU}q>cv#DMLH+ zH$r;TY&d|kV1^OZ?&VlE&J)E;&JK9I`Zd_M>@hSyF@@zklDQn_#?OTE+P4u6w5WD3 z1xXZL0WWMhRRO$s%jkO0?;sFk~C?*qvy!MF<&TTK!Q=KyWxm=wCex&6(Dq z7#xxl7|MSr_khEG?EH4qy1ZATBYHcc_p`=*!5&u|L?(##V-5SY*_O5H zw4_2oy%QLLF6a5~BXv85+TUl;VVZ9Lb5+S5+kW;XKIlUz2ZaRX^>??uQTY%P{Qv9O z=n{i+dL8(Pn6&cZ6ViC~aFE0zoLq7!ufM1D)qfNPd)yt7oKZCxK0gN^5o6i%z~Avf zAEFUX4mp8tS3}$D$2UX=XUqU$gq&X6^Rl>{IYsI_XWgeT>+hBu;?f8u`#<+2-cJ&vzOYk>fiUe4 wIdS3Xv-W;9_-%Ks=R)hJRG-fs>)A2>2Y5)8feY-QD*ylh07*qoM6N<$g8iWwE&u=k literal 0 HcmV?d00001 diff --git a/Phone App/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Phone App/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4541dbc806508baf042f1ba65fc24e4d50b8d3a0 GIT binary patch literal 1235 zcmV;^1T6cBP))c><=Q7n1#Wn66b?NL>-pMHr{Ip2rYD@H$Y1-ZRuUkT_A{n>1pjP37_O9eVqI2 z_j}Jdzu&p{l!AE>E{{+v^vT6?UHoRTN|{DVMbQL7kn^<&|Z|}cO z-|nn%Sx5TjYD>-jfD9K$tk|nRt<ARiZ zTdMAym>hG`Guu95AZ+zB>okYA{UlPz5`wOJ0SJLWsMM#vD^J(#=q<0?J>G5ked@bu z14#Vp<1f~e5>W(iN}qr*LaI(UYWS_EthQj>G-!k=1Ef?eM^`?S#ZN&>gb|t}+kfsZ zxv<7F>a=kNR_xKA4z*#k5U3|q`|`^AzPoym5=P78l({MI@GZ($BA?g3Ca!HdMJaNU zi_s+ETad4yOcj-#xSx`zuiV177+hq!_(Dn&r%dNt2rd$pLPrV0M9cUVf{Q>RQc}TM z9y|?@_Q#eIi;X1%e6wW07v5i61VyXX!>5gvV6>V75;LdYy$=H8K9O$`+Oeig|^{7NRQUQ zE^Rib!jD1t?W2mm(3CUFkb z|6fqva%Ijohu0KAaq32Jdp+=lF@&B!voY4Ya`zt=G982^M}$Vh`(uFPm$iXLJaTWxfQ5 zGPXdtPy$~+xD+^rPaq&^6yzo@#@ud0eeK$p|O77t6e zlp(<4H^8#&gM*iG(-bpcYYo@N=JPEE2gYJ+qwKBy*A}Y* zNBh7{if-(vh%scHBm~5w+ND98;q;xZ^OWnK(LTDdn*R_ z>82f4m|4iiyDd#UWwnKxBipOdJ}j>)=fUG(hI-Fl+3DysPe0Chq!N#t2Jd&5RIODV z$SsS^(iVqGfkOprq}lYNxw7H7hjt9j{O+-(glBYoxbNGmZ-`Y*rBT^Q`(-KW0<;^5 xHUoqFcbj3xLbp4*%=L6*=lO{N+no2S{{bbJpoJ7Dg5v-H002ovPDHLkV1jcRR@MLj literal 0 HcmV?d00001 diff --git a/Phone App/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Phone App/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..1798428804a2ad3552303541e0d030667eb33583 GIT binary patch literal 2526 zcmV<42_g20P) z4^&gv9mjvkd-?N1_!E-w|4)n>5T*F9Lm+@?sjIFsPdD7^+M-VEw4MLzHd}Y*T90k5 zRaablY^`f+)vcWsY5g}*r)2^v{zp{A2qGAP011$PByZ=%LX89jHSflk_ceQ1D3t?%^*D)9 zMSq7M{-K1Jk06rpkw+5I@^nd3%zTn0?WVe>V=cv%TXjdux7gav?E|@PzYXAu1cZFr zs6{e${6b8?mp{Y%A(R_cn&H#F=#y{=PlZ0*nI`(=Rd$U$il zFOB~P&tIf`XvGW$+#C}-0w4e8ozM-k?8w{jj~`PJPgAfPC`kg*<=JV5IWA0NjsIgiIUvM%~t9o<3&m*59d%}^m~Azq9SXWHzA#|f(J2Rti+ z<0` z7n^PjxZaqo7~P}rbtVHFjO&Xb0ip1I+Dr%%`8%a!oPqZnFs}2Ghgw4;8;QC^PH!uRyNncu4~5!e7(JzGRHL&LsU?D>x$zx#tz81fa(~%7?%NaMm*i4hg?{5 z_JQvI6egP${_y?hk5H+5KxzQa;llF7Y?!1N<&;i69c*)XA#A!(h)f$@ATz+9FM^dR zGhl4+5T|sDtrOlmxf8zAoJ1v!R*)E=^bdix=`$f%6yV}02`?x7)bXMd%J^MXrSSR1 z!?fm$#DMt;&$ujns!PPjcp$~l#EIs{Pl6-0<#1hpo3=cW7{JF|XNx*gNv%~8^rFpO zATeO$x&5F@3vz=ft7m~|iz)~3yZC_m>lf(DBb{#f*d zNA*_S39?qu%{Cq67Jdk;)Gxs3pdX)x>B}czefn&8=fn=!d;JtDd9;DFAJA&)fCYy) z!K8T z8QH27i1w8`dbAdMCu*r~0l5KPRmu$bNr(z6o9m&VYT(h(#|4iw zfTG=Ma>VX^c?1}&=*Da5z#|Rlg6V}1o^F6Qz#4!U)&Rt?1|Wtt05Plqh+z#t3~K;l zaB7D*_PKFrpvs>51(>L9PEd>SQp~vBWYJ-SARM(4Z^Ul4Hsh8?gNBG?4M2glsjUV# z-PD~YB4ujSV!RQPR#$|=KH0lmr*a%$)P}^`f9lr2E(4>(oP>b^hbVU^(S{n7Y;OoDKa?kru|^}gExo8G9e{o?RTQ7=#0$F_*v23h~ru~#}%+k;>V?4%!S1k|g!z)!S}- za&Vr_VAA(?TAxFbcC*z~lecev;I?USouZv&R9V!3%e&_aU zOt|IPrEd*qH3x&ICoY$ci(A0KG0$0D=RuG*yHQtG@sW1>@we`7YW79H2jy+;=8n3} zM_<3av+RB8g!tK#WK|Ad;ydi26f*>?zP+-gq;hLx;rWf$A9}d`Wx%r{I-A>@v^z>x zYriR3O+?96KC$8HghHMu43Wh0WWLduNI>n(EnwO}KQ(|39I)6sEKQasL$$fS`I51w ov8?S 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 h-I?r}naIi`Md~l;u`5PR%(ch|Xgt + + + + + + diff --git a/Phone App/android/app/src/main/res/values/colors.xml b/Phone App/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..9a9a847 --- /dev/null +++ b/Phone App/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #2E9E6B + \ No newline at end of file diff --git a/Phone App/android/app/src/main/res/values/styles.xml b/Phone App/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/Phone App/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/Phone App/android/app/src/profile/AndroidManifest.xml b/Phone App/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/Phone App/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/Phone App/android/build.gradle b/Phone App/android/build.gradle new file mode 100644 index 0000000..d2ffbff --- /dev/null +++ b/Phone App/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/Phone App/android/gradle.properties b/Phone App/android/gradle.properties new file mode 100644 index 0000000..4147ba3 --- /dev/null +++ b/Phone App/android/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/Phone App/android/gradle/wrapper/gradle-wrapper.properties b/Phone App/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c85cfe --- /dev/null +++ b/Phone App/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip diff --git a/Phone App/android/settings.gradle b/Phone App/android/settings.gradle new file mode 100644 index 0000000..10fd726 --- /dev/null +++ b/Phone App/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.6.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false +} + +include ":app" diff --git a/Phone App/android_overlay/app/src/main/AndroidManifest.xml b/Phone App/android_overlay/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dabd850 --- /dev/null +++ b/Phone App/android_overlay/app/src/main/AndroidManifest.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt new file mode 100644 index 0000000..fa22bb6 --- /dev/null +++ b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/GatewayForegroundService.kt @@ -0,0 +1,99 @@ +package app.gsmnode.phone + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.os.PowerManager +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat + +/// Foreground service that keeps the app process alive (and the CPU awake via a +/// partial wakelock) so the Dart gateway loop keeps polling/sending while the +/// screen is off or the app is in the background. +/// +/// Note: this closes the "survives screen lock" gap. Surviving a full task +/// removal or Doze for hours would additionally need a background Dart isolate +/// (e.g. flutter_background_service) — documented as a further step. +class GatewayForegroundService : Service() { + + private var wakeLock: PowerManager.WakeLock? = null + + companion object { + private const val CHANNEL_ID = "sms_gateway_service" + private const val NOTIFICATION_ID = 4711 + + fun start(context: Context) { + val intent = Intent(context, GatewayForegroundService::class.java) + ContextCompat.startForegroundService(context, intent) + } + + fun stop(context: Context) { + context.stopService(Intent(context, GatewayForegroundService::class.java)) + } + } + + override fun onCreate() { + super.onCreate() + createChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForegroundCompat() + acquireWakeLock() + // Restart if the system kills the service. + return START_STICKY + } + + private fun startForegroundCompat() { + val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("gsmnode") + .setContentText("Gateway is running") + .setSmallIcon(android.R.drawable.stat_sys_upload) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground( + NOTIFICATION_ID, notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + @Suppress("WakelockTimeout") + private fun acquireWakeLock() { + if (wakeLock?.isHeld == true) return + val pm = getSystemService(Context.POWER_SERVICE) as PowerManager + wakeLock = pm.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, "gsmnode:gateway" + ).also { it.acquire() } + } + + private fun createChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, "gsmnode", + NotificationManager.IMPORTANCE_LOW + ).apply { description = "Keeps the SMS gateway running" } + getSystemService(NotificationManager::class.java) + .createNotificationChannel(channel) + } + } + + override fun onDestroy() { + wakeLock?.let { if (it.isHeld) it.release() } + wakeLock = null + super.onDestroy() + } + + override fun onBind(intent: Intent?): IBinder? = null +} diff --git a/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt new file mode 100644 index 0000000..f3e1ecb --- /dev/null +++ b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/MainActivity.kt @@ -0,0 +1,187 @@ +package app.gsmnode.phone + +import android.Manifest +import android.app.PendingIntent +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.TelecomManager +import android.telephony.SmsManager +import android.telephony.SubscriptionManager +import androidx.core.content.ContextCompat +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodChannel + +class MainActivity : FlutterActivity() { + + companion object { + const val METHOD_CHANNEL = "app.gsmnode/sms" + const val EVENT_CHANNEL = "app.gsmnode/sms_incoming" + const val STATUS_CHANNEL = "app.gsmnode/sms_status" + const val STATUS_ACTION = "app.gsmnode.SMS_STATUS" + + // Sinks used by the broadcast receivers to push events into Dart. + @Volatile var incomingSink: EventChannel.EventSink? = null + @Volatile var statusSink: EventChannel.EventSink? = null + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + val messenger = flutterEngine.dartExecutor.binaryMessenger + + MethodChannel(messenger, METHOD_CHANNEL).setMethodCallHandler { call, result -> + when (call.method) { + "sendSms" -> { + val phone = call.argument("phone") + val message = call.argument("message") + val simSlot = call.argument("simSlot") + val messageId = call.argument("messageId") + if (phone.isNullOrBlank() || message == null) { + result.error("BAD_ARGS", "phone and message are required", null) + return@setMethodCallHandler + } + try { + sendSms(phone, message, simSlot, messageId) + result.success(true) + } catch (e: Exception) { + result.error("SEND_FAILED", e.message, null) + } + } + "placeCall" -> { + val phone = call.argument("phone") + if (phone.isNullOrBlank()) { + result.error("BAD_ARGS", "phone is required", null) + return@setMethodCallHandler + } + try { + placeCall(phone) + result.success(true) + } catch (e: Exception) { + result.error("CALL_FAILED", e.message, null) + } + } + "startService" -> { + GatewayForegroundService.start(this) + result.success(true) + } + "stopService" -> { + GatewayForegroundService.stop(this) + result.success(true) + } + else -> result.notImplemented() + } + } + + EventChannel(messenger, EVENT_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(args: Any?, sink: EventChannel.EventSink?) { + incomingSink = sink + } + override fun onCancel(args: Any?) { + incomingSink = null + } + }) + + EventChannel(messenger, STATUS_CHANNEL).setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(args: Any?, sink: EventChannel.EventSink?) { + statusSink = sink + } + override fun onCancel(args: Any?) { + statusSink = null + } + }) + } + + private fun smsManagerFor(simSlot: Int?): SmsManager { + if (simSlot == null) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + getSystemService(SmsManager::class.java) + else + @Suppress("DEPRECATION") SmsManager.getDefault() + } + val hasPerm = ContextCompat.checkSelfPermission( + this, Manifest.permission.READ_PHONE_STATE + ) == PackageManager.PERMISSION_GRANTED + if (hasPerm) { + val sm = getSystemService(SubscriptionManager::class.java) + val info = sm?.getActiveSubscriptionInfoForSimSlotIndex(simSlot) + if (info != null) { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + getSystemService(SmsManager::class.java) + .createForSubscriptionId(info.subscriptionId) + else + @Suppress("DEPRECATION") + SmsManager.getSmsManagerForSubscriptionId(info.subscriptionId) + } + } + return smsManagerFor(null) + } + + /// Builds a PendingIntent that, when fired by the radio, broadcasts the + /// send/delivery outcome to SmsStatusReceiver (tagged with the message id). + private fun statusPendingIntent(messageId: String?, phone: String, kind: String): PendingIntent { + val intent = Intent(this, SmsStatusReceiver::class.java).apply { + action = STATUS_ACTION + putExtra("messageId", messageId) + putExtra("phone", phone) + putExtra("kind", kind) + } + val requestCode = (messageId.orEmpty() + phone + kind).hashCode() + return PendingIntent.getBroadcast( + this, requestCode, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + /// Places a phone call. Requires the CALL_PHONE permission (requested in Dart + /// via the phone permission group). + /// + /// Uses TelecomManager.placeCall, which routes through the system telecom + /// service rather than starting the dialer activity ourselves. This is what + /// lets the call go through when the screen is locked / the app is in the + /// background — a plain startActivity(ACTION_CALL) is blocked by Android's + /// background-activity-start restrictions in that state. Falls back to + /// ACTION_CALL only on very old devices without TelecomManager.placeCall. + private fun placeCall(phone: String) { + val uri = Uri.fromParts("tel", phone, null) + val hasPerm = ContextCompat.checkSelfPermission( + this, Manifest.permission.CALL_PHONE + ) == PackageManager.PERMISSION_GRANTED + if (!hasPerm) { + throw SecurityException("CALL_PHONE permission not granted") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val telecom = getSystemService(TelecomManager::class.java) + if (telecom != null) { + telecom.placeCall(uri, Bundle()) + return + } + } + val intent = Intent(Intent.ACTION_CALL, uri) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } + + private fun sendSms(phone: String, message: String, simSlot: Int?, messageId: String?) { + val sms = smsManagerFor(simSlot) + val parts = sms.divideMessage(message) + val sentPI = statusPendingIntent(messageId, phone, "sent") + val deliveredPI = statusPendingIntent(messageId, phone, "delivered") + if (parts.size > 1) { + val sentList = ArrayList(parts.size) + val deliveredList = ArrayList(parts.size) + for (i in parts.indices) { + sentList.add(sentPI) + deliveredList.add(deliveredPI) + } + sms.sendMultipartTextMessage(phone, null, parts, sentList, deliveredList) + } else { + sms.sendTextMessage(phone, null, message, sentPI, deliveredPI) + } + } +} diff --git a/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt new file mode 100644 index 0000000..c3df656 --- /dev/null +++ b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsReceiver.kt @@ -0,0 +1,40 @@ +package app.gsmnode.phone + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper +import android.provider.Telephony + +/// Receives incoming SMS and forwards each message to Dart via the EventChannel +/// sink held by MainActivity. Works while the app process is alive. +class SmsReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != Telephony.Sms.Intents.SMS_RECEIVED_ACTION) return + + val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) ?: return + if (messages.isEmpty()) return + + // Multipart SMS arrive as several PDUs from the same sender; concatenate. + val from = messages[0].displayOriginatingAddress ?: "" + val body = StringBuilder() + var timestamp = System.currentTimeMillis() + for (m in messages) { + body.append(m.displayMessageBody ?: "") + timestamp = m.timestampMillis + } + + val payload = mapOf( + "from" to from, + "body" to body.toString(), + "timestamp" to timestamp, + ) + + // EventSink must be touched on the main thread. + Handler(Looper.getMainLooper()).post { + MainActivity.incomingSink?.success(payload) + } + } +} diff --git a/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt new file mode 100644 index 0000000..0d87a2d --- /dev/null +++ b/Phone App/android_overlay/app/src/main/kotlin/app/gsmnode/phone/SmsStatusReceiver.kt @@ -0,0 +1,33 @@ +package app.gsmnode.phone + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper + +/// Receives SMS send/delivery outcomes from the PendingIntents created in +/// MainActivity.sendSms and forwards them to Dart, tagged with the message id +/// so the gateway loop can report Sent / Delivered / Failed to the API Server. +class SmsStatusReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val messageId = intent.getStringExtra("messageId") + val kind = intent.getStringExtra("kind") ?: return + // For "sent": RESULT_OK means the radio accepted the message. + // For "delivered": RESULT_OK means a positive delivery report arrived. + val success = resultCode == Activity.RESULT_OK + + val payload = mapOf( + "messageId" to messageId, + "kind" to kind, + "success" to success, + "resultCode" to resultCode, + ) + + Handler(Looper.getMainLooper()).post { + MainActivity.statusSink?.success(payload) + } + } +} diff --git a/Phone App/assets/icon/adaptive-foreground.png b/Phone App/assets/icon/adaptive-foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..2423b2c94a0560589f794a50be8148ebfa3f0aff GIT binary patch literal 16595 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 literal 0 HcmV?d00001 diff --git a/Phone App/assets/icon/app-icon.png b/Phone App/assets/icon/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..115a1c7a7199de0bdb1d892b9f332a9878665438 GIT binary patch literal 24992 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 NSX main() async { + WidgetsFlutterBinding.ensureInitialized(); + + storage = await Storage.create(); + storage.apiBase ??= AppConfig.defaultApiBase; + + apiClient = ApiClient(storage); + gateway = GatewayService(apiClient, SmsService()); + + runApp(const GsmNodeApp()); +} + +class GsmNodeApp extends StatelessWidget { + const GsmNodeApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'gsmnode', + debugShowCheckedModeBanner: false, + theme: gsmnodeLightTheme(), + darkTheme: gsmnodeDarkTheme(), + themeMode: ThemeMode.system, + home: storage.isRegistered ? const HomeScreen() : const LoginScreen(), + ); + } +} diff --git a/Phone App/lib/models/message.dart b/Phone App/lib/models/message.dart new file mode 100644 index 0000000..597e8c5 --- /dev/null +++ b/Phone App/lib/models/message.dart @@ -0,0 +1,33 @@ +/// An outbound message handed to the device by the API Server. +class GatewayMessage { + final String id; + final String type; // 'sms' or 'call' + final List phoneNumbers; + final String textMessage; + final int? simNumber; + final String status; + + GatewayMessage({ + required this.id, + required this.type, + required this.phoneNumbers, + required this.textMessage, + this.simNumber, + required this.status, + }); + + bool get isCall => type == 'call'; + + factory GatewayMessage.fromJson(Map json) { + return GatewayMessage( + id: json['id'] as String? ?? '', + type: json['type'] as String? ?? 'sms', + phoneNumbers: (json['phone_numbers'] as List? ?? []) + .map((e) => e.toString()) + .toList(), + textMessage: json['text_message'] as String? ?? '', + simNumber: json['sim_number'] as int?, + status: json['status'] as String? ?? '', + ); + } +} diff --git a/Phone App/lib/screens/home_screen.dart b/Phone App/lib/screens/home_screen.dart new file mode 100644 index 0000000..c6e446a --- /dev/null +++ b/Phone App/lib/screens/home_screen.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../main.dart'; +import '../theme.dart'; +import '../widgets/gsmnode_mark.dart'; +import 'login_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + bool _permsGranted = false; + + @override + void initState() { + super.initState(); + gateway.addListener(_onChange); + _checkPermissions(); + } + + @override + void dispose() { + gateway.removeListener(_onChange); + super.dispose(); + } + + void _onChange() => setState(() {}); + + Future _checkPermissions() async { + final sms = await Permission.sms.status; + final phone = await Permission.phone.status; + setState(() => _permsGranted = sms.isGranted && phone.isGranted); + } + + Future _requestPermissions() async { + // SMS + phone gate the gateway; notification lets the foreground service + // post its ongoing notification on Android 13+. + await [Permission.sms, Permission.phone, Permission.notification].request(); + await _checkPermissions(); + } + + void _toggle() { + if (gateway.running) { + gateway.stop(); + } else { + gateway.start(); + } + } + + Future _logout() async { + gateway.stop(); + await storage.clearSession(); + if (!mounted) return; + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + } + + @override + Widget build(BuildContext context) { + final running = gateway.running; + final cg = context.cg; + return Scaffold( + appBar: AppBar( + title: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + GsmNodeMark(size: 22), + SizedBox(width: 8), + GsmNodeWordmark(size: 17), + ], + ), + actions: [ + IconButton( + onPressed: _logout, + icon: const Icon(Icons.logout), + tooltip: 'Sign out', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _heroCard(running), + const SizedBox(height: 12), + if (!_permsGranted) ...[ + _permissionCard(), + const SizedBox(height: 12), + ], + _infoCard(), + const SizedBox(height: 16), + Text( + 'ACTIVITY', + style: gsmMono(size: 10, color: cg.textMuted, letterSpacing: 1.4), + ), + const SizedBox(height: 8), + Expanded(child: _activityLog()), + ], + ), + ), + ); + } + + /// Hero per the mobile UI kit: brand ring + mark when routing, ink when + /// paused. The primary action carries the brand glow. + Widget _heroCard(bool running) { + final cg = context.cg; + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: running ? cg.brandTint : cg.sunkenBg, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: running + ? GsmColors.green500.withValues(alpha: 0.22) + : cg.borderSubtle, + ), + ), + child: Column( + children: [ + Container( + width: 84, + height: 84, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: running ? GsmColors.green500 : cg.textMuted, + boxShadow: running + ? [ + BoxShadow( + color: GsmColors.green500.withValues(alpha: 0.28), + blurRadius: 18, + offset: const Offset(0, 6), + ), + ] + : null, + ), + child: const Center( + child: GsmNodeMark(size: 44, color: Colors.white), + ), + ), + const SizedBox(height: 14), + Text( + running ? 'Gateway active' : 'Gateway paused', + style: gsmDisplay(size: 20, color: cg.textPrimary), + ), + const SizedBox(height: 4), + Text( + running + ? 'routing sms · mms · calls' + : 'not routing — messages will queue', + style: gsmMono( + size: 12, + color: running ? GsmColors.green500 : cg.textMuted, + ), + ), + const SizedBox(height: 18), + FilledButton.icon( + onPressed: _permsGranted ? _toggle : null, + icon: Icon(running ? Icons.stop : Icons.play_arrow), + label: Text(running ? 'Stop gateway' : 'Start gateway'), + style: running + ? FilledButton.styleFrom( + backgroundColor: cg.danger, + minimumSize: const Size.fromHeight(48), + ) + : null, + ), + ], + ), + ); + } + + Widget _permissionCard() { + final cg = context.cg; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: cg.warningTint, + borderRadius: BorderRadius.circular(14), + ), + child: Row( + children: [ + Icon(Icons.warning_amber_rounded, color: cg.warning), + const SizedBox(width: 12), + const Expanded( + child: Text('SMS & phone permissions are required to send and ' + 'receive messages.'), + ), + TextButton( + onPressed: _requestPermissions, + child: const Text('Grant'), + ), + ], + ), + ); + } + + Widget _infoCard() { + final cg = context.cg; + return Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + children: [ + _infoRow('DEVICE', storage.deviceName ?? '—'), + Divider(height: 16, color: cg.borderSubtle), + _infoRow('ACCOUNT', storage.userEmail ?? '—'), + Divider(height: 16, color: cg.borderSubtle), + _infoRow('SERVER', storage.apiBase ?? '—'), + ], + ), + ), + ); + } + + Widget _infoRow(String label, String value) { + final cg = context.cg; + return Row( + children: [ + SizedBox( + width: 76, + child: Text( + label, + style: gsmMono(size: 9, color: cg.textMuted, letterSpacing: 1.3), + ), + ), + Expanded( + child: Text( + value, + style: gsmMono(size: 12, color: cg.textPrimary), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + ), + ), + ], + ); + } + + Widget _activityLog() { + final cg = context.cg; + final entries = gateway.log; + if (entries.isEmpty) { + return Center( + child: Text( + 'No activity yet.', + style: TextStyle(color: cg.textMuted), + ), + ); + } + return ListView.separated( + itemCount: entries.length, + separatorBuilder: (_, __) => const SizedBox(height: 6), + itemBuilder: (_, i) { + final e = entries[i]; + final t = e.time; + final hh = t.hour.toString().padLeft(2, '0'); + final mm = t.minute.toString().padLeft(2, '0'); + final ss = t.second.toString().padLeft(2, '0'); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: cg.card, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: cg.borderSubtle), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e.error ? cg.danger : cg.success, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text(e.text, style: const TextStyle(fontSize: 13)), + ), + const SizedBox(width: 10), + Text( + '$hh:$mm:$ss', + style: gsmMono(size: 10, color: cg.textMuted), + ), + ], + ), + ); + }, + ); + } +} diff --git a/Phone App/lib/screens/login_screen.dart b/Phone App/lib/screens/login_screen.dart new file mode 100644 index 0000000..b77eade --- /dev/null +++ b/Phone App/lib/screens/login_screen.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; + +import '../main.dart'; +import '../services/api_client.dart'; +import '../theme.dart'; +import '../widgets/gsmnode_mark.dart'; +import 'home_screen.dart'; + +/// Login + device registration. The user authenticates against the API Server, +/// then this phone is registered as a gateway device. +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _apiBase = TextEditingController(text: storage.apiBase ?? ''); + final _email = TextEditingController(text: storage.userEmail ?? ''); + final _password = TextEditingController(); + final _deviceName = TextEditingController(text: storage.deviceName ?? 'My Phone'); + + bool _busy = false; + String? _error; + + @override + void dispose() { + _apiBase.dispose(); + _email.dispose(); + _password.dispose(); + _deviceName.dispose(); + super.dispose(); + } + + Future _submit() async { + setState(() { + _busy = true; + _error = null; + }); + try { + storage.apiBase = _apiBase.text.trim(); + await apiClient.login(_email.text.trim(), _password.text); + + final deviceId = storage.deviceId ?? + 'android-${DateTime.now().millisecondsSinceEpoch}'; + await apiClient.registerDevice( + deviceId: deviceId, + name: _deviceName.text.trim().isEmpty ? 'My Phone' : _deviceName.text.trim(), + ); + + if (!mounted) return; + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const HomeScreen()), + ); + } on ApiException catch (e) { + setState(() => _error = + e.status == 401 ? 'Invalid email or password.' : e.message); + } catch (e) { + setState(() => _error = 'Could not connect: $e'); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + @override + Widget build(BuildContext context) { + final cg = context.cg; + return Scaffold( + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Center(child: GsmNodeMark(size: 56)), + const SizedBox(height: 14), + const Center(child: GsmNodeWordmark(size: 26)), + const SizedBox(height: 8), + Center( + child: Text( + 'CONNECT THIS PHONE TO YOUR GATEWAY', + textAlign: TextAlign.center, + style: gsmMono( + size: 10, + color: cg.textMuted, + letterSpacing: 1.4, + ), + ), + ), + const SizedBox(height: 28), + TextField( + controller: _apiBase, + decoration: const InputDecoration( + labelText: 'API Server URL', + hintText: 'http://10.0.2.2:8080', + ), + style: gsmMono(size: 14, color: cg.textPrimary), + keyboardType: TextInputType.url, + ), + const SizedBox(height: 12), + TextField( + controller: _email, + decoration: const InputDecoration(labelText: 'Email'), + keyboardType: TextInputType.emailAddress, + ), + const SizedBox(height: 12), + TextField( + controller: _password, + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + ), + const SizedBox(height: 12), + TextField( + controller: _deviceName, + decoration: const InputDecoration(labelText: 'Device name'), + ), + const SizedBox(height: 16), + if (_error != null) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: cg.dangerTint, + borderRadius: BorderRadius.circular(10), + ), + child: Text(_error!, style: TextStyle(color: cg.danger)), + ), + const SizedBox(height: 12), + FilledButton( + onPressed: _busy ? null : _submit, + child: _busy + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text('Sign in & register device'), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/Phone App/lib/services/api_client.dart b/Phone App/lib/services/api_client.dart new file mode 100644 index 0000000..0228fc0 --- /dev/null +++ b/Phone App/lib/services/api_client.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +import '../models/message.dart'; +import 'storage.dart'; + +class ApiException implements Exception { + final int status; + final String message; + ApiException(this.status, this.message); + @override + String toString() => 'ApiException($status): $message'; +} + +/// Client for the API Server. Uses the user JWT for login/registration and the +/// opaque device token for the mobile gateway endpoints. +class ApiClient { + final Storage storage; + final http.Client _http; + + ApiClient(this.storage, {http.Client? client}) + : _http = client ?? http.Client(); + + String get _base => (storage.apiBase ?? '').replaceAll(RegExp(r'/$'), ''); + + Uri _uri(String path) => Uri.parse('$_base$path'); + + Map _headers(String? token) => { + 'Content-Type': 'application/json', + if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token', + }; + + dynamic _decode(http.Response res) { + final body = res.body.isNotEmpty ? jsonDecode(res.body) : null; + if (res.statusCode < 200 || res.statusCode >= 300) { + final msg = (body is Map && body['error'] != null) + ? body['error'].toString() + : 'HTTP ${res.statusCode}'; + throw ApiException(res.statusCode, msg); + } + return body; + } + + /// Authenticates a user and stores the JWT. Returns the user email. + Future login(String email, String password) async { + final res = await _http.post( + _uri('/api/auth/login'), + headers: _headers(null), + body: jsonEncode({'email': email, 'password': password}), + ); + final data = _decode(res) as Map; + storage.jwt = data['access_token'] as String?; + final user = data['user'] as Map?; + storage.userEmail = user?['email'] as String?; + return storage.userEmail ?? email; + } + + /// Registers this device and stores the returned device token. + Future registerDevice({ + required String deviceId, + required String name, + String platform = 'android', + String appVersion = '1.0.0', + String? pushToken, + }) async { + final res = await _http.post( + _uri('/api/mobile/v1/device'), + headers: _headers(storage.jwt), + body: jsonEncode({ + 'device_id': deviceId, + 'name': name, + 'platform': platform, + 'app_version': appVersion, + if (pushToken != null) 'push_token': pushToken, + }), + ); + final data = _decode(res) as Map; + storage.deviceId = deviceId; + storage.deviceName = name; + storage.deviceToken = data['auth_token'] as String?; + } + + /// Pulls pending messages for this device (the server marks them Processed). + Future> pullMessages() async { + final res = await _http.get( + _uri('/api/mobile/v1/messages'), + headers: _headers(storage.deviceToken), + ); + final data = _decode(res) as Map; + final items = (data['items'] as List? ?? []); + return items + .map((e) => GatewayMessage.fromJson(e as Map)) + .toList(); + } + + /// Reports a message's delivery state back to the server. + Future reportMessage(String id, String status, {String? error}) async { + final res = await _http.patch( + _uri('/api/mobile/v1/messages/$id'), + headers: _headers(storage.deviceToken), + body: jsonEncode({'status': status, if (error != null) 'error': error}), + ); + _decode(res); + } + + /// Posts an incoming SMS to the server's inbox. + Future postInbox(String phoneNumber, String message, + {DateTime? receivedAt}) async { + final res = await _http.post( + _uri('/api/mobile/v1/inbox'), + headers: _headers(storage.deviceToken), + body: jsonEncode({ + 'phone_number': phoneNumber, + 'message': message, + if (receivedAt != null) + 'received_at': receivedAt.toUtc().toIso8601String(), + }), + ); + _decode(res); + } + + /// Sends a heartbeat ping. + Future ping() async { + final res = await _http.post( + _uri('/api/mobile/v1/ping'), + headers: _headers(storage.deviceToken), + ); + _decode(res); + } +} diff --git a/Phone App/lib/services/gateway_service.dart b/Phone App/lib/services/gateway_service.dart new file mode 100644 index 0000000..61a77ce --- /dev/null +++ b/Phone App/lib/services/gateway_service.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../config.dart'; +import 'api_client.dart'; +import 'sms_service.dart'; + +/// A single line in the on-screen activity log. +class LogEntry { + final DateTime time; + final String text; + final bool error; + LogEntry(this.text, {this.error = false}) : time = DateTime.now(); +} + +/// Orchestrates the gateway loop: polls the API Server for pending messages, +/// sends them over the radio, reports delivery state, forwards incoming SMS to +/// the inbox, and sends heartbeats. +/// +/// This runs in the foreground while the app is open. Moving the loop to a +/// persistent background/foreground service (WorkManager) is a documented next +/// step — see README. +class GatewayService extends ChangeNotifier { + final ApiClient api; + final SmsService sms; + + GatewayService(this.api, this.sms); + + bool _running = false; + bool get running => _running; + + final List _log = []; + List get log => List.unmodifiable(_log); + + Timer? _pollTimer; + Timer? _pingTimer; + StreamSubscription? _incomingSub; + StreamSubscription? _statusSub; + bool _polling = false; + + void start() { + if (_running) return; + _running = true; + _log0('Gateway started'); + + // Keep the process alive while the screen is off / app backgrounded. + sms.startBackgroundService().catchError( + (e) => _log0('Foreground service failed to start: $e', error: true)); + + _incomingSub = sms.incomingSms().listen(_onIncoming, onError: (e) { + _log0('Incoming SMS stream error: $e', error: true); + }); + _statusSub = sms.smsStatus().listen(_onStatus, onError: (e) { + _log0('Status stream error: $e', error: true); + }); + + _pollTimer = Timer.periodic(AppConfig.pollInterval, (_) => _poll()); + _pingTimer = Timer.periodic(AppConfig.pingInterval, (_) => _ping()); + _poll(); + _ping(); + notifyListeners(); + } + + void stop() { + _running = false; + _pollTimer?.cancel(); + _pingTimer?.cancel(); + _incomingSub?.cancel(); + _statusSub?.cancel(); + sms.stopBackgroundService().catchError((_) {}); + _log0('Gateway stopped'); + notifyListeners(); + } + + Future _poll() async { + if (_polling || !_running) return; + _polling = true; + try { + final messages = await api.pullMessages(); + for (final m in messages) { + var handedOff = true; + for (final phone in m.phoneNumbers) { + try { + if (m.isCall) { + await sms.placeCall(phone); + _log0('Calling $phone'); + } else { + await sms.sendSms(phone, m.textMessage, + simSlot: m.simNumber, messageId: m.id); + _log0('Sending to $phone: "${_short(m.textMessage)}"'); + } + } catch (e) { + handedOff = false; + await api.reportMessage(m.id, 'Failed', error: e.toString()); + _log0('${m.isCall ? "Call" : "Send"} to $phone failed: $e', + error: true); + break; + } + } + // Report Sent once handed off. For SMS the native send/delivery + // PendingIntents then refine this to Delivered (or Failed) via _onStatus; + // a call has no delivery report, so it stays Sent. + if (handedOff) await api.reportMessage(m.id, 'Sent'); + } + } catch (e) { + _log0('Poll error: $e', error: true); + } finally { + _polling = false; + } + } + + Future _ping() async { + if (!_running) return; + try { + await api.ping(); + } catch (e) { + _log0('Ping failed: $e', error: true); + } + } + + Future _onStatus(SmsStatus s) async { + final id = s.messageId; + if (id == null || id.isEmpty) return; + try { + if (s.kind == 'delivered') { + if (s.success) { + await api.reportMessage(id, 'Delivered'); + _log0('Delivered: $id'); + } else { + await api.reportMessage(id, 'Failed', error: 'delivery failed'); + _log0('Delivery failed: $id', error: true); + } + } else if (s.kind == 'sent' && !s.success) { + await api.reportMessage(id, 'Failed', error: 'radio rejected message'); + _log0('Send rejected by radio: $id', error: true); + } + } catch (e) { + _log0('Report status failed: $e', error: true); + } + } + + Future _onIncoming(IncomingSms msg) async { + _log0('Received from ${msg.from}: "${_short(msg.body)}"'); + try { + await api.postInbox(msg.from, msg.body, receivedAt: msg.timestamp); + } catch (e) { + _log0('Forward inbox failed: $e', error: true); + } + } + + void _log0(String text, {bool error = false}) { + _log.insert(0, LogEntry(text, error: error)); + if (_log.length > 100) _log.removeLast(); + notifyListeners(); + } + + String _short(String s) => s.length > 40 ? '${s.substring(0, 40)}…' : s; + + @override + void dispose() { + stop(); + super.dispose(); + } +} diff --git a/Phone App/lib/services/sms_service.dart b/Phone App/lib/services/sms_service.dart new file mode 100644 index 0000000..642ec41 --- /dev/null +++ b/Phone App/lib/services/sms_service.dart @@ -0,0 +1,76 @@ +import 'package:flutter/services.dart'; + +/// An incoming SMS delivered from the native side. +class IncomingSms { + final String from; + final String body; + final DateTime timestamp; + IncomingSms(this.from, this.body, this.timestamp); +} + +/// A send/delivery status report for an outbound message. +class SmsStatus { + final String? messageId; + final String kind; // 'sent' or 'delivered' + final bool success; + SmsStatus(this.messageId, this.kind, this.success); +} + +/// Bridges to the native Android side: SmsManager (sending), a BroadcastReceiver +/// (incoming), send/delivery PendingIntents (status), and the foreground service +/// that keeps the gateway alive. See android_overlay/ for the Kotlin side. +class SmsService { + static const _method = MethodChannel('app.gsmnode/sms'); + static const _events = EventChannel('app.gsmnode/sms_incoming'); + static const _status = EventChannel('app.gsmnode/sms_status'); + + /// Sends an SMS via the device radio. [simSlot] is 0-based; null = default SIM. + /// [messageId] is echoed back on the status stream for correlation. + /// Throws [PlatformException] on failure. + Future sendSms(String phoneNumber, String message, + {int? simSlot, String? messageId}) async { + await _method.invokeMethod('sendSms', { + 'phone': phoneNumber, + 'message': message, + 'simSlot': simSlot, + 'messageId': messageId, + }); + } + + /// Places a phone call to [phoneNumber] via the native dialer (ACTION_CALL). + /// Requires the CALL_PHONE permission. Throws [PlatformException] on failure. + Future placeCall(String phoneNumber) async { + await _method.invokeMethod('placeCall', {'phone': phoneNumber}); + } + + /// Starts the foreground service so the gateway loop survives screen-off. + Future startBackgroundService() => _method.invokeMethod('startService'); + + /// Stops the foreground service. + Future stopBackgroundService() => _method.invokeMethod('stopService'); + + /// Stream of incoming SMS messages (active while the app process is alive). + Stream incomingSms() { + return _events.receiveBroadcastStream().map((event) { + final map = Map.from(event as Map); + final ts = map['timestamp'] as int?; + return IncomingSms( + map['from'] as String? ?? '', + map['body'] as String? ?? '', + ts != null ? DateTime.fromMillisecondsSinceEpoch(ts) : DateTime.now(), + ); + }); + } + + /// Stream of send/delivery status reports for outbound messages. + Stream smsStatus() { + return _status.receiveBroadcastStream().map((event) { + final map = Map.from(event as Map); + return SmsStatus( + map['messageId'] as String?, + map['kind'] as String? ?? '', + map['success'] == true, + ); + }); + } +} diff --git a/Phone App/lib/services/storage.dart b/Phone App/lib/services/storage.dart new file mode 100644 index 0000000..be68d61 --- /dev/null +++ b/Phone App/lib/services/storage.dart @@ -0,0 +1,54 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Persists connection settings and credentials across app launches. +class Storage { + static const _kApiBase = 'api_base'; + static const _kJwt = 'jwt'; + static const _kDeviceToken = 'device_token'; + static const _kDeviceId = 'device_id'; + static const _kDeviceName = 'device_name'; + static const _kUserEmail = 'user_email'; + + final SharedPreferences _prefs; + Storage(this._prefs); + + static Future create() async { + return Storage(await SharedPreferences.getInstance()); + } + + String? get apiBase => _prefs.getString(_kApiBase); + set apiBase(String? v) => _set(_kApiBase, v); + + String? get jwt => _prefs.getString(_kJwt); + set jwt(String? v) => _set(_kJwt, v); + + String? get deviceToken => _prefs.getString(_kDeviceToken); + set deviceToken(String? v) => _set(_kDeviceToken, v); + + String? get deviceId => _prefs.getString(_kDeviceId); + set deviceId(String? v) => _set(_kDeviceId, v); + + String? get deviceName => _prefs.getString(_kDeviceName); + set deviceName(String? v) => _set(_kDeviceName, v); + + String? get userEmail => _prefs.getString(_kUserEmail); + set userEmail(String? v) => _set(_kUserEmail, v); + + bool get isRegistered => (deviceToken ?? '').isNotEmpty; + + Future clearSession() async { + await _prefs.remove(_kJwt); + await _prefs.remove(_kDeviceToken); + await _prefs.remove(_kUserEmail); + // Keep device_id stable across logout so re-registering updates the same + // device record instead of creating a new (phantom) one. + } + + void _set(String key, String? v) { + if (v == null) { + _prefs.remove(key); + } else { + _prefs.setString(key, v); + } + } +} diff --git a/Phone App/lib/theme.dart b/Phone App/lib/theme.dart new file mode 100644 index 0000000..b875d4d --- /dev/null +++ b/Phone App/lib/theme.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +/// gsmnode design tokens (see Design/SMS Gateway logo design/tokens in the +/// project root). Brand is signal-green #2E9E6B on an ink/paper cool scale. +abstract class GsmColors { + // Signal green (brand) ramp + static const green100 = Color(0xFFE4F4EC); + static const green200 = Color(0xFFBEE6D0); + static const green300 = Color(0xFF6BC79A); // lifted text on dark + static const green500 = Color(0xFF2E9E6B); // primary + static const green600 = Color(0xFF278A5C); // hover + static const green700 = Color(0xFF1F6E49); // active + static const greenOnDark = Color(0xFF4FB985); // accent on ink surfaces + + // Ink & paper + static const ink = Color(0xFF12161C); // primary text / dark surface + static const ink900 = Color(0xFF0A0D11); // deepest surface + static const ink800 = Color(0xFF1A1F27); + static const paper = Color(0xFFFAFAF9); // page bg + + // Cool gray ramp + static const gray50 = Color(0xFFF4F5F4); + static const gray100 = Color(0xFFE9EBEA); + static const gray200 = Color(0xFFDCDFDE); // subtle border + static const gray300 = Color(0xFFC2C7C6); // strong border + static const gray400 = Color(0xFF9AA0A2); // muted text + static const gray500 = Color(0xFF6B7278); + static const gray600 = Color(0xFF4A5157); + static const gray700 = Color(0xFF333A40); // secondary text + + // Semantic (light) + static const warning = Color(0xFFC68A2E); + static const warningTint = Color(0xFFF6EAD6); + static const danger = Color(0xFFC64A3E); + static const dangerTint = Color(0xFFF6E1DC); + + // Dark surfaces + static const dBase = Color(0xFF0A0D11); + static const dSunken = Color(0xFF0E1216); + static const dCard = Color(0xFF14191F); + static const dRaised = Color(0xFF1A2027); + static const dBorderSubtle = Color(0xFF262B33); + static const dBorderStrong = Color(0xFF363C46); + static const dTextPrimary = Color(0xFFF5F6F4); + static const dTextSecondary = Color(0xFFC2C7C6); + static const dTextMuted = Color(0xFF9AA0A2); + + // Semantic (dark — lifted for contrast) + static const dWarning = Color(0xFFE0A84E); + static const dDanger = Color(0xFFE0655B); +} + +/// Semantic role colors that flip between light and dark, exposed as a +/// ThemeExtension so screens theme through roles, not raw ramp values. +class GsmSemantic extends ThemeExtension { + const GsmSemantic({ + required this.pageBg, + required this.sunkenBg, + required this.card, + required this.borderSubtle, + required this.borderStrong, + required this.textPrimary, + required this.textSecondary, + required this.textMuted, + required this.brandTint, + required this.success, + required this.successTint, + required this.warning, + required this.warningTint, + required this.danger, + required this.dangerTint, + }); + + final Color pageBg; + final Color sunkenBg; + final Color card; + final Color borderSubtle; + final Color borderStrong; + final Color textPrimary; + final Color textSecondary; + final Color textMuted; + final Color brandTint; + final Color success; + final Color successTint; + final Color warning; + final Color warningTint; + final Color danger; + final Color dangerTint; + + static const light = GsmSemantic( + pageBg: GsmColors.paper, + sunkenBg: GsmColors.gray50, + card: Colors.white, + borderSubtle: GsmColors.gray200, + borderStrong: GsmColors.gray300, + textPrimary: GsmColors.ink, + textSecondary: GsmColors.gray700, + textMuted: GsmColors.gray500, + brandTint: GsmColors.green100, + success: GsmColors.green500, + successTint: GsmColors.green100, + warning: GsmColors.warning, + warningTint: GsmColors.warningTint, + danger: GsmColors.danger, + dangerTint: GsmColors.dangerTint, + ); + + static const dark = GsmSemantic( + pageBg: GsmColors.dBase, + sunkenBg: GsmColors.dSunken, + card: GsmColors.dCard, + borderSubtle: GsmColors.dBorderSubtle, + borderStrong: GsmColors.dBorderStrong, + textPrimary: GsmColors.dTextPrimary, + textSecondary: GsmColors.dTextSecondary, + textMuted: GsmColors.dTextMuted, + brandTint: Color(0x292E9E6B), // green-500 @ 16% + success: GsmColors.green300, + successTint: Color(0x2E2E9E6B), + warning: GsmColors.dWarning, + warningTint: Color(0x2EC68A2E), + danger: GsmColors.dDanger, + dangerTint: Color(0x2EC64A3E), + ); + + @override + GsmSemantic copyWith({Color? pageBg}) => this; + + @override + GsmSemantic lerp(ThemeExtension? other, double t) => this; +} + +extension GsmThemeX on BuildContext { + GsmSemantic get cg => Theme.of(this).extension()!; +} + +/// A mono (JetBrains Mono) style for IDs, numbers, timestamps and eyebrows. +TextStyle gsmMono({ + double size = 12, + Color? color, + FontWeight weight = FontWeight.w500, + double letterSpacing = 0, +}) => + GoogleFonts.jetBrainsMono( + fontSize: size, + color: color, + fontWeight: weight, + letterSpacing: letterSpacing, + ); + +/// Display (Space Grotesk) style for titles & metrics. +TextStyle gsmDisplay({ + double size = 20, + Color? color, + FontWeight weight = FontWeight.w700, +}) => + GoogleFonts.spaceGrotesk( + fontSize: size, + color: color, + fontWeight: weight, + letterSpacing: -0.02 * size, + ); + +ThemeData _base(Brightness brightness, GsmSemantic s) { + final isDark = brightness == Brightness.dark; + final scheme = ColorScheme( + brightness: brightness, + primary: GsmColors.green500, + onPrimary: Colors.white, + secondary: isDark ? GsmColors.greenOnDark : GsmColors.green600, + onSecondary: Colors.white, + error: s.danger, + onError: Colors.white, + surface: s.card, + onSurface: s.textPrimary, + outline: s.borderStrong, + outlineVariant: s.borderSubtle, + surfaceContainerLowest: s.sunkenBg, + surfaceContainerLow: s.pageBg, + surfaceContainer: s.card, + ); + + // Body & UI in IBM Plex Sans; titles/metrics reach for Space Grotesk. + final textTheme = GoogleFonts.ibmPlexSansTextTheme( + (isDark ? ThemeData.dark() : ThemeData.light()).textTheme, + ).apply(bodyColor: s.textPrimary, displayColor: s.textPrimary); + + const controlRadius = BorderRadius.all(Radius.circular(8)); + const cardRadius = BorderRadius.all(Radius.circular(18)); + + return ThemeData( + useMaterial3: true, + colorScheme: scheme, + scaffoldBackgroundColor: s.pageBg, + textTheme: textTheme, + extensions: [s], + appBarTheme: AppBarTheme( + backgroundColor: s.card, + foregroundColor: s.textPrimary, + elevation: 0, + scrolledUnderElevation: 0, + shape: Border(bottom: BorderSide(color: s.borderSubtle)), + titleTextStyle: GoogleFonts.spaceGrotesk( + fontSize: 17, + fontWeight: FontWeight.w700, + color: s.textPrimary, + letterSpacing: -0.2, + ), + ), + cardTheme: CardThemeData( + color: s.card, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: cardRadius, + side: BorderSide(color: s.borderSubtle), + ), + margin: EdgeInsets.zero, + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + backgroundColor: GsmColors.green500, + foregroundColor: Colors.white, + elevation: 0, + minimumSize: const Size.fromHeight(48), + shape: const RoundedRectangleBorder(borderRadius: controlRadius), + textStyle: GoogleFonts.ibmPlexSans( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ).copyWith( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return GsmColors.green500.withValues(alpha: 0.45); + } + if (states.contains(WidgetState.pressed)) return GsmColors.green700; + if (states.contains(WidgetState.hovered)) return GsmColors.green600; + return GsmColors.green500; + }), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: s.textPrimary, + side: BorderSide(color: s.borderStrong), + shape: const RoundedRectangleBorder(borderRadius: controlRadius), + textStyle: GoogleFonts.ibmPlexSans( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: isDark ? GsmColors.greenOnDark : GsmColors.green600, + textStyle: GoogleFonts.ibmPlexSans( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: s.card, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + border: OutlineInputBorder( + borderRadius: controlRadius, + borderSide: BorderSide(color: s.borderStrong), + ), + enabledBorder: OutlineInputBorder( + borderRadius: controlRadius, + borderSide: BorderSide(color: s.borderStrong), + ), + focusedBorder: const OutlineInputBorder( + borderRadius: controlRadius, + borderSide: BorderSide(color: GsmColors.green500, width: 2), + ), + labelStyle: TextStyle(color: s.textSecondary), + hintStyle: TextStyle(color: s.textMuted), + ), + dividerTheme: DividerThemeData(color: s.borderSubtle, thickness: 1), + snackBarTheme: SnackBarThemeData( + backgroundColor: isDark ? GsmColors.dRaised : GsmColors.ink, + contentTextStyle: GoogleFonts.ibmPlexSans(color: Colors.white), + shape: const RoundedRectangleBorder(borderRadius: controlRadius), + behavior: SnackBarBehavior.floating, + ), + ); +} + +ThemeData gsmnodeLightTheme() => _base(Brightness.light, GsmSemantic.light); +ThemeData gsmnodeDarkTheme() => _base(Brightness.dark, GsmSemantic.dark); diff --git a/Phone App/lib/widgets/gsmnode_mark.dart b/Phone App/lib/widgets/gsmnode_mark.dart new file mode 100644 index 0000000..5e852d2 --- /dev/null +++ b/Phone App/lib/widgets/gsmnode_mark.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +import '../theme.dart'; + +/// The gsmnode routing mark — two stacked arrows: the top (ink) routes right, +/// the bottom (green) routes left. Geometry matches assets/svg/mark-color.svg +/// (2.6px stroke on a 40px grid, rounded terminals). +/// +/// When [color] is given the whole mark is drawn in that single color +/// (e.g. white on a filled tile); otherwise the top arrow takes the surface +/// ink color and the bottom arrow the signal green. +class GsmNodeMark extends StatelessWidget { + const GsmNodeMark({super.key, this.size = 32, this.color}); + + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + final ink = color ?? Theme.of(context).colorScheme.onSurface; + final green = color ?? GsmColors.green500; + return CustomPaint( + size: Size.square(size), + painter: _MarkPainter(ink: ink, green: green), + ); + } +} + +class _MarkPainter extends CustomPainter { + _MarkPainter({required this.ink, required this.green}); + + final Color ink; + final Color green; + + @override + void paint(Canvas canvas, Size size) { + final k = size.width / 40; // scale from the 40px design grid + Paint stroke(Color c) => Paint() + ..color = c + ..style = PaintingStyle.stroke + ..strokeWidth = 2.6 * k + ..strokeCap = StrokeCap.round + ..strokeJoin = StrokeJoin.round; + + final inkPaint = stroke(ink); + final greenPaint = stroke(green); + + // Top arrow — routes right (ink). + canvas.drawLine(Offset(7 * k, 15 * k), Offset(31 * k, 15 * k), inkPaint); + canvas.drawPath( + Path() + ..moveTo(26 * k, 10 * k) + ..lineTo(32 * k, 15 * k) + ..lineTo(26 * k, 20 * k), + inkPaint, + ); + + // Bottom arrow — routes left (green). + canvas.drawLine(Offset(9 * k, 25 * k), Offset(33 * k, 25 * k), greenPaint); + canvas.drawPath( + Path() + ..moveTo(14 * k, 20 * k) + ..lineTo(8 * k, 25 * k) + ..lineTo(14 * k, 30 * k), + greenPaint, + ); + } + + @override + bool shouldRepaint(_MarkPainter old) => old.ink != ink || old.green != green; +} + +/// The gsmnode wordmark: lowercase `gsm` in ink + `node` in green, monospace. +/// On dark surfaces `node` lifts to the on-dark green. Pass [color] to render +/// the whole wordmark in a single color. +class GsmNodeWordmark extends StatelessWidget { + const GsmNodeWordmark({super.key, this.size = 24, this.color}); + + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final ink = color ?? Theme.of(context).colorScheme.onSurface; + final green = + color ?? (isDark ? GsmColors.greenOnDark : GsmColors.green500); + return Text.rich( + TextSpan(children: [ + TextSpan(text: 'gsm', style: TextStyle(color: ink)), + TextSpan(text: 'node', style: TextStyle(color: green)), + ]), + style: GoogleFonts.jetBrainsMono( + fontSize: size, + fontWeight: FontWeight.w600, + letterSpacing: -0.01 * size, + height: 1, + ), + ); + } +} diff --git a/Phone App/pubspec.yaml b/Phone App/pubspec.yaml new file mode 100644 index 0000000..d4cffee --- /dev/null +++ b/Phone App/pubspec.yaml @@ -0,0 +1,31 @@ +name: sms_gateway_phone +description: gsmnode phone client — turns an Android phone into an SMS gateway controlled via the API Server. +publish_to: "none" +version: 1.0.0+1 + +environment: + sdk: ">=3.4.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + http: ^1.2.2 + shared_preferences: ^2.3.2 + permission_handler: ^11.3.1 + google_fonts: ^8.1.0 + +dev_dependencies: + flutter_lints: ^4.0.0 + flutter_launcher_icons: ^0.14.1 + +flutter: + uses-material-design: true + +# App launcher icons — generated from the gsmnode design-system app icon. +# Regenerate with: dart run flutter_launcher_icons +flutter_launcher_icons: + android: true + image_path: "assets/icon/app-icon.png" + min_sdk_android: 21 + adaptive_icon_background: "#2E9E6B" + adaptive_icon_foreground: "assets/icon/adaptive-foreground.png" diff --git a/README.md b/README.md new file mode 100644 index 0000000..326b179 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# gsmnode + +Turn Android phones into a programmable SMS gateway, controlled through a web UI +and a REST API — inspired by [android-sms-gateway](https://docs.sms-gate.app/). + +Brand + design system live in +[`Design/SMS Gateway logo design/`](Design/SMS%20Gateway%20logo%20design/) — +signal-green `#2E9E6B` on ink, Space Grotesk (display) · IBM Plex Sans (body) · +JetBrains Mono (code/labels), the lowercase `gsm`+`node` wordmark, and the +two-arrow routing mark. All three UI surfaces implement it (the Web App and API +panel share a persisted light/dark toggle, `localStorage` key `gsmnode-theme`, +`data-gsm-theme` attribute; the Phone App follows the system theme). + +Three application surfaces sit in front of a shared PocketBase. **The API Server +is the only component that talks to PocketBase**; the Web App and Phone App talk +only to the API Server. + +``` +┌────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Web App │───────►│ │───────►│ │ +│ (Vue/Go) │ │ API Server │ │ PocketBase │ +├────────────┤ │ (Go) │ │ 10.2.1.10: │ +│ Phone App │───────►│ │───────►│ 8028 │ +│ (Flutter) │ └──────────────────┘ └──────────────┘ +└────────────┘ +``` + +## Surfaces + +| Folder | Stack | Port | Status | +|---|---|---|---| +| [`API Server/`](API%20Server/) | Go | `:8080` | ✅ Built & verified (live E2E) | +| [`Web App/`](Web%20App/) | Go BFF + Vue 3 + Tailwind | `:8090` | ✅ Built & verified | +| [`Phone App/`](Phone%20App/) | Flutter (Android) | — | ✅ Built & run on a real device; foreground service + delivery reports | +| [`Home Assistant Plugin/`](Home%20Assistant%20Plugin/) | HA custom component (Python) | — | ✅ `notify.gsmnode` service; flow validated | + +## PocketBase collections + +Managed by `API Server/scripts/setup-pocketbase.mjs` (idempotent): + +- `users` — auth (existing default collection) +- `devices` — `device_id, name, platform, app_version, push_token, auth_token, status, last_seen_at, owner` +- `messages` — `phone_numbers, text_message, sim_number, status, error, schedule_at, sent_at, delivered_at, device, owner` +- `inbox` — `phone_number, message, received_at, device, owner` +- `webhooks` — `event, url, device, owner` + +Collections are locked to superuser access; the API Server enforces per-user +ownership in application logic. + +## Run order + +1. **PocketBase** — already running at `http://10.2.1.10:8028`. +2. **API Server** (`:8080`): + ```powershell + cd "API Server" + Copy-Item .env.example .env # fill in PB_ADMIN_* and JWT_SECRET + node scripts/setup-pocketbase.mjs # one-time schema setup + node scripts/create-user.mjs you@example.com "password" "Your Name" + ./scripts/Run-ApiServer.ps1 + ``` +3. **Web App** (`:8090`): + ```powershell + cd "Web App"; ./server/Run-WebApp.ps1 + ``` + Open http://localhost:8090 and sign in. +4. **Phone App** — see [`Phone App/README.md`](Phone%20App/README.md) (install + Flutter + JDK 17, `flutter create`, copy `android_overlay/`, `flutter run`). + +## Message lifecycle + +`Pending` → (device pulls) `Processed` → `Sent` → `Delivered` · or `Failed`. + +## Webhooks + +Events `sms:received`, `sms:sent`, `sms:delivered`, `sms:failed` are POSTed to +registered URLs as `{event, device_id, payload, created_at}`. + +## Per-surface docs + +- [API Server README](API%20Server/README.md) — full endpoint reference, setup +- [Web App README](Web%20App/README.md) — dev/build, pages +- [Phone App README](Phone%20App/README.md) — Flutter build + native SMS wiring diff --git a/Web App/README.md b/Web App/README.md new file mode 100644 index 0000000..1f8ca49 --- /dev/null +++ b/Web App/README.md @@ -0,0 +1,82 @@ +# gsmnode — Web App + +Browser console for the gateway: a **Vue 3 + Tailwind** SPA served by a small **Go +backend-for-frontend (BFF)**. Styled with the gsmnode design system +(`../Design/SMS Gateway logo design/`): signal-green `#2E9E6B` on ink, Space +Grotesk (display) · IBM Plex Sans (body) · JetBrains Mono (code/labels), Lucide +icons, and a persisted light/dark toggle (`localStorage` key `gsmnode-theme`, +`data-gsm-theme` attribute). The BFF serves the built SPA and reverse-proxies +`/api/*` to the API Server, so the browser is always same-origin and all data +access still flows through the API Server. + +``` +Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─► PocketBase + └── serves embedded Vue SPA +``` + +## Layout + +``` +server/ Go BFF: embeds web/dist, proxies /api -> API_BASE + main.go + .env.example + dist/ built SPA (generated; embedded at compile time) +web/ Vue 3 + Vite + Tailwind v4 source + src/ + api.js fetch wrapper (bearer token, same-origin /api) + store/auth.js login state + router.js routes + auth guard + views/ Login, Devices, Send, Messages, Inbox, Webhooks +``` + +## Requirements + +- Node 18+ and Go 1.26+ +- A running **API Server** (see `../API Server`) + +## Develop + +Two terminals: + +```powershell +# terminal 1 — API Server (see ../API Server/README.md) +cd "../API Server"; ./scripts/Run-ApiServer.ps1 + +# terminal 2 — Vite dev server with hot reload (proxies /api -> :8080) +cd web; npm install; npm run dev # http://localhost:5173 +``` + +## Build & run (production-style) + +```powershell +./server/Run-WebApp.ps1 # builds frontend, then serves on :8090 +# or manually: +cd web; npm run build # outputs to ../server/dist +cd ../server; go run . # http://localhost:8090 +``` + +Config (`server/.env`, copy from `.env.example`): + +| Variable | Purpose | Default | +|---|---|---| +| `WEB_ADDR` | Listen address | `:8090` | +| `API_BASE` | API Server base URL | `http://localhost:8080` | + +## Pages + +- **Login** — authenticates via `/api/auth/login`, stores the JWT. Includes an + editable **Server settings** section (like the phone app): enter an API Server + URL to point the browser directly at any server (uses the server's CORS), or + leave it blank to use this site's built-in BFF proxy. The choice is remembered + in `localStorage`. +- **Devices** — list/remove your registered phones, see online status & last seen. +- **Send SMS** — queue an outbound message (multiple numbers, pick device/SIM). +- **Call** — remotely tell a device to place an outbound phone call. +- **Messages** — outbound history with live status (`Pending`→…→`Delivered`/`Failed`), filterable. +- **Inbox** — incoming messages received by your devices. +- **Webhooks** — register/delete callbacks for `sms:received|sent|delivered|failed`. + +The header shows a live **API Server status** indicator (green/red dot) that polls +`/api/health` every 10s against the configured server, with latency on hover. + +Log in with the user you created via `../API Server/scripts/create-user.mjs`. diff --git a/Web App/server/.env.example b/Web App/server/.env.example new file mode 100644 index 0000000..4a29e16 --- /dev/null +++ b/Web App/server/.env.example @@ -0,0 +1,8 @@ +# Web App (BFF) configuration + +# Address the Web App listens on +WEB_ADDR=:8090 + +# Base URL of the API Server. The BFF reverse-proxies /api/* to this host, so +# the browser only ever talks same-origin to the Web App. +API_BASE=http://localhost:8080 diff --git a/Web App/server/Run-WebApp.ps1 b/Web App/server/Run-WebApp.ps1 new file mode 100644 index 0000000..1d530ab --- /dev/null +++ b/Web App/server/Run-WebApp.ps1 @@ -0,0 +1,29 @@ +# Builds the Vue frontend (if needed) and runs the Web App BFF. +# +# ./Run-WebApp.ps1 # build frontend then serve +# ./Run-WebApp.ps1 -SkipBuild # serve existing dist only + +param([switch]$SkipBuild) + +$ErrorActionPreference = "Stop" +$serverDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$root = Split-Path -Parent $serverDir # the "Web App" folder +$webDir = Join-Path $root "web" + +$goBin = "C:\Program Files\Go\bin" +if (Test-Path $goBin) { $env:Path = "$goBin;$env:Path" } + +if (-not $SkipBuild) { + Write-Host "Building frontend..." -ForegroundColor Cyan + Push-Location $webDir + try { + if (-not (Test-Path "node_modules")) { npm install } + npm run build + } finally { Pop-Location } +} + +Push-Location $serverDir +try { + Write-Host "Starting Web App on :8090 (Ctrl+C to stop)..." -ForegroundColor Cyan + go run . +} finally { Pop-Location } diff --git a/Web App/server/dist/apple-touch-icon.png b/Web App/server/dist/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..80236eb1492981695e669062d7337b8261acffad GIT binary patch literal 4109 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:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:12px}.rounded-md{border-radius:8px}.rounded-sm{border-radius:6px}.rounded-xl{border-radius:15px}.rounded-xs{border-radius:4px}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-strong{border-color:var(--border-strong)}.border-subtle{border-color:var(--border-subtle)}.\!bg-brand-tint{background-color:var(--brand-tint)!important}.bg-brand-tint{background-color:var(--brand-tint)}.bg-card{background-color:var(--surface-card)}.bg-current{background-color:currentColor}.bg-danger-tint{background-color:var(--danger-tint)}.bg-page{background-color:var(--bg-page)}.bg-subtle{background-color:var(--border-subtle)}.bg-success-tint{background-color:var(--success-tint)}.bg-sunken{background-color:var(--bg-sunken)}.bg-warning-tint{background-color:var(--warning-tint)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-7{padding:calc(var(--spacing) * 7)}.p-8{padding:calc(var(--spacing) * 8)}.p-10{padding:calc(var(--spacing) * 10)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.gn-eyebrow{font-family:var(--font-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--text-muted);font-size:11px}.font-mono{font-family:JetBrains Mono,ui-monospace,SFMono-Regular,Menlo,monospace}.\!text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.whitespace-nowrap{white-space:nowrap}.\!text-brand-active{color:var(--brand-active)!important}.\!text-secondary{color:var(--text-secondary)!important}.text-brand-active{color:var(--brand-active)}.text-danger{color:var(--danger)}.text-muted{color:var(--text-muted)}.text-primary{color:var(--text-primary)}.text-secondary{color:var(--text-secondary)}.text-success{color:var(--success)}.text-warning{color:var(--warning)}.opacity-60{opacity:.6}.shadow-lg{--tw-shadow:var(--sh-lg);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:var(--sh-sm);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xs{--tw-shadow:var(--sh-xs);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:bg-sunken:hover{background-color:var(--bg-sunken)}.hover\:text-primary:hover{color:var(--text-primary)}.hover\:underline:hover{text-decoration-line:underline}}@media(min-width:40rem){.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}}@media(min-width:48rem){.md\:inline{display:inline}}}:root{--green-100:oklch(95% .04 152);--green-200:oklch(88% .07 152);--green-300:oklch(78% .11 152);--green-500:oklch(60% .14 152);--green-600:oklch(52% .13 152);--green-700:oklch(44% .11 152);--green-on-dark:oklch(72% .14 152);--ink:#12161c;--ink-900:#0a0d11;--ink-800:#1a1f27;--paper:#fafaf9;--white:#fff;--gray-50:#f4f5f4;--gray-100:#e9ebea;--gray-200:#dcdfde;--gray-300:#c2c7c6;--gray-400:#9aa0a2;--gray-500:#6b7278;--gray-600:#4a5157;--gray-700:#333a40;--success:oklch(60% .14 152);--warning:oklch(72% .15 75);--danger:oklch(60% .17 25);--info:oklch(62% .13 245);--success-x:oklch(95% .04 152);--warning-x:oklch(95% .05 80);--danger-x:oklch(95% .05 25);--info-x:oklch(95% .04 245);--bg-page:var(--paper);--bg-sunken:var(--gray-50);--surface-card:var(--white);--surface-raised:var(--white);--surface-inverse:var(--ink);--surface-code:var(--ink-900);--border-subtle:#12161c14;--border-strong:#12161c33;--border-focus:var(--green-500);--text-primary:var(--ink);--text-secondary:var(--gray-700);--text-muted:var(--gray-500);--text-inverse:#f5f6f4;--text-brand:var(--green-600);--text-link:var(--green-600);--brand:var(--green-500);--brand-hover:var(--green-600);--brand-active:var(--green-700);--brand-tint:var(--green-100);--brand-contrast:var(--white);--brand-on-dark:var(--green-on-dark);--success-tint:var(--success-x);--warning-tint:var(--warning-x);--danger-tint:var(--danger-x);--info-tint:var(--info-x);--ring-focus:0 0 0 3px oklch(60% .14 152/.45);--ring-danger:0 0 0 3px oklch(60% .17 25/.35);--sh-xs:0 1px 2px #12161c0a;--sh-sm:0 1px 2px #12161c0f, 0 1px 1px #12161c0a;--sh-md:0 4px 12px #12161c14;--sh-lg:0 8px 24px #12161c24;--sh-xl:0 16px 40px #12161c2e;--dur-instant:.1s;--dur-fast:.12s;--dur-base:.18s;--dur-slow:.26s;--ease-standard:cubic-bezier(.2, 0, 0, 1);--ease-entrance:cubic-bezier(.16, 1, .3, 1)}[data-gsm-theme=dark]{--bg-page:var(--ink-900);--bg-sunken:#0e1216;--surface-card:#14191f;--surface-raised:#1a2027;--surface-inverse:var(--white);--surface-code:#05070a;--border-subtle:#ffffff12;--border-strong:#ffffff2e;--border-focus:var(--green-on-dark);--text-primary:#f5f6f4;--text-secondary:var(--gray-200);--text-muted:var(--gray-400);--text-inverse:var(--ink);--text-brand:var(--green-on-dark);--text-link:var(--green-on-dark);--brand:var(--green-500);--brand-hover:var(--green-on-dark);--brand-active:var(--green-600);--brand-tint:oklch(60% .14 152/.16);--brand-contrast:var(--white);--success:var(--green-300);--warning:oklch(80% .14 80);--danger:oklch(72% .16 25);--info:oklch(72% .12 245);--success-tint:oklch(60% .14 152/.16);--warning-tint:oklch(72% .15 75/.16);--danger-tint:oklch(60% .17 25/.18);--info-tint:oklch(62% .13 245/.16);--ring-focus:0 0 0 3px oklch(72% .14 152/.45);--sh-xs:0 1px 2px #0006;--sh-sm:0 1px 3px #00000080, 0 1px 2px #0006;--sh-md:0 4px 12px #00000080, 0 2px 4px #0006;--sh-lg:0 8px 24px #0009, 0 4px 8px #0006;--sh-xl:0 16px 40px #000000b3;color-scheme:dark}html,body,#app{height:100%}body{font-family:var(--font-sans);background:var(--bg-page);color:var(--text-primary);-webkit-font-smoothing:antialiased}h1,h2,h3{font-family:var(--font-display);letter-spacing:-.02em}.gn-input:focus,.gn-textarea:focus{border-color:var(--border-focus);box-shadow:var(--ring-focus);outline:none}.gn-input::placeholder,.gn-textarea::placeholder{color:var(--text-muted)}.gn-btn-pri:hover:not(:disabled){background:var(--brand-hover)}.gn-btn-pri:active:not(:disabled){background:var(--brand-active);transform:translateY(1px)}.gn-btn-pri:focus-visible{box-shadow:var(--ring-focus);outline:none}.gn-btn-pri:disabled{opacity:.45;cursor:not-allowed}.gn-btn-sec:hover:not(:disabled){background:var(--bg-sunken)}.gn-btn-sec:active:not(:disabled){transform:translateY(1px)}.gn-btn-sec:focus-visible{box-shadow:var(--ring-focus);outline:none}.gn-btn-sec:disabled{opacity:.45;cursor:not-allowed}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes ping{75%,to{opacity:0;transform:scale(2)}} diff --git a/Web App/server/dist/assets/index-B7DZHD5s.js b/Web App/server/dist/assets/index-B7DZHD5s.js new file mode 100644 index 0000000..a6e5075 --- /dev/null +++ b/Web App/server/dist/assets/index-B7DZHD5s.js @@ -0,0 +1,150 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&s(i)}).observe(document,{childList:!0,subtree:!0});function n(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(r){if(r.ep)return;r.ep=!0;const o=n(r);fetch(r.href,o)}})();/** +* @vue/shared v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Ys(e){const t=Object.create(null);for(const n of e.split(","))t[n]=1;return n=>n in t}const oe={},Kt=[],nt=()=>{},vo=()=>!1,Zn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),es=e=>e.startsWith("onUpdate:"),_e=Object.assign,Xs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},qi=Object.prototype.hasOwnProperty,X=(e,t)=>qi.call(e,t),U=Array.isArray,Gt=e=>Cn(e)==="[object Map]",ts=e=>Cn(e)==="[object Set]",_r=e=>Cn(e)==="[object Date]",B=e=>typeof e=="function",ae=e=>typeof e=="string",je=e=>typeof e=="symbol",ee=e=>e!==null&&typeof e=="object",bo=e=>(ee(e)||B(e))&&B(e.then)&&B(e.catch),_o=Object.prototype.toString,Cn=e=>_o.call(e),zi=e=>Cn(e).slice(8,-1),yo=e=>Cn(e)==="[object Object]",Zs=e=>ae(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,fn=Ys(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),ns=e=>{const t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},Ji=/-\w/g,Oe=ns(e=>e.replace(Ji,t=>t.slice(1).toUpperCase())),Qi=/\B([A-Z])/g,Dt=ns(e=>e.replace(Qi,"-$1").toLowerCase()),ss=ns(e=>e.charAt(0).toUpperCase()+e.slice(1)),vs=ns(e=>e?`on${ss(e)}`:""),tt=(e,t)=>!Object.is(e,t),Ln=(e,...t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:s,value:n})},rs=e=>{const t=parseFloat(e);return isNaN(t)?e:t};let yr;const os=()=>yr||(yr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function is(e){if(U(e)){const t={};for(let n=0;n{if(n){const s=n.split(Xi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function On(e){let t="";if(ae(e))t=e;else if(U(e))for(let n=0;nPn(n,t))}const So=e=>!!(e&&e.__v_isRef===!0),K=e=>ae(e)?e:e==null?"":U(e)||ee(e)&&(e.toString===_o||!B(e.toString))?So(e)?K(e.value):JSON.stringify(e,Eo,2):String(e),Eo=(e,t)=>So(t)?Eo(e,t.value):Gt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r],o)=>(n[bs(s,o)+" =>"]=r,n),{})}:ts(t)?{[`Set(${t.size})`]:[...t.values()].map(n=>bs(n))}:je(t)?bs(t):ee(t)&&!U(t)&&!yo(t)?String(t):t,bs=(e,t="")=>{var n;return je(e)?`Symbol(${(n=e.description)!=null?n:t})`:e};/** +* @vue/reactivity v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let ve;class ol{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&ve&&(ve.active?(this.parent=ve,this.index=(ve.scopes||(ve.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,n;if(this.scopes)for(t=0,n=this.scopes.length;t0&&--this._on===0){if(ve===this)ve=this.prevScope;else{let t=ve;for(;t;){if(t.prevScope===this){t.prevScope=this.prevScope;break}t=t.prevScope}}this.prevScope=void 0}}stop(t){if(this._active){this._active=!1;let n,s;for(n=0,s=this.effects.length;n0)return;if(pn){let t=pn;for(pn=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;dn;){let t=dn;for(dn=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function Oo(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Po(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),nr(s),ll(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Ns(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(Io(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function Io(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===bn)||(e.globalVersion=bn,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Ns(e))))return;e.flags|=2;const t=e.dep,n=ie,s=Fe;ie=e,Fe=!0;try{Oo(e);const r=e.fn(e._value);(t.version===0||tt(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{ie=n,Fe=s,Po(e),e.flags&=-3}}function nr(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let o=n.computed.deps;o;o=o.nextDep)nr(o,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function ll(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Fe=!0;const To=[];function st(){To.push(Fe),Fe=!1}function rt(){const e=To.pop();Fe=e===void 0?!0:e}function xr(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=ie;ie=void 0;try{t()}finally{ie=n}}}let bn=0;class cl{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class sr{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!ie||!Fe||ie===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==ie)n=this.activeLink=new cl(ie,this),ie.deps?(n.prevDep=ie.depsTail,ie.depsTail.nextDep=n,ie.depsTail=n):ie.deps=ie.depsTail=n,No(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=ie.depsTail,n.nextDep=void 0,ie.depsTail.nextDep=n,ie.depsTail=n,ie.deps===n&&(ie.deps=s)}return n}trigger(t){this.version++,bn++,this.notify(t)}notify(t){er();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{tr()}}}function No(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)No(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const ks=new WeakMap,Nt=Symbol(""),Ms=Symbol(""),_n=Symbol("");function ye(e,t,n){if(Fe&&ie){let s=ks.get(e);s||ks.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new sr),r.map=s,r.key=n),r.track()}}function ft(e,t,n,s,r,o){const i=ks.get(e);if(!i){bn++;return}const l=c=>{c&&c.trigger()};if(er(),t==="clear")i.forEach(l);else{const c=U(e),u=c&&Zs(n);if(c&&n==="length"){const a=Number(s);i.forEach((f,h)=>{(h==="length"||h===_n||!je(h)&&h>=a)&&l(f)})}else switch((n!==void 0||i.has(void 0))&&l(i.get(n)),u&&l(i.get(_n)),t){case"add":c?u&&l(i.get("length")):(l(i.get(Nt)),Gt(e)&&l(i.get(Ms)));break;case"delete":c||(l(i.get(Nt)),Gt(e)&&l(i.get(Ms)));break;case"set":Gt(e)&&l(i.get(Nt));break}}tr()}function Ft(e){const t=Y(e);return t===e?t:(ye(t,"iterate",_n),Le(e)?t:t.map(Ue))}function ls(e){return ye(e=Y(e),"iterate",_n),e}function Ze(e,t){return ht(e)?Qt(kt(e)?Ue(t):t):Ue(t)}const al={__proto__:null,[Symbol.iterator](){return ys(this,Symbol.iterator,e=>Ze(this,e))},concat(...e){return Ft(this).concat(...e.map(t=>U(t)?Ft(t):t))},entries(){return ys(this,"entries",e=>(e[1]=Ze(this,e[1]),e))},every(e,t){return lt(this,"every",e,t,void 0,arguments)},filter(e,t){return lt(this,"filter",e,t,n=>n.map(s=>Ze(this,s)),arguments)},find(e,t){return lt(this,"find",e,t,n=>Ze(this,n),arguments)},findIndex(e,t){return lt(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return lt(this,"findLast",e,t,n=>Ze(this,n),arguments)},findLastIndex(e,t){return lt(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return lt(this,"forEach",e,t,void 0,arguments)},includes(...e){return xs(this,"includes",e)},indexOf(...e){return xs(this,"indexOf",e)},join(e){return Ft(this).join(e)},lastIndexOf(...e){return xs(this,"lastIndexOf",e)},map(e,t){return lt(this,"map",e,t,void 0,arguments)},pop(){return rn(this,"pop")},push(...e){return rn(this,"push",e)},reduce(e,...t){return wr(this,"reduce",e,t)},reduceRight(e,...t){return wr(this,"reduceRight",e,t)},shift(){return rn(this,"shift")},some(e,t){return lt(this,"some",e,t,void 0,arguments)},splice(...e){return rn(this,"splice",e)},toReversed(){return Ft(this).toReversed()},toSorted(e){return Ft(this).toSorted(e)},toSpliced(...e){return Ft(this).toSpliced(...e)},unshift(...e){return rn(this,"unshift",e)},values(){return ys(this,"values",e=>Ze(this,e))}};function ys(e,t,n){const s=ls(e),r=s[t]();return s!==e&&!Le(e)&&(r._next=r.next,r.next=()=>{const o=r._next();return o.done||(o.value=n(o.value)),o}),r}const ul=Array.prototype;function lt(e,t,n,s,r,o){const i=ls(e),l=i!==e&&!Le(e),c=i[t];if(c!==ul[t]){const f=c.apply(e,o);return l?Ue(f):f}let u=n;i!==e&&(l?u=function(f,h){return n.call(this,Ze(e,f),h,e)}:n.length>2&&(u=function(f,h){return n.call(this,f,h,e)}));const a=c.call(i,u,s);return l&&r?r(a):a}function wr(e,t,n,s){const r=ls(e),o=r!==e&&!Le(e);let i=n,l=!1;r!==e&&(o?(l=s.length===0,i=function(u,a,f){return l&&(l=!1,u=Ze(e,u)),n.call(this,u,Ze(e,a),f,e)}):n.length>3&&(i=function(u,a,f){return n.call(this,u,a,f,e)}));const c=r[t](i,...s);return l?Ze(e,c):c}function xs(e,t,n){const s=Y(e);ye(s,"iterate",_n);const r=s[t](...n);return(r===-1||r===!1)&&ir(n[0])?(n[0]=Y(n[0]),s[t](...n)):r}function rn(e,t,n=[]){st(),er();const s=Y(e)[t].apply(e,n);return tr(),rt(),s}const fl=Ys("__proto__,__v_isRef,__isVue"),ko=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(je));function dl(e){je(e)||(e=String(e));const t=Y(this);return ye(t,"has",e),t.hasOwnProperty(e)}class Mo{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return o;if(n==="__v_raw")return s===(r?o?wl:Vo:o?Lo:$o).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const i=U(t);if(!r){let c;if(i&&(c=al[n]))return c;if(n==="hasOwnProperty")return dl}const l=Reflect.get(t,n,we(t)?t:s);if((je(n)?ko.has(n):fl(n))||(r||ye(t,"get",n),o))return l;if(we(l)){const c=i&&Zs(n)?l:l.value;return r&&ee(c)?Un(c):c}return ee(l)?r?Un(l):In(l):l}}class Do extends Mo{constructor(t=!1){super(!1,t)}set(t,n,s,r){let o=t[n];const i=U(t)&&Zs(n);if(!this._isShallow){const u=ht(o);if(!Le(s)&&!ht(s)&&(o=Y(o),s=Y(s)),!i&&we(o)&&!we(s))return u||(o.value=s),!0}const l=i?Number(n)e,kn=e=>Reflect.getPrototypeOf(e);function vl(e,t,n){return function(...s){const r=this.__v_raw,o=Y(r),i=Gt(o),l=e==="entries"||e===Symbol.iterator&&i,c=e==="keys"&&i,u=r[e](...s),a=n?Ds:t?Qt:Ue;return!t&&ye(o,"iterate",c?Ms:Nt),_e(Object.create(u),{next(){const{value:f,done:h}=u.next();return h?{value:f,done:h}:{value:l?[a(f[0]),a(f[1])]:a(f),done:h}}})}}function Mn(e){return function(...t){return e==="delete"?!1:e==="clear"?void 0:this}}function bl(e,t){const n={get(r){const o=this.__v_raw,i=Y(o),l=Y(r);e||(tt(r,l)&&ye(i,"get",r),ye(i,"get",l));const{has:c}=kn(i),u=t?Ds:e?Qt:Ue;if(c.call(i,r))return u(o.get(r));if(c.call(i,l))return u(o.get(l));o!==i&&o.get(r)},get size(){const r=this.__v_raw;return!e&&ye(Y(r),"iterate",Nt),r.size},has(r){const o=this.__v_raw,i=Y(o),l=Y(r);return e||(tt(r,l)&&ye(i,"has",r),ye(i,"has",l)),r===l?o.has(r):o.has(r)||o.has(l)},forEach(r,o){const i=this,l=i.__v_raw,c=Y(l),u=t?Ds:e?Qt:Ue;return!e&&ye(c,"iterate",Nt),l.forEach((a,f)=>r.call(o,u(a),u(f),i))}};return _e(n,e?{add:Mn("add"),set:Mn("set"),delete:Mn("delete"),clear:Mn("clear")}:{add(r){const o=Y(this),i=kn(o),l=Y(r),c=!t&&!Le(r)&&!ht(r)?l:r;return i.has.call(o,c)||tt(r,c)&&i.has.call(o,r)||tt(l,c)&&i.has.call(o,l)||(o.add(c),ft(o,"add",c,c)),this},set(r,o){!t&&!Le(o)&&!ht(o)&&(o=Y(o));const i=Y(this),{has:l,get:c}=kn(i);let u=l.call(i,r);u||(r=Y(r),u=l.call(i,r));const a=c.call(i,r);return i.set(r,o),u?tt(o,a)&&ft(i,"set",r,o):ft(i,"add",r,o),this},delete(r){const o=Y(this),{has:i,get:l}=kn(o);let c=i.call(o,r);c||(r=Y(r),c=i.call(o,r)),l&&l.call(o,r);const u=o.delete(r);return c&&ft(o,"delete",r,void 0),u},clear(){const r=Y(this),o=r.size!==0,i=r.clear();return o&&ft(r,"clear",void 0,void 0),i}}),["keys","values","entries",Symbol.iterator].forEach(r=>{n[r]=vl(r,e,t)}),n}function rr(e,t){const n=bl(e,t);return(s,r,o)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(X(n,r)&&r in s?n:s,r,o)}const _l={get:rr(!1,!1)},yl={get:rr(!1,!0)},xl={get:rr(!0,!1)};const $o=new WeakMap,Lo=new WeakMap,Vo=new WeakMap,wl=new WeakMap;function Sl(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function In(e){return ht(e)?e:or(e,!1,hl,_l,$o)}function Fo(e){return or(e,!1,gl,yl,Lo)}function Un(e){return or(e,!0,ml,xl,Vo)}function or(e,t,n,s,r){if(!ee(e)||e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;const o=r.get(e);if(o)return o;const i=Sl(zi(e));if(i===0)return e;const l=new Proxy(e,i===2?s:n);return r.set(e,l),l}function kt(e){return ht(e)?kt(e.__v_raw):!!(e&&e.__v_isReactive)}function ht(e){return!!(e&&e.__v_isReadonly)}function Le(e){return!!(e&&e.__v_isShallow)}function ir(e){return e?!!e.__v_raw:!1}function Y(e){const t=e&&e.__v_raw;return t?Y(t):e}function El(e){return!X(e,"__v_skip")&&Object.isExtensible(e)&&xo(e,"__v_skip",!0),e}const Ue=e=>ee(e)?In(e):e,Qt=e=>ee(e)?Un(e):e;function we(e){return e?e.__v_isRef===!0:!1}function G(e){return jo(e,!1)}function Al(e){return jo(e,!0)}function jo(e,t){return we(e)?e:new Rl(e,t)}class Rl{constructor(t,n){this.dep=new sr,this.__v_isRef=!0,this.__v_isShallow=!1,this._rawValue=n?t:Y(t),this._value=n?t:Ue(t),this.__v_isShallow=n}get value(){return this.dep.track(),this._value}set value(t){const n=this._rawValue,s=this.__v_isShallow||Le(t)||ht(t);t=s?t:Y(t),tt(t,n)&&(this._rawValue=t,this._value=s?t:Ue(t),this.dep.trigger())}}function Z(e){return we(e)?e.value:e}const Cl={get:(e,t,n)=>t==="__v_raw"?e:Z(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return we(r)&&!we(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function Uo(e){return kt(e)?e:new Proxy(e,Cl)}class Ol{constructor(t,n,s){this.fn=t,this.setter=n,this._value=void 0,this.dep=new sr(this),this.__v_isRef=!0,this.deps=void 0,this.depsTail=void 0,this.flags=16,this.globalVersion=bn-1,this.next=void 0,this.effect=this,this.__v_isReadonly=!n,this.isSSR=s}notify(){if(this.flags|=16,!(this.flags&8)&&ie!==this)return Co(this,!0),!0}get value(){const t=this.dep.track();return Io(this),t&&(t.version=this.dep.version),this._value}set value(t){this.setter&&this.setter(t)}}function Pl(e,t,n=!1){let s,r;return B(e)?s=e:(s=e.get,r=e.set),new Ol(s,r,n)}const Dn={},Hn=new WeakMap;let Pt;function Il(e,t=!1,n=Pt){if(n){let s=Hn.get(n);s||Hn.set(n,s=[]),s.push(e)}}function Tl(e,t,n=oe){const{immediate:s,deep:r,once:o,scheduler:i,augmentJob:l,call:c}=n,u=k=>r?k:Le(k)||r===!1||r===0?dt(k,1):dt(k);let a,f,h,v,T=!1,A=!1;if(we(e)?(f=()=>e.value,T=Le(e)):kt(e)?(f=()=>u(e),T=!0):U(e)?(A=!0,T=e.some(k=>kt(k)||Le(k)),f=()=>e.map(k=>{if(we(k))return k.value;if(kt(k))return u(k);if(B(k))return c?c(k,2):k()})):B(e)?t?f=c?()=>c(e,2):e:f=()=>{if(h){st();try{h()}finally{rt()}}const k=Pt;Pt=a;try{return c?c(e,3,[v]):e(v)}finally{Pt=k}}:f=nt,t&&r){const k=f,se=r===!0?1/0:r;f=()=>dt(k(),se)}const j=il(),V=()=>{a.stop(),j&&j.active&&Xs(j.effects,a)};if(o&&t){const k=t;t=(...se)=>{const pe=k(...se);return V(),pe}}let P=A?new Array(e.length).fill(Dn):Dn;const M=k=>{if(!(!(a.flags&1)||!a.dirty&&!k))if(t){const se=a.run();if(k||r||T||(A?se.some((pe,le)=>tt(pe,P[le])):tt(se,P))){h&&h();const pe=Pt;Pt=a;try{const le=[se,P===Dn?void 0:A&&P[0]===Dn?[]:P,v];P=se,c?c(t,3,le):t(...le)}finally{Pt=pe}}}else a.run()};return l&&l(M),a=new Ao(f),a.scheduler=i?()=>i(M,!1):M,v=k=>Il(k,!1,a),h=a.onStop=()=>{const k=Hn.get(a);if(k){if(c)c(k,4);else for(const se of k)se();Hn.delete(a)}},t?s?M(!0):P=a.run():i?i(M.bind(null,!0),!0):a.run(),V.pause=a.pause.bind(a),V.resume=a.resume.bind(a),V.stop=V,V}function dt(e,t=1/0,n){if(t<=0||!ee(e)||e.__v_skip||(n=n||new Map,(n.get(e)||0)>=t))return e;if(n.set(e,t),t--,we(e))dt(e.value,t,n);else if(U(e))for(let s=0;s{dt(s,t,n)});else if(yo(e)){for(const s in e)dt(e[s],t,n);for(const s of Object.getOwnPropertySymbols(e))Object.prototype.propertyIsEnumerable.call(e,s)&&dt(e[s],t,n)}return e}/** +* @vue/runtime-core v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/function Tn(e,t,n,s){try{return s?e(...s):e()}catch(r){cs(r,t,n)}}function He(e,t,n,s){if(B(e)){const r=Tn(e,t,n,s);return r&&bo(r)&&r.catch(o=>{cs(o,t,n)}),r}if(U(e)){const r=[];for(let o=0;o>>1,r=Ce[s],o=yn(r);o=yn(n)?Ce.push(e):Ce.splice(kl(t),0,e),e.flags|=1,Bo()}}function Bo(){Bn||(Bn=Ho.then(Go))}function Ml(e){U(e)?Wt.push(...e):xt&&e.id===-1?xt.splice(Ht+1,0,e):e.flags&1||(Wt.push(e),e.flags|=1),Bo()}function Sr(e,t,n=Xe+1){for(;nyn(n)-yn(s));if(Wt.length=0,xt){xt.push(...t);return}for(xt=t,Ht=0;Hte.id==null?e.flags&2?-1:1/0:e.id;function Go(e){try{for(Xe=0;Xe{s._d&&qn(-1);const o=Kn(t);let i;try{i=e(...r)}finally{Kn(o),s._d&&qn(1)}return i};return s._n=!0,s._c=!0,s._d=!0,s}function Ne(e,t){if(be===null)return e;const n=ds(be),s=e.dirs||(e.dirs=[]);for(let r=0;r1)return n&&B(t)?t.call(s&&s.proxy):t}}const Dl=Symbol.for("v-scx"),$l=()=>Ve(Dl);function Fn(e,t,n){return qo(e,t,n)}function qo(e,t,n=oe){const{immediate:s,deep:r,flush:o,once:i}=n,l=_e({},n),c=t&&s||!t&&o!=="post";let u;if(Sn){if(o==="sync"){const v=$l();u=v.__watcherHandles||(v.__watcherHandles=[])}else if(!c){const v=()=>{};return v.stop=nt,v.resume=nt,v.pause=nt,v}}const a=xe;l.call=(v,T,A)=>He(v,a,T,A);let f=!1;o==="post"?l.scheduler=v=>{Ie(v,a&&a.suspense)}:o!=="sync"&&(f=!0,l.scheduler=(v,T)=>{T?v():cr(v)}),l.augmentJob=v=>{t&&(v.flags|=4),f&&(v.flags|=2,a&&(v.id=a.uid,v.i=a))};const h=Tl(e,t,l);return Sn&&(u?u.push(h):c&&h()),h}function Ll(e,t,n){const s=this.proxy,r=ae(e)?e.includes(".")?zo(s,e):()=>s[e]:e.bind(s,s);let o;B(t)?o=t:(o=t.handler,n=t);const i=Nn(this),l=qo(r,o.bind(s),n);return i(),l}function zo(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;re.__isTeleport,ws=Symbol("_leaveCb");function ar(e,t){e.shapeFlag&6&&e.component?(e.transition=t,ar(e.component.subTree,t)):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Jo(e,t){return B(e)?_e({name:e.name},t,{setup:e}):e}function Qo(e){e.ids=[e.ids[0]+e.ids[2]+++"-",0,0]}function Er(e,t){let n;return!!((n=Object.getOwnPropertyDescriptor(e,t))&&!n.configurable)}const Gn=new WeakMap;function hn(e,t,n,s,r=!1){if(U(e)){e.forEach((A,j)=>hn(A,t&&(U(t)?t[j]:t),n,s,r));return}if(qt(s)&&!r){s.shapeFlag&512&&s.type.__asyncResolved&&s.component.subTree.component&&hn(e,t,n,s.component.subTree);return}const o=s.shapeFlag&4?ds(s.component):s.el,i=r?null:o,{i:l,r:c}=e,u=t&&t.r,a=l.refs===oe?l.refs={}:l.refs,f=l.setupState,h=Y(f),v=f===oe?vo:A=>Er(a,A)?!1:X(h,A),T=(A,j)=>!(j&&Er(a,j));if(u!=null&&u!==c){if(Ar(t),ae(u))a[u]=null,v(u)&&(f[u]=null);else if(we(u)){const A=t;T(u,A.k)&&(u.value=null),A.k&&(a[A.k]=null)}}if(B(c)){st();try{Tn(c,l,12,[i,a])}finally{rt()}}else{const A=ae(c),j=we(c);if(A||j){const V=()=>{if(e.f){const P=A?v(c)?f[c]:a[c]:T()||!e.k?c.value:a[e.k];if(r)U(P)&&Xs(P,o);else if(U(P))P.includes(o)||P.push(o);else if(A)a[c]=[o],v(c)&&(f[c]=a[c]);else{const M=[o];T(c,e.k)&&(c.value=M),e.k&&(a[e.k]=M)}}else A?(a[c]=i,v(c)&&(f[c]=i)):j&&(T(c,e.k)&&(c.value=i),e.k&&(a[e.k]=i))};if(i){const P=()=>{V(),Gn.delete(e)};P.id=-1,Gn.set(e,P),Ie(P,n)}else Ar(e),V()}}}function Ar(e){const t=Gn.get(e);t&&(t.flags|=8,Gn.delete(e))}os().requestIdleCallback;os().cancelIdleCallback;const qt=e=>!!e.type.__asyncLoader,Yo=e=>e.type.__isKeepAlive;function jl(e,t){Xo(e,"a",t)}function Ul(e,t){Xo(e,"da",t)}function Xo(e,t,n=xe){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(as(t,s,n),n){let r=n.parent;for(;r&&r.parent;)Yo(r.parent.vnode)&&Hl(s,t,n,r),r=r.parent}}function Hl(e,t,n,s){const r=as(t,e,s,!0);ur(()=>{Xs(s[t],r)},n)}function as(e,t,n=xe,s=!1){if(n){const r=n[e]||(n[e]=[]),o=t.__weh||(t.__weh=(...i)=>{st();const l=Nn(n),c=He(t,n,e,i);return l(),rt(),c});return s?r.unshift(o):r.push(o),o}}const gt=e=>(t,n=xe)=>{(!Sn||e==="sp")&&as(e,(...s)=>t(...s),n)},Bl=gt("bm"),Et=gt("m"),Kl=gt("bu"),Gl=gt("u"),Wl=gt("bum"),ur=gt("um"),ql=gt("sp"),zl=gt("rtg"),Jl=gt("rtc");function Ql(e,t=xe){as("ec",e,t)}const Zo="components";function Yl(e,t){return ti(Zo,e,!0,t)||e}const ei=Symbol.for("v-ndc");function Xl(e){return ae(e)?ti(Zo,e,!1)||e:e||ei}function ti(e,t,n=!0,s=!1){const r=be||xe;if(r){const o=r.type;{const l=Vc(o,!1);if(l&&(l===t||l===Oe(t)||l===ss(Oe(t))))return o}const i=Rr(r[e]||o[e],t)||Rr(r.appContext[e],t);return!i&&s?o:i}}function Rr(e,t){return e&&(e[t]||e[Oe(t)]||e[ss(Oe(t))])}function ot(e,t,n,s){let r;const o=n,i=U(e);if(i||ae(e)){const l=i&&kt(e);let c=!1,u=!1;l&&(c=!Le(e),u=ht(e),e=ls(e)),r=new Array(e.length);for(let a=0,f=e.length;at(l,c,void 0,o));else{const l=Object.keys(e);r=new Array(l.length);for(let c=0,u=l.length;c0;return n.name=t,N(),pt(ce,null,[W("slot",n,s)],u?-2:64)}let o=e[t];o&&o._c&&(o._d=!1),N();const i=o&&ni(o(n)),l=n.key||i&&i.key,c=pt(ce,{key:(l&&!je(l)?l:`_${t}`)+(!i&&s?"_fb":"")},i||[],i&&e._===1?64:-2);return c.scopeId&&(c.slotScopeIds=[c.scopeId+"-s"]),o&&o._c&&(o._d=!0),c}function ni(e){return e.some(t=>wn(t)?!(t.type===mt||t.type===ce&&!ni(t.children)):!0)?e:null}const $s=e=>e?wi(e)?ds(e):$s(e.parent):null,mn=_e(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>$s(e.parent),$root:e=>$s(e.root),$host:e=>e.ce,$emit:e=>e.emit,$options:e=>ri(e),$forceUpdate:e=>e.f||(e.f=()=>{cr(e.update)}),$nextTick:e=>e.n||(e.n=lr.bind(e.proxy)),$watch:e=>Ll.bind(e)}),Ss=(e,t)=>e!==oe&&!e.__isScriptSetup&&X(e,t),ec={get({_:e},t){if(t==="__v_skip")return!0;const{ctx:n,setupState:s,data:r,props:o,accessCache:i,type:l,appContext:c}=e;if(t[0]!=="$"){const h=i[t];if(h!==void 0)switch(h){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return o[t]}else{if(Ss(s,t))return i[t]=1,s[t];if(r!==oe&&X(r,t))return i[t]=2,r[t];if(X(o,t))return i[t]=3,o[t];if(n!==oe&&X(n,t))return i[t]=4,n[t];Ls&&(i[t]=0)}}const u=mn[t];let a,f;if(u)return t==="$attrs"&&ye(e.attrs,"get",""),u(e);if((a=l.__cssModules)&&(a=a[t]))return a;if(n!==oe&&X(n,t))return i[t]=4,n[t];if(f=c.config.globalProperties,X(f,t))return f[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:o}=e;return Ss(r,t)?(r[t]=n,!0):s!==oe&&X(s,t)?(s[t]=n,!0):X(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(o[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,props:o,type:i}},l){let c;return!!(n[l]||e!==oe&&l[0]!=="$"&&X(e,l)||Ss(t,l)||X(o,l)||X(s,l)||X(mn,l)||X(r.config.globalProperties,l)||(c=i.__cssModules)&&c[l])},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:X(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function Cr(e){return U(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let Ls=!0;function tc(e){const t=ri(e),n=e.proxy,s=e.ctx;Ls=!1,t.beforeCreate&&Or(t.beforeCreate,e,"bc");const{data:r,computed:o,methods:i,watch:l,provide:c,inject:u,created:a,beforeMount:f,mounted:h,beforeUpdate:v,updated:T,activated:A,deactivated:j,beforeDestroy:V,beforeUnmount:P,destroyed:M,unmounted:k,render:se,renderTracked:pe,renderTriggered:le,errorCaptured:Ke,serverPrefetch:vt,expose:Ge,inheritAttrs:bt,components:At,directives:We,filters:nn}=t;if(u&&nc(u,s,null),i)for(const te in i){const J=i[te];B(J)&&(s[te]=J.bind(n))}if(r){const te=r.call(n,n);ee(te)&&(e.data=In(te))}if(Ls=!0,o)for(const te in o){const J=o[te],it=B(J)?J.bind(n,n):B(J.get)?J.get.bind(n,n):nt,_t=!B(J)&&B(J.set)?J.set.bind(n):nt,qe=$e({get:it,set:_t});Object.defineProperty(s,te,{enumerable:!0,configurable:!0,get:()=>qe.value,set:Pe=>qe.value=Pe})}if(l)for(const te in l)si(l[te],s,n,te);if(c){const te=B(c)?c.call(n):c;Reflect.ownKeys(te).forEach(J=>{Vn(J,te[J])})}a&&Or(a,e,"c");function me(te,J){U(J)?J.forEach(it=>te(it.bind(n))):J&&te(J.bind(n))}if(me(Bl,f),me(Et,h),me(Kl,v),me(Gl,T),me(jl,A),me(Ul,j),me(Ql,Ke),me(Jl,pe),me(zl,le),me(Wl,P),me(ur,k),me(ql,vt),U(Ge))if(Ge.length){const te=e.exposed||(e.exposed={});Ge.forEach(J=>{Object.defineProperty(te,J,{get:()=>n[J],set:it=>n[J]=it,enumerable:!0})})}else e.exposed||(e.exposed={});se&&e.render===nt&&(e.render=se),bt!=null&&(e.inheritAttrs=bt),At&&(e.components=At),We&&(e.directives=We),vt&&Qo(e)}function nc(e,t,n=nt){U(e)&&(e=Vs(e));for(const s in e){const r=e[s];let o;ee(r)?"default"in r?o=Ve(r.from||s,r.default,!0):o=Ve(r.from||s):o=Ve(r),we(o)?Object.defineProperty(t,s,{enumerable:!0,configurable:!0,get:()=>o.value,set:i=>o.value=i}):t[s]=o}}function Or(e,t,n){He(U(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function si(e,t,n,s){let r=s.includes(".")?zo(n,s):()=>n[s];if(ae(e)){const o=t[e];B(o)&&Fn(r,o)}else if(B(e))Fn(r,e.bind(n));else if(ee(e))if(U(e))e.forEach(o=>si(o,t,n,s));else{const o=B(e.handler)?e.handler.bind(n):t[e.handler];B(o)&&Fn(r,o,e)}}function ri(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:o,config:{optionMergeStrategies:i}}=e.appContext,l=o.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(u=>Wn(c,u,i,!0)),Wn(c,t,i)),ee(t)&&o.set(t,c),c}function Wn(e,t,n,s=!1){const{mixins:r,extends:o}=t;o&&Wn(e,o,n,!0),r&&r.forEach(i=>Wn(e,i,n,!0));for(const i in t)if(!(s&&i==="expose")){const l=sc[i]||n&&n[i];e[i]=l?l(e[i],t[i]):t[i]}return e}const sc={data:Pr,props:Ir,emits:Ir,methods:an,computed:an,beforeCreate:Ae,created:Ae,beforeMount:Ae,mounted:Ae,beforeUpdate:Ae,updated:Ae,beforeDestroy:Ae,beforeUnmount:Ae,destroyed:Ae,unmounted:Ae,activated:Ae,deactivated:Ae,errorCaptured:Ae,serverPrefetch:Ae,components:an,directives:an,watch:oc,provide:Pr,inject:rc};function Pr(e,t){return t?e?function(){return _e(B(e)?e.call(this,this):e,B(t)?t.call(this,this):t)}:t:e}function rc(e,t){return an(Vs(e),Vs(t))}function Vs(e){if(U(e)){const t={};for(let n=0;nt==="modelValue"||t==="model-value"?e.modelModifiers:e[`${t}Modifiers`]||e[`${Oe(t)}Modifiers`]||e[`${Dt(t)}Modifiers`];function ac(e,t,...n){if(e.isUnmounted)return;const s=e.vnode.props||oe;let r=n;const o=t.startsWith("update:"),i=o&&cc(s,t.slice(7));i&&(i.trim&&(r=n.map(a=>ae(a)?a.trim():a)),i.number&&(r=n.map(rs)));let l,c=s[l=vs(t)]||s[l=vs(Oe(t))];!c&&o&&(c=s[l=vs(Dt(t))]),c&&He(c,e,6,r);const u=s[l+"Once"];if(u){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,He(u,e,6,r)}}const uc=new WeakMap;function ii(e,t,n=!1){const s=n?uc:t.emitsCache,r=s.get(e);if(r!==void 0)return r;const o=e.emits;let i={},l=!1;if(!B(e)){const c=u=>{const a=ii(u,t,!0);a&&(l=!0,_e(i,a))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!o&&!l?(ee(e)&&s.set(e,null),null):(U(o)?o.forEach(c=>i[c]=null):_e(i,o),ee(e)&&s.set(e,i),i)}function us(e,t){return!e||!Zn(t)?!1:(t=t.slice(2),t=t==="Once"?t:t.replace(/Once$/,""),X(e,t[0].toLowerCase()+t.slice(1))||X(e,Dt(t))||X(e,t))}function Tr(e){const{type:t,vnode:n,proxy:s,withProxy:r,propsOptions:[o],slots:i,attrs:l,emit:c,render:u,renderCache:a,props:f,data:h,setupState:v,ctx:T,inheritAttrs:A}=e,j=Kn(e);let V,P;try{if(n.shapeFlag&4){const k=r||s,se=k;V=et(u.call(se,k,a,f,v,h,T)),P=l}else{const k=t;V=et(k.length>1?k(f,{attrs:l,slots:i,emit:c}):k(f,null)),P=t.props?l:fc(l)}}catch(k){gn.length=0,cs(k,e,1),V=W(mt)}let M=V;if(P&&A!==!1){const k=Object.keys(P),{shapeFlag:se}=M;k.length&&se&7&&(o&&k.some(es)&&(P=dc(P,o)),M=Xt(M,P,!1,!0))}return n.dirs&&(M=Xt(M,null,!1,!0),M.dirs=M.dirs?M.dirs.concat(n.dirs):n.dirs),n.transition&&ar(M,n.transition),V=M,Kn(j),V}const fc=e=>{let t;for(const n in e)(n==="class"||n==="style"||Zn(n))&&((t||(t={}))[n]=e[n]);return t},dc=(e,t)=>{const n={};for(const s in e)(!es(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function pc(e,t,n){const{props:s,children:r,component:o}=e,{props:i,children:l,patchFlag:c}=t,u=o.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?Nr(s,i,u):!!i;if(c&8){const a=t.dynamicProps;for(let f=0;fObject.create(ci),ui=e=>Object.getPrototypeOf(e)===ci;function mc(e,t,n,s=!1){const r={},o=ai();e.propsDefaults=Object.create(null),fi(e,t,r,o);for(const i in e.propsOptions[0])i in r||(r[i]=void 0);n?e.props=s?r:Fo(r):e.type.props?e.props=r:e.props=o,e.attrs=o}function gc(e,t,n,s){const{props:r,attrs:o,vnode:{patchFlag:i}}=e,l=Y(r),[c]=e.propsOptions;let u=!1;if((s||i>0)&&!(i&16)){if(i&8){const a=e.vnode.dynamicProps;for(let f=0;f{c=!0;const[h,v]=di(f,t,!0);_e(i,h),v&&l.push(...v)};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}if(!o&&!c)return ee(e)&&s.set(e,Kt),Kt;if(U(o))for(let a=0;ae==="_"||e==="_ctx"||e==="$stable",dr=e=>U(e)?e.map(et):[et(e)],bc=(e,t,n)=>{if(t._n)return t;const s=Yt((...r)=>dr(t(...r)),n);return s._c=!1,s},pi=(e,t,n)=>{const s=e._ctx;for(const r in e){if(fr(r))continue;const o=e[r];if(B(o))t[r]=bc(r,o,s);else if(o!=null){const i=dr(o);t[r]=()=>i}}},hi=(e,t)=>{const n=dr(t);e.slots.default=()=>n},mi=(e,t,n)=>{for(const s in t)(n||!fr(s))&&(e[s]=t[s])},_c=(e,t,n)=>{const s=e.slots=ai();if(e.vnode.shapeFlag&32){const r=t._;r?(mi(s,t,n),n&&xo(s,"_",r,!0)):pi(t,s)}else t&&hi(e,t)},yc=(e,t,n)=>{const{vnode:s,slots:r}=e;let o=!0,i=oe;if(s.shapeFlag&32){const l=t._;l?n&&l===1?o=!1:mi(r,t,n):(o=!t.$stable,pi(t,r)),i=t}else t&&(hi(e,t),i={default:1});if(o)for(const l in r)!fr(l)&&i[l]==null&&delete r[l]},Ie=Ac;function xc(e){return wc(e)}function wc(e,t){const n=os();n.__VUE__=!0;const{insert:s,remove:r,patchProp:o,createElement:i,createText:l,createComment:c,setText:u,setElementText:a,parentNode:f,nextSibling:h,setScopeId:v=nt,insertStaticContent:T}=e,A=(d,p,m,_=null,x=null,b=null,R=void 0,E=null,S=!!p.dynamicChildren)=>{if(d===p)return;d&&!on(d,p)&&(_=y(d),Pe(d,x,b,!0),d=null),p.patchFlag===-2&&(S=!1,p.dynamicChildren=null);const{type:w,ref:F,shapeFlag:O}=p;switch(w){case fs:j(d,p,m,_);break;case mt:V(d,p,m,_);break;case As:d==null&&P(p,m,_,R);break;case ce:At(d,p,m,_,x,b,R,E,S);break;default:O&1?se(d,p,m,_,x,b,R,E,S):O&6?We(d,p,m,_,x,b,R,E,S):(O&64||O&128)&&w.process(d,p,m,_,x,b,R,E,S,D)}F!=null&&x?hn(F,d&&d.ref,b,p||d,!p):F==null&&d&&d.ref!=null&&hn(d.ref,null,b,d,!0)},j=(d,p,m,_)=>{if(d==null)s(p.el=l(p.children),m,_);else{const x=p.el=d.el;p.children!==d.children&&u(x,p.children)}},V=(d,p,m,_)=>{d==null?s(p.el=c(p.children||""),m,_):p.el=d.el},P=(d,p,m,_)=>{[d.el,d.anchor]=T(d.children,p,m,_,d.el,d.anchor)},M=({el:d,anchor:p},m,_)=>{let x;for(;d&&d!==p;)x=h(d),s(d,m,_),d=x;s(p,m,_)},k=({el:d,anchor:p})=>{let m;for(;d&&d!==p;)m=h(d),r(d),d=m;r(p)},se=(d,p,m,_,x,b,R,E,S)=>{if(p.type==="svg"?R="svg":p.type==="math"&&(R="mathml"),d==null)pe(p,m,_,x,b,R,E,S);else{const w=d.el&&d.el._isVueCE?d.el:null;try{w&&w._beginPatch(),vt(d,p,x,b,R,E,S)}finally{w&&w._endPatch()}}},pe=(d,p,m,_,x,b,R,E)=>{let S,w;const{props:F,shapeFlag:O,transition:$,dirs:H}=d;if(S=d.el=i(d.type,b,F&&F.is,F),O&8?a(S,d.children):O&16&&Ke(d.children,S,null,_,x,Es(d,b),R,E),H&&Ct(d,null,_,"created"),le(S,d,d.scopeId,R,_),F){for(const re in F)re!=="value"&&!fn(re)&&o(S,re,null,F[re],b,_);"value"in F&&o(S,"value",null,F.value,b),(w=F.onVnodeBeforeMount)&&Ye(w,_,d)}H&&Ct(d,null,_,"beforeMount");const z=Sc(x,$);z&&$.beforeEnter(S),s(S,p,m),((w=F&&F.onVnodeMounted)||z||H)&&Ie(()=>{try{w&&Ye(w,_,d),z&&$.enter(S),H&&Ct(d,null,_,"mounted")}finally{}},x)},le=(d,p,m,_,x)=>{if(m&&v(d,m),_)for(let b=0;b<_.length;b++)v(d,_[b]);if(x){let b=x.subTree;if(p===b||_i(b.type)&&(b.ssContent===p||b.ssFallback===p)){const R=x.vnode;le(d,R,R.scopeId,R.slotScopeIds,x.parent)}}},Ke=(d,p,m,_,x,b,R,E,S=0)=>{for(let w=S;w{const E=p.el=d.el;let{patchFlag:S,dynamicChildren:w,dirs:F}=p;S|=d.patchFlag&16;const O=d.props||oe,$=p.props||oe;let H;if(m&&Ot(m,!1),(H=$.onVnodeBeforeUpdate)&&Ye(H,m,p,d),F&&Ct(p,d,m,"beforeUpdate"),m&&Ot(m,!0),w&&(!d.dynamicChildren||d.dynamicChildren.length!==w.length)&&(S=0,R=!1,w=null),(O.innerHTML&&$.innerHTML==null||O.textContent&&$.textContent==null)&&a(E,""),w?Ge(d.dynamicChildren,w,E,m,_,Es(p,x),b):R||J(d,p,E,null,m,_,Es(p,x),b,!1),S>0){if(S&16)bt(E,O,$,m,x);else if(S&2&&O.class!==$.class&&o(E,"class",null,$.class,x),S&4&&o(E,"style",O.style,$.style,x),S&8){const z=p.dynamicProps;for(let re=0;re{H&&Ye(H,m,p,d),F&&Ct(p,d,m,"updated")},_)},Ge=(d,p,m,_,x,b,R)=>{for(let E=0;E{if(p!==m){if(p!==oe)for(const b in p)!fn(b)&&!(b in m)&&o(d,b,p[b],null,x,_);for(const b in m){if(fn(b))continue;const R=m[b],E=p[b];R!==E&&b!=="value"&&o(d,b,E,R,x,_)}"value"in m&&o(d,"value",p.value,m.value,x)}},At=(d,p,m,_,x,b,R,E,S)=>{const w=p.el=d?d.el:l(""),F=p.anchor=d?d.anchor:l("");let{patchFlag:O,dynamicChildren:$,slotScopeIds:H}=p;H&&(E=E?E.concat(H):H),d==null?(s(w,m,_),s(F,m,_),Ke(p.children||[],m,F,x,b,R,E,S)):O>0&&O&64&&$&&d.dynamicChildren&&d.dynamicChildren.length===$.length?(Ge(d.dynamicChildren,$,m,x,b,R,E),(p.key!=null||x&&p===x.subTree)&&gi(d,p,!0)):J(d,p,m,F,x,b,R,E,S)},We=(d,p,m,_,x,b,R,E,S)=>{p.slotScopeIds=E,d==null?p.shapeFlag&512?x.ctx.activate(p,m,_,R,S):nn(p,m,_,x,b,R,S):$t(d,p,S)},nn=(d,p,m,_,x,b,R)=>{const E=d.component=Nc(d,_,x);if(Yo(d)&&(E.ctx.renderer=D),Mc(E,!1,R),E.asyncDep){if(x&&x.registerDep(E,me,R),!d.el){const S=E.subTree=W(mt);V(null,S,p,m),d.placeholder=S.el}}else me(E,d,p,m,x,b,R)},$t=(d,p,m)=>{const _=p.component=d.component;if(pc(d,p,m))if(_.asyncDep&&!_.asyncResolved){te(_,p,m);return}else _.next=p,_.update();else p.el=d.el,_.vnode=p},me=(d,p,m,_,x,b,R)=>{const E=()=>{if(d.isMounted){let{next:O,bu:$,u:H,parent:z,vnode:re}=d;{const Je=vi(d);if(Je){O&&(O.el=re.el,te(d,O,R)),Je.asyncDep.then(()=>{Ie(()=>{d.isUnmounted||w()},x)});return}}let ne=O,fe;Ot(d,!1),O?(O.el=re.el,te(d,O,R)):O=re,$&&Ln($),(fe=O.props&&O.props.onVnodeBeforeUpdate)&&Ye(fe,z,O,re),Ot(d,!0);const ge=Tr(d),ze=d.subTree;d.subTree=ge,A(ze,ge,f(ze.el),y(ze),d,x,b),O.el=ge.el,ne===null&&hc(d,ge.el),H&&Ie(H,x),(fe=O.props&&O.props.onVnodeUpdated)&&Ie(()=>Ye(fe,z,O,re),x)}else{let O;const{el:$,props:H}=p,{bm:z,m:re,parent:ne,root:fe,type:ge}=d,ze=qt(p);Ot(d,!1),z&&Ln(z),!ze&&(O=H&&H.onVnodeBeforeMount)&&Ye(O,ne,p),Ot(d,!0);{fe.ce&&fe.ce._hasShadowRoot()&&fe.ce._injectChildStyle(ge,d.parent?d.parent.type:void 0);const Je=d.subTree=Tr(d);A(null,Je,m,_,d,x,b),p.el=Je.el}if(re&&Ie(re,x),!ze&&(O=H&&H.onVnodeMounted)){const Je=p;Ie(()=>Ye(O,ne,Je),x)}(p.shapeFlag&256||ne&&qt(ne.vnode)&&ne.vnode.shapeFlag&256)&&d.a&&Ie(d.a,x),d.isMounted=!0,p=m=_=null}};d.scope.on();const S=d.effect=new Ao(E);d.scope.off();const w=d.update=S.run.bind(S),F=d.job=S.runIfDirty.bind(S);F.i=d,F.id=d.uid,S.scheduler=()=>cr(F),Ot(d,!0),w()},te=(d,p,m)=>{p.component=d;const _=d.vnode.props;d.vnode=p,d.next=null,gc(d,p.props,_,m),yc(d,p.children,m),st(),Sr(d),rt()},J=(d,p,m,_,x,b,R,E,S=!1)=>{const w=d&&d.children,F=d?d.shapeFlag:0,O=p.children,{patchFlag:$,shapeFlag:H}=p;if($>0){if($&128){_t(w,O,m,_,x,b,R,E,S);return}else if($&256){it(w,O,m,_,x,b,R,E,S);return}}H&8?(F&16&&De(w,x,b),O!==w&&a(m,O)):F&16?H&16?_t(w,O,m,_,x,b,R,E,S):De(w,x,b,!0):(F&8&&a(m,""),H&16&&Ke(O,m,_,x,b,R,E,S))},it=(d,p,m,_,x,b,R,E,S)=>{d=d||Kt,p=p||Kt;const w=d.length,F=p.length,O=Math.min(w,F);let $;for($=0;$F?De(d,x,b,!0,!1,O):Ke(p,m,_,x,b,R,E,S,O)},_t=(d,p,m,_,x,b,R,E,S)=>{let w=0;const F=p.length;let O=d.length-1,$=F-1;for(;w<=O&&w<=$;){const H=d[w],z=p[w]=S?ut(p[w]):et(p[w]);if(on(H,z))A(H,z,m,null,x,b,R,E,S);else break;w++}for(;w<=O&&w<=$;){const H=d[O],z=p[$]=S?ut(p[$]):et(p[$]);if(on(H,z))A(H,z,m,null,x,b,R,E,S);else break;O--,$--}if(w>O){if(w<=$){const H=$+1,z=H$)for(;w<=O;)Pe(d[w],x,b,!0),w++;else{const H=w,z=w,re=new Map;for(w=z;w<=$;w++){const Te=p[w]=S?ut(p[w]):et(p[w]);Te.key!=null&&re.set(Te.key,w)}let ne,fe=0;const ge=$-z+1;let ze=!1,Je=0;const sn=new Array(ge);for(w=0;w=ge){Pe(Te,x,b,!0);continue}let Qe;if(Te.key!=null)Qe=re.get(Te.key);else for(ne=z;ne<=$;ne++)if(sn[ne-z]===0&&on(Te,p[ne])){Qe=ne;break}Qe===void 0?Pe(Te,x,b,!0):(sn[Qe-z]=w+1,Qe>=Je?Je=Qe:ze=!0,A(Te,p[Qe],m,null,x,b,R,E,S),fe++)}const gr=ze?Ec(sn):Kt;for(ne=gr.length-1,w=ge-1;w>=0;w--){const Te=z+w,Qe=p[Te],vr=p[Te+1],br=Te+1{const{el:b,type:R,transition:E,children:S,shapeFlag:w}=d;if(w&6){qe(d.component.subTree,p,m,_);return}if(w&128){d.suspense.move(p,m,_);return}if(w&64){R.move(d,p,m,D);return}if(R===ce){s(b,p,m);for(let O=0;OE.enter(b),x));else{const{leave:O,delayLeave:$,afterLeave:H}=E,z=()=>{d.ctx.isUnmounted?r(b):s(b,p,m)},re=()=>{const ne=b._isLeaving||!!b[ws];b._isLeaving&&b[ws](!0),E.persisted&&!ne?z():O(b,()=>{z(),H&&H()})};$?$(b,z,re):re()}else s(b,p,m)},Pe=(d,p,m,_=!1,x=!1)=>{const{type:b,props:R,ref:E,children:S,dynamicChildren:w,shapeFlag:F,patchFlag:O,dirs:$,cacheIndex:H,memo:z}=d;if(O===-2&&(x=!1),E!=null&&(st(),hn(E,null,m,d,!0),rt()),H!=null&&(p.renderCache[H]=void 0),F&256){p.ctx.deactivate(d);return}const re=F&1&&$,ne=!qt(d);let fe;if(ne&&(fe=R&&R.onVnodeBeforeUnmount)&&Ye(fe,p,d),F&6)Rt(d.component,m,_);else{if(F&128){d.suspense.unmount(m,_);return}re&&Ct(d,null,p,"beforeUnmount"),F&64?d.type.remove(d,p,m,D,_):w&&!w.hasOnce&&(b!==ce||O>0&&O&64)?De(w,p,m,!1,!0):(b===ce&&O&384||!x&&F&16)&&De(S,p,m),_&&Lt(d)}const ge=z!=null&&H==null;(ne&&(fe=R&&R.onVnodeUnmounted)||re||ge)&&Ie(()=>{fe&&Ye(fe,p,d),re&&Ct(d,null,p,"unmounted"),ge&&(d.el=null)},m)},Lt=d=>{const{type:p,el:m,anchor:_,transition:x}=d;if(p===ce){Vt(m,_);return}if(p===As){k(d);return}const b=()=>{r(m),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(d.shapeFlag&1&&x&&!x.persisted){const{leave:R,delayLeave:E}=x,S=()=>R(m,b);E?E(d.el,b,S):S()}else b()},Vt=(d,p)=>{let m;for(;d!==p;)m=h(d),r(d),d=m;r(p)},Rt=(d,p,m)=>{const{bum:_,scope:x,job:b,subTree:R,um:E,m:S,a:w}=d;Mr(S),Mr(w),_&&Ln(_),x.stop(),b&&(b.flags|=8,Pe(R,d,p,m)),E&&Ie(E,p),Ie(()=>{d.isUnmounted=!0},p)},De=(d,p,m,_=!1,x=!1,b=0)=>{for(let R=b;R{if(d.shapeFlag&6)return y(d.component.subTree);if(d.shapeFlag&128)return d.suspense.next();const p=h(d.anchor||d.el),m=p&&p[Vl];return m?h(m):p};let I=!1;const C=(d,p,m)=>{let _;d==null?p._vnode&&(Pe(p._vnode,null,null,!0),_=p._vnode.component):A(p._vnode||null,d,p,null,null,null,m),p._vnode=d,I||(I=!0,Sr(_),Ko(),I=!1)},D={p:A,um:Pe,m:qe,r:Lt,mt:nn,mc:Ke,pc:J,pbc:Ge,n:y,o:e};return{render:C,hydrate:void 0,createApp:lc(C)}}function Es({type:e,props:t},n){return n==="svg"&&e==="foreignObject"||n==="mathml"&&e==="annotation-xml"&&t&&t.encoding&&t.encoding.includes("html")?void 0:n}function Ot({effect:e,job:t},n){n?(e.flags|=32,t.flags|=4):(e.flags&=-33,t.flags&=-5)}function Sc(e,t){return(!e||e&&!e.pendingBranch)&&t&&!t.persisted}function gi(e,t,n=!1){const s=e.children,r=t.children;if(U(s)&&U(r))for(let o=0;o>1,e[n[l]]0&&(t[s]=n[o-1]),n[o]=s)}}for(o=n.length,i=n[o-1];o-- >0;)n[o]=i,i=t[i];return n}function vi(e){const t=e.subTree.component;if(t)return t.asyncDep&&!t.asyncResolved?t:vi(t)}function Mr(e){if(e)for(let t=0;te.__isSuspense;function Ac(e,t){t&&t.pendingBranch?U(e)?t.effects.push(...e):t.effects.push(e):Ml(e)}const ce=Symbol.for("v-fgt"),fs=Symbol.for("v-txt"),mt=Symbol.for("v-cmt"),As=Symbol.for("v-stc"),gn=[];let ke=null;function N(e=!1){gn.push(ke=e?null:[])}function Rc(){gn.pop(),ke=gn[gn.length-1]||null}let xn=1;function qn(e,t=!1){xn+=e,e<0&&ke&&t&&(ke.hasOnce=!0)}function yi(e){return e.dynamicChildren=xn>0?ke||Kt:null,Rc(),xn>0&&ke&&ke.push(e),e}function L(e,t,n,s,r,o){return yi(g(e,t,n,s,r,o,!0))}function pt(e,t,n,s,r){return yi(W(e,t,n,s,r,!0))}function wn(e){return e?e.__v_isVNode===!0:!1}function on(e,t){return e.type===t.type&&e.key===t.key}const xi=({key:e})=>e??null,jn=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ae(e)||we(e)||B(e)?{i:be,r:e,k:t,f:!!n}:e:null);function g(e,t=null,n=null,s=0,r=null,o=e===ce?0:1,i=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&xi(t),ref:t&&jn(t),scopeId:Wo,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetStart:null,targetAnchor:null,staticCount:0,shapeFlag:o,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:be};return l?(zn(c,n),o&128&&e.normalize(c)):n&&(c.shapeFlag|=ae(n)?8:16),xn>0&&!i&&ke&&(c.patchFlag>0||o&6)&&c.patchFlag!==32&&ke.push(c),c}const W=Cc;function Cc(e,t=null,n=null,s=0,r=null,o=!1){if((!e||e===ei)&&(e=mt),wn(e)){const l=Xt(e,t,!0);return n&&zn(l,n),xn>0&&!o&&ke&&(l.shapeFlag&6?ke[ke.indexOf(e)]=l:ke.push(l)),l.patchFlag=-2,l}if(Fc(e)&&(e=e.__vccOpts),t){t=Oc(t);let{class:l,style:c}=t;l&&!ae(l)&&(t.class=On(l)),ee(c)&&(ir(c)&&!U(c)&&(c=_e({},c)),t.style=is(c))}const i=ae(e)?1:_i(e)?128:Fl(e)?64:ee(e)?4:B(e)?2:0;return g(e,t,n,s,r,i,o,!0)}function Oc(e){return e?ir(e)||ui(e)?_e({},e):e:null}function Xt(e,t,n=!1,s=!1){const{props:r,ref:o,patchFlag:i,children:l,transition:c}=e,u=t?Pc(r||{},t):r,a={__v_isVNode:!0,__v_skip:!0,type:e.type,props:u,key:u&&xi(u),ref:t&&t.ref?n&&o?U(o)?o.concat(jn(t)):[o,jn(t)]:jn(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetStart:e.targetStart,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==ce?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:c,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&Xt(e.ssContent),ssFallback:e.ssFallback&&Xt(e.ssFallback),placeholder:e.placeholder,el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce};return c&&s&&ar(a,c.clone(a)),a}function de(e=" ",t=0){return W(fs,null,e,t)}function Se(e="",t=!1){return t?(N(),pt(mt,null,e)):W(mt,null,e)}function et(e){return e==null||typeof e=="boolean"?W(mt):U(e)?W(ce,null,e.slice()):wn(e)?ut(e):W(fs,null,String(e))}function ut(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:Xt(e)}function zn(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(U(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),zn(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!ui(t)?t._ctx=be:r===3&&be&&(be.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else if(B(t)){if(s&65){zn(e,{default:t});return}t={default:t,_ctx:be},n=32}else t=String(t),s&64?(n=16,t=[de(t)]):n=8;e.children=t,e.shapeFlag|=n}function Pc(...e){const t={};for(let n=0;nxe||be;let Jn,js;{const e=os(),t=(n,s)=>{let r;return(r=e[n])||(r=e[n]=[]),r.push(s),o=>{r.length>1?r.forEach(i=>i(o)):r[0](o)}};Jn=t("__VUE_INSTANCE_SETTERS__",n=>xe=n),js=t("__VUE_SSR_SETTERS__",n=>Sn=n)}const Nn=e=>{const t=xe;return Jn(e),e.scope.on(),()=>{e.scope.off(),Jn(t)}},Dr=()=>{xe&&xe.scope.off(),Jn(null)};function wi(e){return e.vnode.shapeFlag&4}let Sn=!1;function Mc(e,t=!1,n=!1){t&&js(t);const{props:s,children:r}=e.vnode,o=wi(e);mc(e,s,o,t),_c(e,r,n||t);const i=o?Dc(e,t):void 0;return t&&js(!1),i}function Dc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=new Proxy(e.ctx,ec);const{setup:s}=n;if(s){st();const r=e.setupContext=s.length>1?Lc(e):null,o=Nn(e),i=Tn(s,e,0,[e.props,r]),l=bo(i);if(rt(),o(),(l||e.sp)&&!qt(e)&&Qo(e),l){if(i.then(Dr,Dr),t)return i.then(c=>{$r(e,c)}).catch(c=>{cs(c,e,0)});e.asyncDep=i}else $r(e,i)}else Si(e)}function $r(e,t,n){B(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ee(t)&&(e.setupState=Uo(t)),Si(e)}function Si(e,t,n){const s=e.type;e.render||(e.render=s.render||nt);{const r=Nn(e);st();try{tc(e)}finally{rt(),r()}}}const $c={get(e,t){return ye(e,"get",""),e[t]}};function Lc(e){const t=n=>{e.exposed=n||{}};return{attrs:new Proxy(e.attrs,$c),slots:e.slots,emit:e.emit,expose:t}}function ds(e){return e.exposed?e.exposeProxy||(e.exposeProxy=new Proxy(Uo(El(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in mn)return mn[n](e)},has(t,n){return n in t||n in mn}})):e.proxy}function Vc(e,t=!0){return B(e)?e.displayName||e.name:e.name||t&&e.__name}function Fc(e){return B(e)&&"__vccOpts"in e}const $e=(e,t)=>Pl(e,t,Sn);function En(e,t,n){try{qn(-1);const s=arguments.length;return s===2?ee(t)&&!U(t)?wn(t)?W(e,null,[t]):W(e,t):W(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&wn(n)&&(n=[n]),W(e,t,n))}finally{qn(1)}}const jc="3.5.39";/** +* @vue/runtime-dom v3.5.39 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**/let Us;const Lr=typeof window<"u"&&window.trustedTypes;if(Lr)try{Us=Lr.createPolicy("vue",{createHTML:e=>e})}catch{}const Ei=Us?e=>Us.createHTML(e):e=>e,Uc="http://www.w3.org/2000/svg",Hc="http://www.w3.org/1998/Math/MathML",at=typeof document<"u"?document:null,Vr=at&&at.createElement("template"),Bc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t==="svg"?at.createElementNS(Uc,e):t==="mathml"?at.createElementNS(Hc,e):n?at.createElement(e,{is:n}):at.createElement(e);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>at.createTextNode(e),createComment:e=>at.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>at.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,o){const i=n?n.previousSibling:t.lastChild;if(r&&(r===o||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===o||!(r=r.nextSibling)););else{Vr.innerHTML=Ei(s==="svg"?`${e}`:s==="mathml"?`${e}`:e);const l=Vr.content;if(s==="svg"||s==="mathml"){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[i?i.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}},Kc=Symbol("_vtc");function Gc(e,t,n){const s=e[Kc];s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}const Qn=Symbol("_vod"),Ai=Symbol("_vsh"),Wc={name:"show",beforeMount(e,{value:t},{transition:n}){e[Qn]=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):ln(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:s}){!t!=!n&&(s?t?(s.beforeEnter(e),ln(e,!0),s.enter(e)):s.leave(e,()=>{ln(e,!1)}):ln(e,t))},beforeUnmount(e,{value:t}){ln(e,t)}};function ln(e,t){e.style.display=t?e[Qn]:"none",e[Ai]=!t}const qc=Symbol(""),zc=/(?:^|;)\s*display\s*:/;function Jc(e,t,n){const s=e.style,r=ae(n);let o=!1;if(n&&!r){if(t)if(ae(t))for(const i of t.split(";")){const l=i.slice(0,i.indexOf(":")).trim();n[l]==null&&un(s,l,"")}else for(const i in t)n[i]==null&&un(s,i,"");for(const i in n){i==="display"&&(o=!0);const l=n[i];l!=null?Yc(e,i,!ae(t)&&t?t[i]:void 0,l)||un(s,i,l):un(s,i,"")}}else if(r){if(t!==n){const i=s[qc];i&&(n+=";"+i),s.cssText=n,o=zc.test(n)}}else t&&e.removeAttribute("style");Qn in e&&(e[Qn]=o?s.display:"",e[Ai]&&(s.display="none"))}const Fr=/\s*!important$/;function un(e,t,n){if(U(n))n.forEach(s=>un(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=Qc(e,t);Fr.test(n)?e.setProperty(Dt(s),n.replace(Fr,""),"important"):e[s]=n}}const jr=["Webkit","Moz","ms"],Rs={};function Qc(e,t){const n=Rs[t];if(n)return n;let s=Oe(t);if(s!=="filter"&&s in e)return Rs[t]=s;s=ss(s);for(let r=0;rCs||(sa.then(()=>Cs=0),Cs=Date.now());function oa(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;const r=n.value;if(U(r)){const o=s.stopImmediatePropagation;s.stopImmediatePropagation=()=>{o.call(s),s._stopped=!0};const i=r.slice(),l=[s];for(let c=0;ce.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,ia=(e,t,n,s,r,o)=>{const i=r==="svg";t==="class"?Gc(e,s,i):t==="style"?Jc(e,n,s):Zn(t)?es(t)||Zc(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):la(e,t,s,i))?(Br(e,t,s),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&Hr(e,t,s,i,o,t!=="value")):e._isVueCE&&(ca(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!ae(s)))?Br(e,Oe(t),s,o,t):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),Hr(e,t,s,i))};function la(e,t,n,s){if(s)return!!(t==="innerHTML"||t==="textContent"||t in e&&Gr(t)&&B(n));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const r=e.tagName;if(r==="IMG"||r==="VIDEO"||r==="CANVAS"||r==="SOURCE")return!1}return Gr(t)&&ae(n)?!1:t in e}function ca(e,t){const n=e._def.props;if(!n)return!1;const s=Oe(t);return Array.isArray(n)?n.some(r=>Oe(r)===s):Object.keys(n).some(r=>Oe(r)===s)}const Yn=e=>{const t=e.props["onUpdate:modelValue"]||!1;return U(t)?n=>Ln(t,n):t};function aa(e){e.target.composing=!0}function Wr(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const Jt=Symbol("_assign");function qr(e,t,n){return t&&(e=e.trim()),n&&(e=rs(e)),e}const St={created(e,{modifiers:{lazy:t,trim:n,number:s}},r){e[Jt]=Yn(r);const o=s||r.props&&r.props.type==="number";It(e,t?"change":"input",i=>{i.target.composing||e[Jt](qr(e.value,n,o))}),(n||o)&&It(e,"change",()=>{e.value=qr(e.value,n,o)}),t||(It(e,"compositionstart",aa),It(e,"compositionend",Wr),It(e,"change",Wr))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:s,trim:r,number:o}},i){if(e[Jt]=Yn(i),e.composing)return;const l=(o||e.type==="number")&&!/^0\d/.test(e.value)?rs(e.value):e.value,c=t??"";if(l===c)return;const u=e.getRootNode();(u instanceof Document||u instanceof ShadowRoot)&&u.activeElement===e&&e.type!=="range"&&(s&&t===n||r&&e.value.trim()===c)||(e.value=c)}},ps={deep:!0,created(e,{value:t,modifiers:{number:n}},s){const r=ts(t);It(e,"change",()=>{const o=Array.prototype.filter.call(e.options,i=>i.selected).map(i=>n?rs(Xn(i)):Xn(i));e[Jt](e.multiple?r?new Set(o):o:o[0]),e._assigning=!0,lr(()=>{e._assigning=!1})}),e[Jt]=Yn(s)},mounted(e,{value:t}){zr(e,t)},beforeUpdate(e,t,n){e[Jt]=Yn(n)},updated(e,{value:t}){e._assigning||zr(e,t)}};function zr(e,t){const n=e.multiple,s=U(t);if(!(n&&!s&&!ts(t))){for(let r=0,o=e.options.length;rString(u)===String(l)):i.selected=rl(t,l)>-1}else i.selected=t.has(l);else if(Pn(Xn(i),t)){e.selectedIndex!==r&&(e.selectedIndex=r);return}}!n&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Xn(e){return"_value"in e?e._value:e.value}const ua=["ctrl","shift","alt","meta"],fa={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>ua.some(n=>e[`${n}Key`]&&!t.includes(n))},hs=(e,t)=>{if(!e)return e;const n=e._withMods||(e._withMods={}),s=t.join(".");return n[s]||(n[s]=((r,...o)=>{for(let i=0;i{const t=pa().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=ga(s);if(!r)return;const o=t._component;!B(o)&&!o.render&&!o.template&&(o.template=r.innerHTML),r.nodeType===1&&(r.textContent="");const i=n(r,!1,ma(r));return r instanceof Element&&(r.removeAttribute("v-cloak"),r.setAttribute("data-v-app","")),i},t});function ma(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function ga(e){return ae(e)?document.querySelector(e):e}const va=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},ba={};function _a(e,t){const n=Yl("router-view");return N(),pt(n)}const ya=va(ba,[["render",_a]]);/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */const Bt=typeof document<"u";function Ri(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function xa(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Ri(e.default)}const Q=Object.assign;function Os(e,t){const n={};for(const s in t){const r=t[s];n[s]=Be(r)?r.map(e):e(r)}return n}const vn=()=>{},Be=Array.isArray;function Qr(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const Ci=/#/g,wa=/&/g,Sa=/\//g,Ea=/=/g,Aa=/\?/g,Oi=/\+/g,Ra=/%5B/g,Ca=/%5D/g,Pi=/%5E/g,Oa=/%60/g,Ii=/%7B/g,Pa=/%7C/g,Ti=/%7D/g,Ia=/%20/g;function pr(e){return e==null?"":encodeURI(""+e).replace(Pa,"|").replace(Ra,"[").replace(Ca,"]")}function Ta(e){return pr(e).replace(Ii,"{").replace(Ti,"}").replace(Pi,"^")}function Hs(e){return pr(e).replace(Oi,"%2B").replace(Ia,"+").replace(Ci,"%23").replace(wa,"%26").replace(Oa,"`").replace(Ii,"{").replace(Ti,"}").replace(Pi,"^")}function Na(e){return Hs(e).replace(Ea,"%3D")}function ka(e){return pr(e).replace(Ci,"%23").replace(Aa,"%3F")}function Ma(e){return ka(e).replace(Sa,"%2F")}function An(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const Da=/\/$/,$a=e=>e.replace(Da,"");function Ps(e,t,n="/"){let s,r={},o="",i="";const l=t.indexOf("#");let c=t.indexOf("?");return c=l>=0&&c>l?-1:c,c>=0&&(s=t.slice(0,c),o=t.slice(c,l>0?l:t.length),r=e(o.slice(1))),l>=0&&(s=s||t.slice(0,l),i=t.slice(l,t.length)),s=ja(s??t,n),{fullPath:s+o+i,path:s,query:r,hash:An(i)}}function La(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Yr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Va(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&Zt(t.matched[s],n.matched[r])&&Ni(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function Zt(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Ni(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Fa(e[n],t[n]))return!1;return!0}function Fa(e,t){return Be(e)?Xr(e,t):Be(t)?Xr(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function Xr(e,t){return Be(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function ja(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let o=n.length-1,i,l;for(i=0;i1&&o--;else break;return n.slice(0,o).join("/")+"/"+s.slice(i).join("/")}const yt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0};let Bs=(function(e){return e.pop="pop",e.push="push",e})({}),Is=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function Ua(e){if(!e)if(Bt){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),$a(e)}const Ha=/^[^#]+#/;function Ba(e,t){return e.replace(Ha,"#")+t}function Ka(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const ms=()=>({left:window.scrollX,top:window.scrollY});function Ga(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=Ka(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.scrollX,t.top!=null?t.top:window.scrollY)}function Zr(e,t){return(history.state?history.state.position-t:-1)+e}const Ks=new Map;function Wa(e,t){Ks.set(e,t)}function qa(e){const t=Ks.get(e);return Ks.delete(e),t}function za(e){return typeof e=="string"||e&&typeof e=="object"}function ki(e){return typeof e=="string"||typeof e=="symbol"}let ue=(function(e){return e[e.MATCHER_NOT_FOUND=1]="MATCHER_NOT_FOUND",e[e.NAVIGATION_GUARD_REDIRECT=2]="NAVIGATION_GUARD_REDIRECT",e[e.NAVIGATION_ABORTED=4]="NAVIGATION_ABORTED",e[e.NAVIGATION_CANCELLED=8]="NAVIGATION_CANCELLED",e[e.NAVIGATION_DUPLICATED=16]="NAVIGATION_DUPLICATED",e})({});const Mi=Symbol("");ue.MATCHER_NOT_FOUND+"",ue.NAVIGATION_GUARD_REDIRECT+"",ue.NAVIGATION_ABORTED+"",ue.NAVIGATION_CANCELLED+"",ue.NAVIGATION_DUPLICATED+"";function en(e,t){return Q(new Error,{type:e,[Mi]:!0},t)}function ct(e,t){return e instanceof Error&&Mi in e&&(t==null||!!(e.type&t))}const Ja=["params","query","hash"];function Qa(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;const t={};for(const n of Ja)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function Ya(e){const t={};if(e===""||e==="?")return t;const n=(e[0]==="?"?e.slice(1):e).split("&");for(let s=0;sr&&Hs(r)):[s&&Hs(s)]).forEach(r=>{r!==void 0&&(t+=(t.length?"&":"")+n,r!=null&&(t+="="+r))})}return t}function Xa(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=Be(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const Za=Symbol(""),to=Symbol(""),gs=Symbol(""),hr=Symbol(""),Gs=Symbol("");function cn(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function wt(e,t,n,s,r,o=i=>i()){const i=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((l,c)=>{const u=h=>{h===!1?c(en(ue.NAVIGATION_ABORTED,{from:n,to:t})):h instanceof Error?c(h):za(h)?c(en(ue.NAVIGATION_GUARD_REDIRECT,{from:t,to:h})):(i&&s.enterCallbacks[r]===i&&typeof h=="function"&&i.push(h),l())},a=o(()=>e.call(s&&s.instances[r],t,n,u));let f=Promise.resolve(a);e.length<3&&(f=f.then(u)),f.catch(h=>c(h))})}function Ts(e,t,n,s,r=o=>o()){const o=[];for(const i of e)for(const l in i.components){let c=i.components[l];if(!(t!=="beforeRouteEnter"&&!i.instances[l]))if(Ri(c)){const u=(c.__vccOpts||c)[t];u&&o.push(wt(u,n,s,i,l,r))}else{let u=c();o.push(()=>u.then(a=>{if(!a)throw new Error(`Couldn't resolve component "${l}" at "${i.path}"`);const f=xa(a)?a.default:a;i.mods[l]=a,i.components[l]=f;const h=(f.__vccOpts||f)[t];return h&&wt(h,n,s,i,l,r)()}))}}return o}function eu(e,t){const n=[],s=[],r=[],o=Math.max(t.matched.length,e.matched.length);for(let i=0;iZt(u,l))?s.push(l):n.push(l));const c=e.matched[i];c&&(t.matched.find(u=>Zt(u,c))||r.push(c))}return[n,s,r]}/*! + * vue-router v4.6.4 + * (c) 2025 Eduardo San Martin Morote + * @license MIT + */let tu=()=>location.protocol+"//"+location.host;function Di(e,t){const{pathname:n,search:s,hash:r}=t,o=e.indexOf("#");if(o>-1){let i=r.includes(e.slice(o))?e.slice(o).length:1,l=r.slice(i);return l[0]!=="/"&&(l="/"+l),Yr(l,"")}return Yr(n,e)+s+r}function nu(e,t,n,s){let r=[],o=[],i=null;const l=({state:h})=>{const v=Di(e,location),T=n.value,A=t.value;let j=0;if(h){if(n.value=v,t.value=h,i&&i===T){i=null;return}j=A?h.position-A.position:0}else s(v);r.forEach(V=>{V(n.value,T,{delta:j,type:Bs.pop,direction:j?j>0?Is.forward:Is.back:Is.unknown})})};function c(){i=n.value}function u(h){r.push(h);const v=()=>{const T=r.indexOf(h);T>-1&&r.splice(T,1)};return o.push(v),v}function a(){if(document.visibilityState==="hidden"){const{history:h}=window;if(!h.state)return;h.replaceState(Q({},h.state,{scroll:ms()}),"")}}function f(){for(const h of o)h();o=[],window.removeEventListener("popstate",l),window.removeEventListener("pagehide",a),document.removeEventListener("visibilitychange",a)}return window.addEventListener("popstate",l),window.addEventListener("pagehide",a),document.addEventListener("visibilitychange",a),{pauseListeners:c,listen:u,destroy:f}}function no(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?ms():null}}function su(e){const{history:t,location:n}=window,s={value:Di(e,n)},r={value:t.state};r.value||o(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function o(c,u,a){const f=e.indexOf("#"),h=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+c:tu()+e+c;try{t[a?"replaceState":"pushState"](u,"",h),r.value=u}catch(v){console.error(v),n[a?"replace":"assign"](h)}}function i(c,u){o(c,Q({},t.state,no(r.value.back,c,r.value.forward,!0),u,{position:r.value.position}),!0),s.value=c}function l(c,u){const a=Q({},r.value,t.state,{forward:c,scroll:ms()});o(a.current,a,!0),o(c,Q({},no(s.value,c,null),{position:a.position+1},u),!1),s.value=c}return{location:s,state:r,push:l,replace:i}}function ru(e){e=Ua(e);const t=su(e),n=nu(e,t.state,t.location,t.replace);function s(o,i=!0){i||n.pauseListeners(),history.go(o)}const r=Q({location:"",base:e,go:s,createHref:Ba.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}let Tt=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.Group=2]="Group",e})({});var he=(function(e){return e[e.Static=0]="Static",e[e.Param=1]="Param",e[e.ParamRegExp=2]="ParamRegExp",e[e.ParamRegExpEnd=3]="ParamRegExpEnd",e[e.EscapeNext=4]="EscapeNext",e})(he||{});const ou={type:Tt.Static,value:""},iu=/[a-zA-Z0-9_]/;function lu(e){if(!e)return[[]];if(e==="/")return[[ou]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(v){throw new Error(`ERR (${n})/"${u}": ${v}`)}let n=he.Static,s=n;const r=[];let o;function i(){o&&r.push(o),o=[]}let l=0,c,u="",a="";function f(){u&&(n===he.Static?o.push({type:Tt.Static,value:u}):n===he.Param||n===he.ParamRegExp||n===he.ParamRegExpEnd?(o.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${u}) must be alone in its segment. eg: '/:ids+.`),o.push({type:Tt.Param,value:u,regexp:a,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),u="")}function h(){u+=c}for(;lt.length?t.length===1&&t[0]===Re.Static+Re.Segment?1:-1:0}function $i(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const du={strict:!1,end:!0,sensitive:!1};function pu(e,t,n){const s=uu(lu(e.path),n),r=Q(s,{record:e,parent:t,children:[],alias:[]});return t&&!r.record.aliasOf==!t.record.aliasOf&&t.children.push(r),r}function hu(e,t){const n=[],s=new Map;t=Qr(du,t);function r(f){return s.get(f)}function o(f,h,v){const T=!v,A=io(f);A.aliasOf=v&&v.record;const j=Qr(t,f),V=[A];if("alias"in f){const k=typeof f.alias=="string"?[f.alias]:f.alias;for(const se of k)V.push(io(Q({},A,{components:v?v.record.components:A.components,path:se,aliasOf:v?v.record:A})))}let P,M;for(const k of V){const{path:se}=k;if(h&&se[0]!=="/"){const pe=h.record.path,le=pe[pe.length-1]==="/"?"":"/";k.path=h.record.path+(se&&le+se)}if(P=pu(k,h,j),v?v.alias.push(P):(M=M||P,M!==P&&M.alias.push(P),T&&f.name&&!lo(P)&&i(f.name)),Li(P)&&c(P),A.children){const pe=A.children;for(let le=0;le{i(M)}:vn}function i(f){if(ki(f)){const h=s.get(f);h&&(s.delete(f),n.splice(n.indexOf(h),1),h.children.forEach(i),h.alias.forEach(i))}else{const h=n.indexOf(f);h>-1&&(n.splice(h,1),f.record.name&&s.delete(f.record.name),f.children.forEach(i),f.alias.forEach(i))}}function l(){return n}function c(f){const h=vu(f,n);n.splice(h,0,f),f.record.name&&!lo(f)&&s.set(f.record.name,f)}function u(f,h){let v,T={},A,j;if("name"in f&&f.name){if(v=s.get(f.name),!v)throw en(ue.MATCHER_NOT_FOUND,{location:f});j=v.record.name,T=Q(oo(h.params,v.keys.filter(M=>!M.optional).concat(v.parent?v.parent.keys.filter(M=>M.optional):[]).map(M=>M.name)),f.params&&oo(f.params,v.keys.map(M=>M.name))),A=v.stringify(T)}else if(f.path!=null)A=f.path,v=n.find(M=>M.re.test(A)),v&&(T=v.parse(A),j=v.record.name);else{if(v=h.name?s.get(h.name):n.find(M=>M.re.test(h.path)),!v)throw en(ue.MATCHER_NOT_FOUND,{location:f,currentLocation:h});j=v.record.name,T=Q({},h.params,f.params),A=v.stringify(T)}const V=[];let P=v;for(;P;)V.unshift(P.record),P=P.parent;return{name:j,path:A,params:T,matched:V,meta:gu(V)}}e.forEach(f=>o(f));function a(){n.length=0,s.clear()}return{addRoute:o,resolve:u,removeRoute:i,clearRoutes:a,getRoutes:l,getRecordMatcher:r}}function oo(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function io(e){const t={path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:e.aliasOf,beforeEnter:e.beforeEnter,props:mu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}};return Object.defineProperty(t,"mods",{value:{}}),t}function mu(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="object"?n[s]:n;return t}function lo(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function gu(e){return e.reduce((t,n)=>Q(t,n.meta),{})}function vu(e,t){let n=0,s=t.length;for(;n!==s;){const o=n+s>>1;$i(e,t[o])<0?s=o:n=o+1}const r=bu(e);return r&&(s=t.lastIndexOf(r,s-1)),s}function bu(e){let t=e;for(;t=t.parent;)if(Li(t)&&$i(e,t)===0)return t}function Li({record:e}){return!!(e.name||e.components&&Object.keys(e.components).length||e.redirect)}function co(e){const t=Ve(gs),n=Ve(hr),s=$e(()=>{const c=Z(e.to);return t.resolve(c)}),r=$e(()=>{const{matched:c}=s.value,{length:u}=c,a=c[u-1],f=n.matched;if(!a||!f.length)return-1;const h=f.findIndex(Zt.bind(null,a));if(h>-1)return h;const v=ao(c[u-2]);return u>1&&ao(a)===v&&f[f.length-1].path!==v?f.findIndex(Zt.bind(null,c[u-2])):h}),o=$e(()=>r.value>-1&&wu(n.params,s.value.params)),i=$e(()=>r.value>-1&&r.value===n.matched.length-1&&Ni(n.params,s.value.params));function l(c={}){if(xu(c)){const u=t[Z(e.replace)?"replace":"push"](Z(e.to)).catch(vn);return e.viewTransition&&typeof document<"u"&&"startViewTransition"in document&&document.startViewTransition(()=>u),u}return Promise.resolve()}return{route:s,href:$e(()=>s.value.href),isActive:o,isExactActive:i,navigate:l}}function _u(e){return e.length===1?e[0]:e}const yu=Jo({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"},viewTransition:Boolean},useLink:co,setup(e,{slots:t}){const n=In(co(e)),{options:s}=Ve(gs),r=$e(()=>({[uo(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[uo(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const o=t.default&&_u(t.default(n));return e.custom?o:En("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},o)}}}),Ws=yu;function xu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function wu(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!Be(r)||r.length!==s.length||s.some((o,i)=>o.valueOf()!==r[i].valueOf()))return!1}return!0}function ao(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const uo=(e,t,n)=>e??t??n,Su=Jo({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=Ve(Gs),r=$e(()=>e.route||s.value),o=Ve(to,0),i=$e(()=>{let u=Z(o);const{matched:a}=r.value;let f;for(;(f=a[u])&&!f.components;)u++;return u}),l=$e(()=>r.value.matched[i.value]);Vn(to,$e(()=>i.value+1)),Vn(Za,l),Vn(Gs,r);const c=G();return Fn(()=>[c.value,l.value,e.name],([u,a,f],[h,v,T])=>{a&&(a.instances[f]=u,v&&v!==a&&u&&u===h&&(a.leaveGuards.size||(a.leaveGuards=v.leaveGuards),a.updateGuards.size||(a.updateGuards=v.updateGuards))),u&&a&&(!v||!Zt(a,v)||!h)&&(a.enterCallbacks[f]||[]).forEach(A=>A(u))},{flush:"post"}),()=>{const u=r.value,a=e.name,f=l.value,h=f&&f.components[a];if(!h)return fo(n.default,{Component:h,route:u});const v=f.props[a],T=v?v===!0?u.params:typeof v=="function"?v(u):v:null,j=En(h,Q({},T,t,{onVnodeUnmounted:V=>{V.component.isUnmounted&&(f.instances[a]=null)},ref:c}));return fo(n.default,{Component:j,route:u})||j}}});function fo(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Vi=Su;function Eu(e){const t=hu(e.routes,e),n=e.parseQuery||Ya,s=e.stringifyQuery||eo,r=e.history,o=cn(),i=cn(),l=cn(),c=Al(yt);let u=yt;Bt&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const a=Os.bind(null,y=>""+y),f=Os.bind(null,Ma),h=Os.bind(null,An);function v(y,I){let C,D;return ki(y)?(C=t.getRecordMatcher(y),D=I):D=y,t.addRoute(D,C)}function T(y){const I=t.getRecordMatcher(y);I&&t.removeRoute(I)}function A(){return t.getRoutes().map(y=>y.record)}function j(y){return!!t.getRecordMatcher(y)}function V(y,I){if(I=Q({},I||c.value),typeof y=="string"){const m=Ps(n,y,I.path),_=t.resolve({path:m.path},I),x=r.createHref(m.fullPath);return Q(m,_,{params:h(_.params),hash:An(m.hash),redirectedFrom:void 0,href:x})}let C;if(y.path!=null)C=Q({},y,{path:Ps(n,y.path,I.path).path});else{const m=Q({},y.params);for(const _ in m)m[_]==null&&delete m[_];C=Q({},y,{params:f(m)}),I.params=f(I.params)}const D=t.resolve(C,I),q=y.hash||"";D.params=a(h(D.params));const d=La(s,Q({},y,{hash:Ta(q),path:D.path})),p=r.createHref(d);return Q({fullPath:d,hash:q,query:s===eo?Xa(y.query):y.query||{}},D,{redirectedFrom:void 0,href:p})}function P(y){return typeof y=="string"?Ps(n,y,c.value.path):Q({},y)}function M(y,I){if(u!==y)return en(ue.NAVIGATION_CANCELLED,{from:I,to:y})}function k(y){return le(y)}function se(y){return k(Q(P(y),{replace:!0}))}function pe(y,I){const C=y.matched[y.matched.length-1];if(C&&C.redirect){const{redirect:D}=C;let q=typeof D=="function"?D(y,I):D;return typeof q=="string"&&(q=q.includes("?")||q.includes("#")?q=P(q):{path:q},q.params={}),Q({query:y.query,hash:y.hash,params:q.path!=null?{}:y.params},q)}}function le(y,I){const C=u=V(y),D=c.value,q=y.state,d=y.force,p=y.replace===!0,m=pe(C,D);if(m)return le(Q(P(m),{state:typeof m=="object"?Q({},q,m.state):q,force:d,replace:p}),I||C);const _=C;_.redirectedFrom=I;let x;return!d&&Va(s,D,C)&&(x=en(ue.NAVIGATION_DUPLICATED,{to:_,from:D}),qe(D,D,!0,!1)),(x?Promise.resolve(x):Ge(_,D)).catch(b=>ct(b)?ct(b,ue.NAVIGATION_GUARD_REDIRECT)?b:_t(b):J(b,_,D)).then(b=>{if(b){if(ct(b,ue.NAVIGATION_GUARD_REDIRECT))return le(Q({replace:p},P(b.to),{state:typeof b.to=="object"?Q({},q,b.to.state):q,force:d}),I||_)}else b=At(_,D,!0,p,q);return bt(_,D,b),b})}function Ke(y,I){const C=M(y,I);return C?Promise.reject(C):Promise.resolve()}function vt(y){const I=Vt.values().next().value;return I&&typeof I.runWithContext=="function"?I.runWithContext(y):y()}function Ge(y,I){let C;const[D,q,d]=eu(y,I);C=Ts(D.reverse(),"beforeRouteLeave",y,I);for(const m of D)m.leaveGuards.forEach(_=>{C.push(wt(_,y,I))});const p=Ke.bind(null,y,I);return C.push(p),De(C).then(()=>{C=[];for(const m of o.list())C.push(wt(m,y,I));return C.push(p),De(C)}).then(()=>{C=Ts(q,"beforeRouteUpdate",y,I);for(const m of q)m.updateGuards.forEach(_=>{C.push(wt(_,y,I))});return C.push(p),De(C)}).then(()=>{C=[];for(const m of d)if(m.beforeEnter)if(Be(m.beforeEnter))for(const _ of m.beforeEnter)C.push(wt(_,y,I));else C.push(wt(m.beforeEnter,y,I));return C.push(p),De(C)}).then(()=>(y.matched.forEach(m=>m.enterCallbacks={}),C=Ts(d,"beforeRouteEnter",y,I,vt),C.push(p),De(C))).then(()=>{C=[];for(const m of i.list())C.push(wt(m,y,I));return C.push(p),De(C)}).catch(m=>ct(m,ue.NAVIGATION_CANCELLED)?m:Promise.reject(m))}function bt(y,I,C){l.list().forEach(D=>vt(()=>D(y,I,C)))}function At(y,I,C,D,q){const d=M(y,I);if(d)return d;const p=I===yt,m=Bt?history.state:{};C&&(D||p?r.replace(y.fullPath,Q({scroll:p&&m&&m.scroll},q)):r.push(y.fullPath,q)),c.value=y,qe(y,I,C,p),_t()}let We;function nn(){We||(We=r.listen((y,I,C)=>{if(!Rt.listening)return;const D=V(y),q=pe(D,Rt.currentRoute.value);if(q){le(Q(q,{replace:!0,force:!0}),D).catch(vn);return}u=D;const d=c.value;Bt&&Wa(Zr(d.fullPath,C.delta),ms()),Ge(D,d).catch(p=>ct(p,ue.NAVIGATION_ABORTED|ue.NAVIGATION_CANCELLED)?p:ct(p,ue.NAVIGATION_GUARD_REDIRECT)?(le(Q(P(p.to),{force:!0}),D).then(m=>{ct(m,ue.NAVIGATION_ABORTED|ue.NAVIGATION_DUPLICATED)&&!C.delta&&C.type===Bs.pop&&r.go(-1,!1)}).catch(vn),Promise.reject()):(C.delta&&r.go(-C.delta,!1),J(p,D,d))).then(p=>{p=p||At(D,d,!1),p&&(C.delta&&!ct(p,ue.NAVIGATION_CANCELLED)?r.go(-C.delta,!1):C.type===Bs.pop&&ct(p,ue.NAVIGATION_ABORTED|ue.NAVIGATION_DUPLICATED)&&r.go(-1,!1)),bt(D,d,p)}).catch(vn)}))}let $t=cn(),me=cn(),te;function J(y,I,C){_t(y);const D=me.list();return D.length?D.forEach(q=>q(y,I,C)):console.error(y),Promise.reject(y)}function it(){return te&&c.value!==yt?Promise.resolve():new Promise((y,I)=>{$t.add([y,I])})}function _t(y){return te||(te=!y,nn(),$t.list().forEach(([I,C])=>y?C(y):I()),$t.reset()),y}function qe(y,I,C,D){const{scrollBehavior:q}=e;if(!Bt||!q)return Promise.resolve();const d=!C&&qa(Zr(y.fullPath,0))||(D||!C)&&history.state&&history.state.scroll||null;return lr().then(()=>q(y,I,d)).then(p=>p&&Ga(p)).catch(p=>J(p,y,I))}const Pe=y=>r.go(y);let Lt;const Vt=new Set,Rt={currentRoute:c,listening:!0,addRoute:v,removeRoute:T,clearRoutes:t.clearRoutes,hasRoute:j,getRoutes:A,resolve:V,options:e,push:k,replace:se,go:Pe,back:()=>Pe(-1),forward:()=>Pe(1),beforeEach:o.add,beforeResolve:i.add,afterEach:l.add,onError:me.add,isReady:it,install(y){y.component("RouterLink",Ws),y.component("RouterView",Vi),y.config.globalProperties.$router=Rt,Object.defineProperty(y.config.globalProperties,"$route",{enumerable:!0,get:()=>Z(c)}),Bt&&!Lt&&c.value===yt&&(Lt=!0,k(r.location).catch(D=>{}));const I={};for(const D in yt)Object.defineProperty(I,D,{get:()=>c.value[D],enumerable:!0});y.provide(gs,Rt),y.provide(hr,Fo(I)),y.provide(Gs,c);const C=y.unmount;Vt.add(y),y.unmount=function(){Vt.delete(y),Vt.size<1&&(u=yt,We&&We(),We=null,c.value=yt,Lt=!1,te=!1),C()}}};function De(y){return y.reduce((I,C)=>I.then(()=>vt(C)),Promise.resolve())}return Rt}function Fi(){return Ve(gs)}function Au(e){return Ve(hr)}const qs="sms_gw_token",zs="sms_gw_api_base";function ji(){return localStorage.getItem(qs)||""}function po(e){e?localStorage.setItem(qs,e):localStorage.removeItem(qs)}function Js(){return localStorage.getItem(zs)||""}function Ru(e){const t=(e||"").trim().replace(/\/+$/,"");t?localStorage.setItem(zs,t):localStorage.removeItem(zs)}class Cu extends Error{constructor(t,n){super(n),this.status=t}}async function $n(e,t,n){const s={},r=ji();r&&(s.Authorization="Bearer "+r),n!==void 0&&(s["Content-Type"]="application/json");const o=await fetch(Js()+"/api"+t,{method:e,headers:s,body:n!==void 0?JSON.stringify(n):void 0}),i=await o.text();let l=null;try{l=i?JSON.parse(i):null}catch{l=i}if(!o.ok){const c=l&&l.error||o.statusText||"Request failed";throw new Cu(o.status,c)}return l}const Me={get:e=>$n("GET",e),post:(e,t)=>$n("POST",e,t),patch:(e,t)=>$n("PATCH",e,t),del:e=>$n("DELETE",e)},Qs="sms_gw_user",jt=In({user:JSON.parse(localStorage.getItem(Qs)||"null"),token:ji()}),Rn={state:Un(jt),isAuthenticated(){return!!jt.token},async login(e,t){const n=await Me.post("/auth/login",{email:e,password:t});return jt.token=n.access_token,jt.user=n.user,po(n.access_token),localStorage.setItem(Qs,JSON.stringify(n.user)),n.user},logout(){jt.token="",jt.user=null,po(""),localStorage.removeItem(Qs)}};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ho=e=>e==="";/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ou=(...e)=>e.filter((t,n,s)=>!!t&&t.trim()!==""&&s.indexOf(t)===n).join(" ").trim();/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const mo=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase();/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Pu=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,n,s)=>s?s.toUpperCase():n.toLowerCase());/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Iu=e=>{const t=Pu(e);return t.charAt(0).toUpperCase()+t.slice(1)};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var Ut={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Tu=Symbol("lucide-icons");function Nu(){return Ve(Tu,{})}/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ku=({name:e,iconNode:t,absoluteStrokeWidth:n,"absolute-stroke-width":s,strokeWidth:r,"stroke-width":o,size:i,color:l,...c},{slots:u})=>{const{size:a,color:f,strokeWidth:h=2,absoluteStrokeWidth:v=!1,class:T=""}=Nu(),A=$e(()=>{const j=ho(n)||ho(s)||n===!0||s===!0||v===!0,V=r||o||h||Ut["stroke-width"];return j?Number(V)*24/Number(i??a??Ut.width):V});return En("svg",{...Ut,...c,width:i??a??Ut.width,height:i??a??Ut.height,stroke:l??f??Ut.stroke,"stroke-width":A.value,class:Ou("lucide",T,...e?[`lucide-${mo(Iu(e))}-icon`,`lucide-${mo(e)}`]:["lucide-icon"])},[...t.map(j=>En(...j)),...u.default?[u.default()]:[]])};/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ee=(e,t)=>(n,{slots:s,attrs:r})=>En(ku,{...r,...n,iconNode:t,name:e},s.default?{default:s.default}:void 0);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Mu=[["path",{d:"M17 7 7 17",key:"15tmo1"}],["path",{d:"M17 17H7V7",key:"1org7z"}]],Du=Ee("arrow-down-left",Mu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const $u=[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]],Lu=Ee("chevron-down",$u);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Vu=[["path",{d:"m18 15-6-6-6 6",key:"153udz"}]],Fu=Ee("chevron-up",Vu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ju=[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12",key:"o97t9d"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}]],Uu=Ee("inbox",ju);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Hu=[["path",{d:"m16 17 5-5-5-5",key:"1bji2h"}],["path",{d:"M21 12H9",key:"dn1m92"}],["path",{d:"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4",key:"1uf3rs"}]],Bu=Ee("log-out",Hu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ku=[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z",key:"18887p"}]],Gu=Ee("message-square",Ku);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Wu=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",key:"kfwtm"}]],qu=Ee("moon",Wu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zu=[["path",{d:"m16 8 6-6",key:"oawc05"}],["path",{d:"M22 8V2h-6",key:"oqy2zc"}],["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384",key:"9njp5v"}]],Ju=Ee("phone-outgoing",zu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Qu=[["path",{d:"M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384",key:"9njp5v"}]],Ui=Ee("phone",Qu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Yu=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]],Xu=Ee("plus",Yu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Zu=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],mr=Ee("refresh-cw",Zu);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ef=[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",key:"1ffxy3"}],["path",{d:"m21.854 2.147-10.94 10.939",key:"12cjpa"}]],Hi=Ee("send",ef);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const tf=[["path",{d:"M14 17H5",key:"gfn3mx"}],["path",{d:"M19 7h-9",key:"6i9tg"}],["circle",{cx:"17",cy:"17",r:"3",key:"18b49y"}],["circle",{cx:"7",cy:"7",r:"3",key:"dfmy0x"}]],nf=Ee("settings-2",tf);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const sf=[["rect",{width:"14",height:"20",x:"5",y:"2",rx:"2",ry:"2",key:"1yt0o3"}],["path",{d:"M12 18h.01",key:"mhygvu"}]],rf=Ee("smartphone",sf);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const of=[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]],lf=Ee("sun",of);/** + * @license @lucide/vue v1.23.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const cf=[["path",{d:"M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2",key:"q3hayz"}],["path",{d:"m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06",key:"1go1hn"}],["path",{d:"m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8",key:"qlwsc0"}]],af=Ee("webhook",cf),Bi="gsmnode-theme";function uf(){try{return localStorage.getItem(Bi)==="dark"?"dark":"light"}catch{return"light"}}const Mt=G(uf());function Ki(e){Mt.value=e,document.documentElement.setAttribute("data-gsm-theme",e);try{localStorage.setItem(Bi,e)}catch{}}function go(){Ki(Mt.value==="dark"?"light":"dark")}Ki(Mt.value);const ff={class:"flex min-h-screen items-center justify-center bg-page p-4"},df={class:"w-full max-w-sm rounded-xl border border-subtle bg-card p-8 shadow-lg"},pf={class:"mb-7 text-center"},hf=["src"],mf={class:"rounded-md border border-subtle"},gf={class:"flex items-center gap-2"},vf={class:"border-t border-subtle p-3"},bf={key:0,class:"rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},_f=["disabled"],yf={__name:"Login",setup(e){const t=Fi(),n=Au(),s=G(""),r=G(""),o=G(""),i=G(!1),l=G(Js()),c=G(!!Js());async function u(){o.value="",i.value=!0;try{Ru(l.value),await Rn.login(s.value.trim(),r.value),t.push(n.query.redirect||"/")}catch(a){a.status===401?o.value="Invalid email or password.":a.status===void 0?o.value="Cannot reach the API Server. Check the server URL.":o.value=a.message}finally{i.value=!1}}return(a,f)=>(N(),L("div",ff,[g("div",df,[g("div",pf,[g("img",{src:Z(Mt)==="dark"?"/gsmnode-horizontal-white.png":"/gsmnode-horizontal.png",alt:"gsmnode",class:"mx-auto h-8"},null,8,hf),f[4]||(f[4]=g("p",{class:"gn-eyebrow mt-4"},"Sign in to your gateway",-1))]),g("form",{class:"space-y-4",onSubmit:hs(u,["prevent"])},[g("div",null,[f[5]||(f[5]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Email",-1)),Ne(g("input",{"onUpdate:modelValue":f[0]||(f[0]=h=>s.value=h),type:"email",required:"",autocomplete:"username",class:"gn-input",placeholder:"you@example.com"},null,512),[[St,s.value]])]),g("div",null,[f[6]||(f[6]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Password",-1)),Ne(g("input",{"onUpdate:modelValue":f[1]||(f[1]=h=>r.value=h),type:"password",required:"",autocomplete:"current-password",class:"gn-input",placeholder:"••••••••"},null,512),[[St,r.value]])]),g("div",mf,[g("button",{type:"button",class:"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm font-medium text-secondary transition-colors hover:bg-sunken",onClick:f[2]||(f[2]=h=>c.value=!c.value)},[g("span",gf,[W(Z(nf),{class:"h-4 w-4"}),f[7]||(f[7]=de("Server settings ",-1))]),c.value?(N(),pt(Z(Fu),{key:0,class:"h-4 w-4 text-muted"})):(N(),pt(Z(Lu),{key:1,class:"h-4 w-4 text-muted"}))]),Ne(g("div",vf,[f[8]||(f[8]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"API Server URL",-1)),Ne(g("input",{"onUpdate:modelValue":f[3]||(f[3]=h=>l.value=h),type:"url",autocomplete:"off",class:"gn-input font-mono !text-xs",placeholder:"http://10.2.1.101:8080"},null,512),[[St,l.value]]),f[9]||(f[9]=g("p",{class:"mt-1.5 text-xs text-muted"}," Leave blank to use this site's built-in server. ",-1))],512),[[Wc,c.value]])]),o.value?(N(),L("p",bf,K(o.value),1)):Se("",!0),g("button",{type:"submit",disabled:i.value,class:"gn-btn-pri w-full"},K(i.value?"Signing in…":"Sign in"),9,_f)],32)])]))}},xf=["title"],wf={class:"relative flex h-2 w-2"},Sf={key:0,class:"absolute inline-flex h-full w-full animate-ping rounded-full opacity-60",style:{background:"var(--success)"}},Ef={__name:"ApiStatus",setup(e){const t=G("checking"),n=G(null);let s=null;async function r(){const l=performance.now();try{await Me.get("/health"),n.value=Math.round(performance.now()-l),t.value="online"}catch{n.value=null,t.value="offline"}}Et(()=>{r(),s=setInterval(r,1e4)}),ur(()=>clearInterval(s));const o={checking:"background: var(--gray-300)",online:"background: var(--success)",offline:"background: var(--danger)"},i={checking:"Checking…",online:"API online",offline:"API offline"};return(l,c)=>(N(),L("div",{class:"flex items-center gap-2",title:t.value==="online"?`API Server reachable · ${n.value} ms`:t.value==="offline"?"API Server unreachable":"Checking API Server"},[g("span",wf,[t.value==="online"?(N(),L("span",Sf)):Se("",!0),g("span",{class:"relative inline-flex h-2 w-2 rounded-full",style:is(o[t.value])},null,4)]),g("span",{class:On(["font-mono text-xs",t.value==="offline"?"text-danger":"text-secondary"])},[de(K(i[t.value]),1),n.value!==null?(N(),L(ce,{key:0},[de(" · "+K(n.value)+"ms",1)],64)):Se("",!0)],2)],8,xf))}},Af={class:"flex min-h-screen"},Rf={class:"hidden w-60 shrink-0 flex-col border-r border-subtle bg-card sm:flex"},Cf={class:"px-5 pb-4 pt-5"},Of=["src"],Pf={class:"flex-1 space-y-0.5 px-3"},If={class:"border-t border-subtle px-5 py-4"},Tf={class:"flex min-w-0 flex-1 flex-col"},Nf={class:"flex h-16 items-center justify-between border-b border-subtle bg-card px-6"},kf={class:"flex gap-1 overflow-x-auto sm:hidden"},Mf={class:"flex items-center gap-3 text-sm"},Df={class:"hidden font-mono text-xs text-secondary md:inline"},$f=["title"],Lf={class:"flex-1 p-7"},Vf={__name:"Layout",setup(e){const t=Fi(),n=[{name:"devices",label:"Devices",icon:rf},{name:"send",label:"Send SMS",icon:Hi},{name:"call",label:"Call",icon:Ui},{name:"messages",label:"Messages",icon:Gu},{name:"inbox",label:"Inbox",icon:Uu},{name:"webhooks",label:"Webhooks",icon:af}];function s(){Rn.logout(),t.push({name:"login"})}return(r,o)=>{var i;return N(),L("div",Af,[g("aside",Rf,[g("div",Cf,[g("img",{src:Z(Mt)==="dark"?"/gsmnode-horizontal-white.png":"/gsmnode-horizontal.png",alt:"gsmnode",class:"h-6"},null,8,Of)]),o[1]||(o[1]=g("div",{class:"gn-eyebrow px-5 pb-2"},"Gateway",-1)),g("nav",Pf,[(N(),L(ce,null,ot(n,l=>W(Z(Ws),{key:l.name,to:{name:l.name},class:"flex items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium text-secondary transition-colors hover:bg-sunken hover:text-primary","active-class":"!bg-brand-tint !text-brand-active"},{default:Yt(()=>[(N(),pt(Xl(l.icon),{class:"h-[18px] w-[18px]"})),de(K(l.label),1)]),_:2},1032,["to"])),64))]),g("div",If,[W(Ef)])]),g("div",Tf,[g("header",Nf,[g("nav",kf,[(N(),L(ce,null,ot(n,l=>W(Z(Ws),{key:l.name,to:{name:l.name},class:"rounded-sm px-2 py-1 text-xs font-medium whitespace-nowrap text-secondary","active-class":"!bg-brand-tint !text-brand-active"},{default:Yt(()=>[de(K(l.label),1)]),_:2},1032,["to"])),64))]),o[4]||(o[4]=g("div",{class:"hidden sm:block"},null,-1)),g("div",Mf,[g("span",Df,K((i=Z(Rn).state.user)==null?void 0:i.email),1),o[3]||(o[3]=g("span",{class:"hidden h-4 w-px bg-subtle sm:block"},null,-1)),g("button",{class:"gn-btn-sec gn-btn-sm",title:Z(Mt)==="dark"?"Switch to light theme":"Switch to dark theme",onClick:o[0]||(o[0]=(...l)=>Z(go)&&Z(go)(...l))},[Z(Mt)==="dark"?(N(),pt(Z(lf),{key:0,class:"h-4 w-4"})):(N(),pt(Z(qu),{key:1,class:"h-4 w-4"}))],8,$f),g("button",{class:"gn-btn-sec gn-btn-sm",onClick:s},[W(Z(Bu),{class:"h-4 w-4"}),o[2]||(o[2]=de("Sign out ",-1))])])]),g("main",Lf,[W(Z(Vi))])])])}}},Ff={class:"mb-6 flex items-end justify-between gap-4"},jf={class:"text-2xl font-semibold tracking-[-0.02em] text-primary"},Uf={key:0,class:"mt-0.5 text-sm text-secondary"},tn={__name:"PageHeader",props:{title:String,subtitle:String},setup(e){return(t,n)=>(N(),L("div",Ff,[g("div",null,[g("h2",jf,K(e.title),1),e.subtitle?(N(),L("p",Uf,K(e.subtitle),1)):Se("",!0)]),Zl(t.$slots,"actions")]))}},Gi={__name:"StatusBadge",props:{status:String},setup(e){const t=e,n={Pending:"bg-subtle text-secondary",Processed:"bg-brand-tint text-brand-active",Sent:"bg-warning-tint text-warning",Delivered:"bg-success-tint text-success",Failed:"bg-danger-tint text-danger",online:"bg-success-tint text-success",offline:"bg-subtle text-muted"};return(s,r)=>(N(),L("span",{class:On(["inline-flex items-center gap-1.5 rounded-sm px-2 py-0.5 font-mono text-[11px] font-medium",n[t.status]||"bg-subtle text-secondary"])},[r[0]||(r[0]=g("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),de(" "+K(e.status||"—"),1)],2))}},Hf={key:0,class:"mb-4 rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},Bf={class:"overflow-hidden rounded-lg border border-subtle bg-card shadow-xs"},Kf={class:"w-full text-left text-sm"},Gf={key:0},Wf={key:1},qf={class:"border-t border-subtle px-5 py-3 font-medium text-primary"},zf={class:"border-t border-subtle px-5 py-3 font-mono text-xs text-secondary"},Jf={class:"border-t border-subtle px-5 py-3 text-secondary"},Qf={class:"border-t border-subtle px-5 py-3"},Yf={class:"border-t border-subtle px-5 py-3 font-mono text-xs text-secondary"},Xf={class:"border-t border-subtle px-5 py-3 text-right"},Zf=["onClick"],ed={__name:"Devices",setup(e){const t=G([]),n=G(!0),s=G("");async function r(){n.value=!0,s.value="";try{const l=await Me.get("/devices");t.value=l.items||[]}catch(l){s.value=l.message}finally{n.value=!1}}async function o(l){if(confirm(`Remove device "${l.name||l.device_id}"?`))try{await Me.del("/devices/"+l.id),t.value=t.value.filter(c=>c.id!==l.id)}catch(c){alert("Could not remove device: "+c.message)}}function i(l){return l?new Date(l).toLocaleString():"—"}return Et(r),(l,c)=>(N(),L("div",null,[W(tn,{title:"Devices",subtitle:"Phones connected to your gateway"},{actions:Yt(()=>[g("button",{class:"gn-btn-sec gn-btn-sm",onClick:r},[W(Z(mr),{class:"h-3.5 w-3.5"}),c[0]||(c[0]=de("Refresh ",-1))])]),_:1}),s.value?(N(),L("p",Hf,K(s.value),1)):Se("",!0),g("div",Bf,[g("table",Kf,[c[3]||(c[3]=g("thead",null,[g("tr",{class:"gn-eyebrow"},[g("th",{class:"px-5 py-3 font-medium"},"Name"),g("th",{class:"px-5 py-3 font-medium"},"Device ID"),g("th",{class:"px-5 py-3 font-medium"},"Platform"),g("th",{class:"px-5 py-3 font-medium"},"Status"),g("th",{class:"px-5 py-3 font-medium"},"Last seen"),g("th",{class:"px-5 py-3"})])],-1)),g("tbody",null,[n.value?(N(),L("tr",Gf,[...c[1]||(c[1]=[g("td",{colspan:"6",class:"border-t border-subtle px-5 py-10 text-center text-sm text-muted"},"Loading…",-1)])])):t.value.length?Se("",!0):(N(),L("tr",Wf,[...c[2]||(c[2]=[g("td",{colspan:"6",class:"border-t border-subtle px-5 py-10 text-center text-sm text-muted"}," No devices yet. Register one from the phone app. ",-1)])])),(N(!0),L(ce,null,ot(t.value,u=>(N(),L("tr",{key:u.id,class:"transition-colors hover:bg-sunken"},[g("td",qf,K(u.name||"—"),1),g("td",zf,K(u.device_id),1),g("td",Jf,K(u.platform)+" "+K(u.app_version),1),g("td",Qf,[W(Gi,{status:u.status},null,8,["status"])]),g("td",Yf,K(i(u.last_seen_at)),1),g("td",Xf,[g("button",{class:"text-sm font-medium text-danger hover:underline",onClick:a=>o(u)},"Remove",8,Zf)])]))),128))])])])]))}},td={class:"max-w-2xl"},nd={class:"space-y-5 p-6"},sd={class:"grid grid-cols-2 gap-4"},rd=["value"],od={key:0,class:"rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},id={key:1,class:"rounded-md bg-success-tint px-3 py-2 text-sm text-success"},ld={class:"font-mono text-xs"},cd=["disabled"],ad={__name:"Send",setup(e){const t=G([]),n=G(""),s=G(""),r=G(""),o=G(""),i=G(!1),l=G(null),c=G("");Et(async()=>{try{const a=await Me.get("/devices");t.value=a.items||[]}catch{}});async function u(){c.value="",l.value=null;const a=n.value.split(/[\n,;]+/).map(f=>f.trim()).filter(Boolean);if(!a.length){c.value="Enter at least one phone number.";return}if(!s.value.trim()){c.value="Message text is required.";return}i.value=!0;try{const f={phone_numbers:a,text_message:s.value};r.value&&(f.device_id=r.value),o.value&&(f.sim_number=Number(o.value)),l.value=await Me.post("/messages",f),s.value=""}catch(f){c.value=f.message}finally{i.value=!1}}return(a,f)=>(N(),L("div",td,[W(tn,{title:"Send SMS",subtitle:"Queue an outbound message for a device"}),g("form",{class:"rounded-lg border border-subtle bg-card shadow-sm",onSubmit:hs(u,["prevent"])},[f[11]||(f[11]=g("div",{class:"border-b border-subtle px-6 py-4"},[g("div",{class:"text-base font-semibold text-primary"},"New message"),g("div",{class:"mt-0.5 font-mono text-[11px] text-muted"},"POST /api/messages")],-1)),g("div",nd,[g("div",null,[f[4]||(f[4]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Phone numbers",-1)),Ne(g("textarea",{"onUpdate:modelValue":f[0]||(f[0]=h=>n.value=h),rows:"2",class:"gn-textarea font-mono !text-xs",placeholder:"+15551234567, +15559876543"},null,512),[[St,n.value]]),f[5]||(f[5]=g("p",{class:"mt-1.5 text-xs text-muted"},"Separate multiple numbers with commas or new lines.",-1))]),g("div",null,[f[6]||(f[6]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Message",-1)),Ne(g("textarea",{"onUpdate:modelValue":f[1]||(f[1]=h=>s.value=h),rows:"4",class:"gn-textarea",placeholder:"Your message…"},null,512),[[St,s.value]])]),g("div",sd,[g("div",null,[f[8]||(f[8]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Device",-1)),Ne(g("select",{"onUpdate:modelValue":f[2]||(f[2]=h=>r.value=h),class:"gn-input"},[f[7]||(f[7]=g("option",{value:""},"Auto (most recent)",-1)),(N(!0),L(ce,null,ot(t.value,h=>(N(),L("option",{key:h.id,value:h.device_id},K(h.name||h.device_id),9,rd))),128))],512),[[ps,r.value]])]),g("div",null,[f[9]||(f[9]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"SIM number (optional)",-1)),Ne(g("input",{"onUpdate:modelValue":f[3]||(f[3]=h=>o.value=h),type:"number",min:"1",class:"gn-input",placeholder:"1"},null,512),[[St,o.value]])])]),c.value?(N(),L("p",od,K(c.value),1)):Se("",!0),l.value?(N(),L("p",id,[f[10]||(f[10]=de(" Queued — message ",-1)),g("span",ld,K(l.value.id),1),de(" ("+K(l.value.status)+") ",1)])):Se("",!0),g("button",{type:"submit",disabled:i.value,class:"gn-btn-pri"},[W(Z(Hi),{class:"h-4 w-4"}),de(K(i.value?"Queuing…":"Send message"),1)],8,cd)])],32)]))}},ud={class:"max-w-2xl"},fd={class:"space-y-5 p-6"},dd=["value"],pd={key:0,class:"rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},hd={key:1,class:"rounded-md bg-success-tint px-3 py-2 text-sm text-success"},md={class:"font-mono text-xs"},gd=["disabled"],vd={__name:"Call",setup(e){const t=G([]),n=G(""),s=G(""),r=G(!1),o=G(null),i=G("");Et(async()=>{try{const c=await Me.get("/devices");t.value=c.items||[]}catch{}});async function l(){i.value="",o.value=null;const c=n.value.trim();if(!c){i.value="Enter a phone number to call.";return}r.value=!0;try{const u={phone_number:c};s.value&&(u.device_id=s.value),o.value=await Me.post("/calls",u),n.value=""}catch(u){i.value=u.message}finally{r.value=!1}}return(c,u)=>(N(),L("div",ud,[W(tn,{title:"Make a call",subtitle:"Tell a device to place an outbound phone call"}),g("form",{class:"rounded-lg border border-subtle bg-card shadow-sm",onSubmit:hs(l,["prevent"])},[u[7]||(u[7]=g("div",{class:"border-b border-subtle px-6 py-4"},[g("div",{class:"text-base font-semibold text-primary"},"New call"),g("div",{class:"mt-0.5 font-mono text-[11px] text-muted"},"POST /api/calls")],-1)),g("div",fd,[g("div",null,[u[2]||(u[2]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Phone number",-1)),Ne(g("input",{"onUpdate:modelValue":u[0]||(u[0]=a=>n.value=a),type:"tel",class:"gn-input font-mono !text-xs",placeholder:"+15551234567"},null,512),[[St,n.value]])]),g("div",null,[u[4]||(u[4]=g("label",{class:"mb-1.5 block text-sm font-medium text-primary"},"Device",-1)),Ne(g("select",{"onUpdate:modelValue":u[1]||(u[1]=a=>s.value=a),class:"gn-input"},[u[3]||(u[3]=g("option",{value:""},"Auto (most recent)",-1)),(N(!0),L(ce,null,ot(t.value,a=>(N(),L("option",{key:a.id,value:a.device_id},K(a.name||a.device_id),9,dd))),128))],512),[[ps,s.value]])]),i.value?(N(),L("p",pd,K(i.value),1)):Se("",!0),o.value?(N(),L("p",hd,[u[5]||(u[5]=de(" Queued — call ",-1)),g("span",md,K(o.value.id),1),de(" ("+K(o.value.status)+") ",1)])):Se("",!0),g("button",{type:"submit",disabled:r.value,class:"gn-btn-pri"},[W(Z(Ui),{class:"h-4 w-4"}),de(K(r.value?"Queuing…":"Place call"),1)],8,gd),u[6]||(u[6]=g("p",{class:"text-xs text-muted"}," The selected device must be online with the gateway running and granted phone-call permission. The call is placed from that phone. ",-1))])],32)]))}},bd={class:"flex items-center gap-2"},_d=["value"],yd={key:0,class:"mb-4 rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},xd={class:"overflow-hidden rounded-lg border border-subtle bg-card shadow-xs"},wd={class:"w-full text-left text-sm"},Sd={key:0},Ed={key:1},Ad={class:"border-t border-subtle px-5 py-3 font-mono text-xs text-primary"},Rd={class:"border-t border-subtle px-5 py-3 text-secondary"},Cd={key:0,class:"flex items-center gap-1.5"},Od={key:1,class:"max-w-md truncate"},Pd={key:2,class:"mt-0.5 text-xs text-danger"},Id={class:"border-t border-subtle px-5 py-3"},Td={class:"border-t border-subtle px-5 py-3 font-mono text-xs text-secondary"},Nd={__name:"Messages",setup(e){const t=G([]),n=G(!0),s=G(""),r=G(""),o=["","Pending","Processed","Sent","Delivered","Failed"];async function i(){n.value=!0,s.value="";try{const c=r.value?"?status="+encodeURIComponent(r.value):"",u=await Me.get("/messages"+c);t.value=u.items||[]}catch(c){s.value=c.message}finally{n.value=!1}}function l(c){return c?new Date(c).toLocaleString():"—"}return Et(i),(c,u)=>(N(),L("div",null,[W(tn,{title:"Messages",subtitle:"Outbound message history"},{actions:Yt(()=>[g("div",bd,[Ne(g("select",{"onUpdate:modelValue":u[0]||(u[0]=a=>r.value=a),class:"gn-input !h-8 !w-auto !text-xs",onChange:i},[(N(),L(ce,null,ot(o,a=>g("option",{key:a,value:a},K(a||"All statuses"),9,_d)),64))],544),[[ps,r.value]]),g("button",{class:"gn-btn-sec gn-btn-sm",onClick:i},[W(Z(mr),{class:"h-3.5 w-3.5"}),u[1]||(u[1]=de("Refresh ",-1))])])]),_:1}),s.value?(N(),L("p",yd,K(s.value),1)):Se("",!0),g("div",xd,[g("table",wd,[u[5]||(u[5]=g("thead",null,[g("tr",{class:"gn-eyebrow"},[g("th",{class:"px-5 py-3 font-medium"},"To"),g("th",{class:"px-5 py-3 font-medium"},"Message"),g("th",{class:"px-5 py-3 font-medium"},"Status"),g("th",{class:"px-5 py-3 font-medium"},"Created")])],-1)),g("tbody",null,[n.value?(N(),L("tr",Sd,[...u[2]||(u[2]=[g("td",{colspan:"4",class:"border-t border-subtle px-5 py-10 text-center text-sm text-muted"},"Loading…",-1)])])):t.value.length?Se("",!0):(N(),L("tr",Ed,[...u[3]||(u[3]=[g("td",{colspan:"4",class:"border-t border-subtle px-5 py-10 text-center text-sm text-muted"},[de(" No messages yet. Send your first with a POST to "),g("span",{class:"rounded-xs bg-sunken px-1.5 py-0.5 font-mono text-xs text-secondary"},"/api/messages"),de(". ")],-1)])])),(N(!0),L(ce,null,ot(t.value,a=>(N(),L("tr",{key:a.id,class:"align-top transition-colors hover:bg-sunken"},[g("td",Ad,K((a.phone_numbers||[]).join(", ")),1),g("td",Rd,[a.type==="call"?(N(),L("div",Cd,[W(Z(Ju),{class:"h-3.5 w-3.5"}),u[4]||(u[4]=g("span",{class:"gn-eyebrow !text-secondary"},"Voice call",-1))])):(N(),L("div",Od,K(a.text_message),1)),a.error?(N(),L("div",Pd,K(a.error),1)):Se("",!0)]),g("td",Id,[W(Gi,{status:a.status},null,8,["status"])]),g("td",Td,K(l(a.created_at)),1)]))),128))])])])]))}},kd={key:0,class:"mb-4 rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},Md={key:1,class:"text-sm text-muted"},Dd={key:2,class:"rounded-lg border border-dashed border-strong p-10 text-center text-sm text-muted"},$d={key:3,class:"space-y-3"},Ld={class:"flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-success-tint text-success"},Vd={class:"min-w-0 flex-1"},Fd={class:"flex items-baseline justify-between gap-3"},jd={class:"font-mono text-sm font-medium text-primary"},Ud={class:"shrink-0 font-mono text-[11px] text-muted"},Hd={class:"mt-1 text-sm text-secondary"},Bd={__name:"Inbox",setup(e){const t=G([]),n=G(!0),s=G("");async function r(){n.value=!0,s.value="";try{const i=await Me.get("/inbox");t.value=i.items||[]}catch(i){s.value=i.message}finally{n.value=!1}}function o(i){return i?new Date(i).toLocaleString():"—"}return Et(r),(i,l)=>(N(),L("div",null,[W(tn,{title:"Inbox",subtitle:"Incoming messages received by your devices"},{actions:Yt(()=>[g("button",{class:"gn-btn-sec gn-btn-sm",onClick:r},[W(Z(mr),{class:"h-3.5 w-3.5"}),l[0]||(l[0]=de("Refresh ",-1))])]),_:1}),s.value?(N(),L("p",kd,K(s.value),1)):Se("",!0),n.value?(N(),L("div",Md,"Loading…")):t.value.length?(N(),L("div",$d,[(N(!0),L(ce,null,ot(t.value,c=>(N(),L("div",{key:c.id,class:"flex items-start gap-3 rounded-lg border border-subtle bg-card p-4 shadow-xs"},[g("span",Ld,[W(Z(Du),{class:"h-4 w-4"})]),g("div",Vd,[g("div",Fd,[g("span",jd,K(c.phone_number),1),g("span",Ud,K(o(c.received_at)),1)]),g("p",Hd,K(c.message),1)])]))),128))])):(N(),L("div",Dd," No incoming messages yet. "))]))}},Kd={class:"max-w-3xl"},Gd=["value"],Wd={class:"flex-1"},qd=["disabled"],zd={key:0,class:"mb-4 rounded-md bg-danger-tint px-3 py-2 text-sm text-danger"},Jd={class:"overflow-hidden rounded-lg border border-subtle bg-card shadow-xs"},Qd={class:"w-full text-left text-sm"},Yd={key:0},Xd={key:1},Zd={class:"border-t border-subtle px-5 py-3"},ep={class:"rounded-sm bg-brand-tint px-2 py-0.5 font-mono text-xs text-brand-active"},tp={class:"border-t border-subtle px-5 py-3 font-mono text-xs text-secondary"},np={class:"border-t border-subtle px-5 py-3 text-right"},sp=["onClick"],rp={__name:"Webhooks",setup(e){const t=G([]),n=G(!0),s=G(""),r=G("sms:received"),o=G(""),i=G(!1),l=["sms:received","sms:sent","sms:delivered","sms:failed"];async function c(){n.value=!0,s.value="";try{const f=await Me.get("/webhooks");t.value=f.items||[]}catch(f){s.value=f.message}finally{n.value=!1}}async function u(){if(s.value="",!o.value.trim()){s.value="URL is required.";return}i.value=!0;try{const f=await Me.post("/webhooks",{event:r.value,url:o.value.trim()});t.value.unshift(f),o.value=""}catch(f){s.value=f.message}finally{i.value=!1}}async function a(f){if(confirm("Delete this webhook?"))try{await Me.del("/webhooks/"+f.id),t.value=t.value.filter(h=>h.id!==f.id)}catch(h){alert("Could not delete: "+h.message)}}return Et(c),(f,h)=>(N(),L("div",Kd,[W(tn,{title:"Webhooks",subtitle:"Get notified when messages change state or arrive"}),g("form",{class:"mb-6 flex flex-wrap items-end gap-3 rounded-lg border border-subtle bg-card p-4 shadow-xs",onSubmit:hs(u,["prevent"])},[g("div",null,[h[2]||(h[2]=g("label",{class:"gn-eyebrow mb-1.5 block"},"Event",-1)),Ne(g("select",{"onUpdate:modelValue":h[0]||(h[0]=v=>r.value=v),class:"gn-input !w-auto font-mono !text-xs"},[(N(),L(ce,null,ot(l,v=>g("option",{key:v,value:v},K(v),9,Gd)),64))],512),[[ps,r.value]])]),g("div",Wd,[h[3]||(h[3]=g("label",{class:"gn-eyebrow mb-1.5 block"},"Target URL",-1)),Ne(g("input",{"onUpdate:modelValue":h[1]||(h[1]=v=>o.value=v),type:"url",placeholder:"https://example.com/hook",class:"gn-input font-mono !text-xs"},null,512),[[St,o.value]])]),g("button",{type:"submit",disabled:i.value,class:"gn-btn-pri"},[W(Z(Xu),{class:"h-4 w-4"}),h[4]||(h[4]=de("Add webhook ",-1))],8,qd)],32),s.value?(N(),L("p",zd,K(s.value),1)):Se("",!0),g("div",Jd,[g("table",Qd,[h[7]||(h[7]=g("thead",null,[g("tr",{class:"gn-eyebrow"},[g("th",{class:"px-5 py-3 font-medium"},"Event"),g("th",{class:"px-5 py-3 font-medium"},"URL"),g("th",{class:"px-5 py-3"})])],-1)),g("tbody",null,[n.value?(N(),L("tr",Yd,[...h[5]||(h[5]=[g("td",{colspan:"3",class:"border-t border-subtle px-5 py-10 text-center text-sm text-muted"},"Loading…",-1)])])):t.value.length?Se("",!0):(N(),L("tr",Xd,[...h[6]||(h[6]=[g("td",{colspan:"3",class:"border-t border-subtle px-5 py-10 text-center text-sm text-muted"},"No webhooks.",-1)])])),(N(!0),L(ce,null,ot(t.value,v=>(N(),L("tr",{key:v.id,class:"transition-colors hover:bg-sunken"},[g("td",Zd,[g("span",ep,K(v.event),1)]),g("td",tp,K(v.url),1),g("td",np,[g("button",{class:"text-sm font-medium text-danger hover:underline",onClick:T=>a(v)},"Delete",8,sp)])]))),128))])])])]))}},op=[{path:"/login",name:"login",component:yf},{path:"/",component:Vf,meta:{requiresAuth:!0},children:[{path:"",redirect:{name:"devices"}},{path:"devices",name:"devices",component:ed},{path:"send",name:"send",component:ad},{path:"call",name:"call",component:vd},{path:"messages",name:"messages",component:Nd},{path:"inbox",name:"inbox",component:Bd},{path:"webhooks",name:"webhooks",component:rp}]},{path:"/:pathMatch(.*)*",redirect:"/"}],Wi=Eu({history:ru(),routes:op});Wi.beforeEach(e=>{if(e.meta.requiresAuth&&!Rn.isAuthenticated())return{name:"login",query:{redirect:e.fullPath}};if(e.name==="login"&&Rn.isAuthenticated())return{path:"/"}});ha(ya).use(Wi).mount("#app"); diff --git a/Web App/server/dist/favicon-32.png b/Web App/server/dist/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..23301f08f08ab2a23fe5b940a8994bf2f3cce797 GIT binary patch literal 759 zcmV|H~v=l|9uC}?_AD(96|*7 zw=H_X&#Yir4WbnU=y~%$V8AdKdk>;^pD5P!9IYP;_b7Z(&{(;f1?UzL(!;fxfHAnh z0_BdwwU4A66foHY8z?!&8kd_7yAj6sLITL85af7NG7}GECJQL ztRf6JMCxP2ZJH=W{H4#fgHa*B|xGw?haX%DPhm=6Ns%*!S`{P>#p zDiAtc*umo4O9zjM@ zDvmeTq4kp^ST`i=f>QULZE$&ihDIwxvB18(ji_F~KlqqQyIpRTeeMA~?R&?w9Et^6 z4_ri=M$5AtqZ7Zd>7JEmITQ=zr!U6UZO4$QO+&{(FB&?Z!RHrwKay`A7ru-Q^DKvA z0STQ(i<&LvSeuoL(=YDeN$*?Un=VlaNJx*_ss%=L4))=9z|ZT`C2E0KGbca=wn{gM zbo`#EN$8jC1At{}&Kp7Z?EjW8y=%@AM4=Zz68BO82IggcA$#_ZX5qz}8Y=dH;%qe2 zhf`pfh?RDgUVkiQE+COxXU@ + + + + + + + + \ No newline at end of file diff --git a/Web App/server/dist/gsmnode-horizontal-white.png b/Web App/server/dist/gsmnode-horizontal-white.png new file mode 100644 index 0000000000000000000000000000000000000000..ac100c0a094d3ff30d5db56e0794c27c2a02a455 GIT binary patch literal 11104 zcmeHt_gholvu`LOQbd|aQ;L8@dJ(0IfFd=J&_P6s1P#(fTBu4Fkluqp2u%c}7X^e+ zMS2Ml2}P=a5FnJhd0+4MoOAzzd!Oh2!ro!;H8X40%$}LgOoV~neR>)W8W0FXuk}FP z5Coz?1NT3user#1QQFlY5D!R8{Vw#S^*S!_t(~LQ=GJDdZwMl17JQS8m$8}hq1X)* zW<=;)yUP*j9~3w#;iKAy$jB|WIq)S%YE5ctEeATrP>=@FSAgoF7UR1#dTev>Zk8skT4vAsBh5q25$FB+<7c-ZRmWzC6D*_V&KIP`PAFg2!GB@2@ z?wGEA;HDdWrf>co353k^$M-&0U^c^0RBOs_H*0DTDql zASYuz`~M|Ma`4%ehnn*L6Zn5k2NI|~oI7rEx8Tr)q!WnP99h7g2T@a=ove-e`lD2T zGhITy@^DT@>QDJgsxNL!##DMW`j@YN?BXbrX(`1-J!vWifz}=XM?W8qln?(|x2vMQ z&>GQAB;L~3aNvkp(eTuXX1=Q;J0%Fj zABCi`nYm=p^`Oyzn}*q+Xq;guCSN}|mL$4G&-6}40o7}+Z zRulpT%tQBA1VQt$S17TbTB+x;lQl7#J8)Qy9UTZ1O-Bw!y53XeW{y)nTsBHk(0

9Qi9WAHmlgG0V9aF~KJQ`x}4R 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 literal 0 HcmV?d00001 diff --git a/Web App/server/dist/gsmnode-horizontal.png b/Web App/server/dist/gsmnode-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd2937882be756086acb72136a40562e3a0afc4 GIT binary patch literal 11370 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 literal 0 HcmV?d00001 diff --git a/Web App/server/dist/gsmnode-mark.svg b/Web App/server/dist/gsmnode-mark.svg new file mode 100644 index 0000000..d8de483 --- /dev/null +++ b/Web App/server/dist/gsmnode-mark.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Web App/server/dist/icon-192.png b/Web App/server/dist/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..616b0e0a7bf8a8f4a454485bd44234a70528d6b6 GIT binary patch literal 4246 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 literal 0 HcmV?d00001 diff --git a/Web App/server/dist/icon-512.png b/Web App/server/dist/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..2d94601849a7de6d5c4cda528a4963ded4ed2430 GIT binary patch literal 11170 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 + + + + + + + + + gsmnode + + + + +

+ + diff --git a/Web App/server/go.mod b/Web App/server/go.mod new file mode 100644 index 0000000..36864ba --- /dev/null +++ b/Web App/server/go.mod @@ -0,0 +1,3 @@ +module smsgateway/webapp + +go 1.26 diff --git a/Web App/server/main.go b/Web App/server/main.go new file mode 100644 index 0000000..355fa2c --- /dev/null +++ b/Web App/server/main.go @@ -0,0 +1,113 @@ +// Command webapp is the Web App backend-for-frontend. It serves the embedded +// Vue single-page app and reverse-proxies /api/* to the API Server, so the +// browser only ever talks to this server (same-origin) and all data access +// still flows through the API Server. +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + "time" +) + +//go:embed all:dist +var distFS embed.FS + +func getenv(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func main() { + log.SetFlags(log.LstdFlags | log.Lmsgprefix) + log.SetPrefix("[web] ") + + loadDotEnv(".env") + addr := getenv("WEB_ADDR", ":8090") + apiBase := strings.TrimRight(getenv("API_BASE", "http://localhost:8080"), "/") + + apiURL, err := url.Parse(apiBase) + if err != nil { + log.Fatalf("invalid API_BASE %q: %v", apiBase, err) + } + + // Reverse proxy: /api/* -> API Server (path preserved). + proxy := httputil.NewSingleHostReverseProxy(apiURL) + proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e error) { + log.Printf("proxy error for %s: %v", r.URL.Path, e) + http.Error(w, `{"error":"api server unavailable"}`, http.StatusBadGateway) + } + + // Embedded SPA file server. + sub, err := fs.Sub(distFS, "dist") + if err != nil { + log.Fatalf("embed dist: %v", err) + } + spa := http.FileServer(http.FS(sub)) + + mux := http.NewServeMux() + mux.Handle("/api/", proxy) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Serve static assets when they exist; otherwise fall back to index.html + // so client-side routing works on deep links. + if r.URL.Path != "/" { + if f, err := sub.Open(strings.TrimPrefix(r.URL.Path, "/")); err == nil { + f.Close() + spa.ServeHTTP(w, r) + return + } + } + r2 := r.Clone(r.Context()) + r2.URL.Path = "/" + spa.ServeHTTP(w, r2) + }) + + srv := &http.Server{ + Addr: addr, + Handler: logRequests(mux), + ReadHeaderTimeout: 10 * time.Second, + } + log.Printf("listening on %s (proxying /api -> %s)", addr, apiBase) + if err := srv.ListenAndServe(); err != nil { + log.Fatalf("server error: %v", err) + } +} + +func logRequests(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond)) + }) +} + +// loadDotEnv loads KEY=VALUE pairs from a .env file if present. +func loadDotEnv(path string) { + data, err := os.ReadFile(path) + if err != nil { + return + } + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + k, v, ok := strings.Cut(line, "=") + if !ok { + continue + } + k = strings.TrimSpace(k) + v = strings.Trim(strings.TrimSpace(v), `"'`) + if _, exists := os.LookupEnv(k); !exists { + _ = os.Setenv(k, v) + } + } +} diff --git a/Web App/web/.gitignore b/Web App/web/.gitignore new file mode 100644 index 0000000..aa0926a --- /dev/null +++ b/Web App/web/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.env +*.log diff --git a/Web App/web/index.html b/Web App/web/index.html new file mode 100644 index 0000000..072f71f --- /dev/null +++ b/Web App/web/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + gsmnode + + +
+ + + diff --git a/Web App/web/package-lock.json b/Web App/web/package-lock.json new file mode 100644 index 0000000..b09cbc9 --- /dev/null +++ b/Web App/web/package-lock.json @@ -0,0 +1,1996 @@ +{ + "name": "sms-gateway-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sms-gateway-web", + "version": "1.0.0", + "dependencies": { + "@lucide/vue": "^1.23.0", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "tailwindcss": "^4.0.0", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lucide/vue": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lucide/vue/-/vue-1.23.0.tgz", + "integrity": "sha512-9SIYeY5K+R1iv8F8JUKMGSL7Pck/86BJ8djtZz/lnYsoHtKFUj1H2z6+PvKnC/8blZ8tqTDeDXAoTKobuX80hg==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/Web App/web/package.json b/Web App/web/package.json new file mode 100644 index 0000000..1157fd3 --- /dev/null +++ b/Web App/web/package.json @@ -0,0 +1,22 @@ +{ + "name": "gsmnode-web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@lucide/vue": "^1.23.0", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@vitejs/plugin-vue": "^5.2.1", + "tailwindcss": "^4.0.0", + "vite": "^6.0.7" + } +} diff --git a/Web App/web/public/apple-touch-icon.png b/Web App/web/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..80236eb1492981695e669062d7337b8261acffad GIT binary patch literal 4109 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 + + + + + + + + \ No newline at end of file diff --git a/Web App/web/public/gsmnode-horizontal-white.png b/Web App/web/public/gsmnode-horizontal-white.png new file mode 100644 index 0000000000000000000000000000000000000000..ac100c0a094d3ff30d5db56e0794c27c2a02a455 GIT binary patch literal 11104 zcmeHt_gholvu`LOQbd|aQ;L8@dJ(0IfFd=J&_P6s1P#(fTBu4Fkluqp2u%c}7X^e+ zMS2Ml2}P=a5FnJhd0+4MoOAzzd!Oh2!ro!;H8X40%$}LgOoV~neR>)W8W0FXuk}FP z5Coz?1NT3user#1QQFlY5D!R8{Vw#S^*S!_t(~LQ=GJDdZwMl17JQS8m$8}hq1X)* zW<=;)yUP*j9~3w#;iKAy$jB|WIq)S%YE5ctEeATrP>=@FSAgoF7UR1#dTev>Zk8skT4vAsBh5q25$FB+<7c-ZRmWzC6D*_V&KIP`PAFg2!GB@2@ z?wGEA;HDdWrf>co353k^$M-&0U^c^0RBOs_H*0DTDql zASYuz`~M|Ma`4%ehnn*L6Zn5k2NI|~oI7rEx8Tr)q!WnP99h7g2T@a=ove-e`lD2T zGhITy@^DT@>QDJgsxNL!##DMW`j@YN?BXbrX(`1-J!vWifz}=XM?W8qln?(|x2vMQ z&>GQAB;L~3aNvkp(eTuXX1=Q;J0%Fj zABCi`nYm=p^`Oyzn}*q+Xq;guCSN}|mL$4G&-6}40o7}+Z zRulpT%tQBA1VQt$S17TbTB+x;lQl7#J8)Qy9UTZ1O-Bw!y53XeW{y)nTsBHk(0

9Qi9WAHmlgG0V9aF~KJQ`x}4R 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 literal 0 HcmV?d00001 diff --git a/Web App/web/public/gsmnode-horizontal.png b/Web App/web/public/gsmnode-horizontal.png new file mode 100644 index 0000000000000000000000000000000000000000..bcd2937882be756086acb72136a40562e3a0afc4 GIT binary patch literal 11370 zcmeHt^;?wPxAzPPNSAaf5H1+*Wm~5i9LI-z4waG`m7bMsjfhXM}r3ffe4ip<+MQ{OgQj*;Vw4t zpD0JA3It*VDak$8^)lO<3;bg0?!B|OI>nD`$MqW9Cb)M}I}dBS`%{xj-q^8^Q&F%P z$;^BYR+8?_SIrRkS~q9^;U{ELI;T!>^j` zf8@s6o1DzjWz`?BJ-ymHwbVP%)N+A3%jDmgVCX)xSv%YI_Yk_M%-RX4xfK)yqSqu| z`TGI{rnr4B;IQ9H4Fd6yXo7E_W{fb5+h?P+G}i6YD+v1cm%AAM{s0Su{r2-#{5!h0 zPbk&h|DE9fw=;mr6B0SqYwFh4sRmjt4*LEk1})&;-m7$^(Mb`P0oL=~B<>N0sk#pv z(xCZY?V}wH$cum)vdP1^w(s#wxja>S1sI^OOr)CNDq0K(?fUi_(m)r@$y7KYJivLm zJKFyJr25wiFvQ~DA!%#dYd5KIlW&7IDV>rT-#FOVP%jprgFu57fc{Jqf~%w4j!OIs zA-m!E!d)F4uf4|N+QR5#!T_06m|z%o!gof6hR0@Z8YA4;rXJz9Z4?qQT>6Ov8fZiW zrjSNHgw=#ItZn(9y{R~_7f7?Tsa03|G^cHRg%6A%{LctMp1`v+x|;Qrsx(U`^As9l zG?UA2*d8MYWQPmr^HwNy`bC59#kG!{@5tNF;hR#V^yW8hZDGVeKL9H4``?GWlyh>M zdPJTE*F9X%9f;YE!U2Pz6aQ|e$<5>FP%4N0X3n}&)k;(VdEMDQiFv!uF6FHIonnR#^_%=-=}; z9vSgegZX4dlz2?3rm}%S3&I%eP!CyYM_2oP$&=5~j9!EJNT;YMgCDyH<}hLjdSbx1 zZ%rKd`8wY2aOHsHN!7yP9D7B-wdN4bD%PwZ$kUac1NJ`Pkx) zfG-kpS@H2&^%95Bp*+Pe-$iCe{orPfV2nL!k|O6Et-9F)|05FF*9Jj#c`n76;7%1m&R=`FfYB$`-N& zFco@J@zXwG5lPV z@P66apO#N3+=HaNcE&v;yJ{X$%|#ZOeTo!^S=6!-a`xQ|c}I$Tt{d>Ch%xiUz1j99 zdnO+ZjyrLWlhgiO$StMjW30KTZsOu^VfM`BJzhWMQr_ALX;zZsj>O@IR9oI@{hjJ! zpi zJ`yEP1h-9h@3$8f6P1i(ZKZgE=bk{`@RP-hi&aH>YkSRim?eEYW$J&HTWDFY94a=)!cNcLQv2^J01Tv>#-etmlj&CL{Yr9yvTU0RSD z!jPHz>^_&h41T9Ju1eqLr0UE1&C%F(ORVaKM%|)OBu$70Jf-qWq&2TkHhZb?fdp&#I|}@u!rlvt~6}+TTH+$Ji|@I0md! zxf6@I6CdKeVmjaEYgQSZ=NHafalc$CZJ0IQQn5xiCUcx4T`F_}XHW?^pcRh8_Ah## zPH0!A^L;PSH9u`e`hDM!8*=%gUse5^FXc6^-6WDvDK%Y-7ASgU&N@eb4W<1CD{zAa zeJE>X^`+>DZ7h7*P$@i?{tsfT42fujDeeJd6O2rS>m47!X%$TsS{!xg?Jft(*CVGRTN2X`O7m); zT};i4KEuyOM^7hbOk23OcuBBEw=#b$t}XJFCZJp8#GRs+fU}#xYXrwk|1Drq#@a*lu5REe-bK8cujOh%>}218=95xR3D8aD8C*H*i3te&Zazpvg=O9%5~)vA&SYbB?Q z6p3eoyvH4Y4k*zoM_BuEjXE_1XEw6Kt54FgiBXtbo$f_W1(x5fPE?<{_7r{RX*eSd zE=F(f`*!paeVjbjDgIGe{=7k7P4ag|qt`cs>E2};&D>QG5ZV`mSn>7d_1iaWu|3~j zCFP)%z4nHtSHx!6X-G3yexnu7lMW? zyI8PKuFXmKdn(~`QoqqN%oHx=uM8_-J*0^nqWb;Z>nG~zicvxj2h8K-P>Pd%V-ak? z8YZ`e&TXZO;d-uTMnUmfZh6-0DQ41Yvnl4fOHg@(mFSdyZmTiC~gd+UC&*K_i2 z$GFla2}PSZ)cff6O|)`Tj0i99MdUsuKzI6TgIksW$=gzFGrbE+&f^Z+4}1BVo$~Lt zb-$s@`0hbb+PwWeBxOzrIC-%lsg4i3>dka79RV1?qU&ps4SQKy(1qyvxkRPA)cvz9 zV#^&@$@|Wzc%qoXSdXUq{JZrF@sW=|OcVz9W()(9mSq-YW--#7*9&wi2XeTn zDl*uc02=&(_CW!1S9#U^vv#~;Po;rN^r!CTN z{;%xHnn~mki>m5SE zRl{s17*g*0GSUYpA;t<#Xoe=|Bhnis)fkJphfGSSwcUbNoJms^q_@_$e8VE^xM+p`NnH2VNKgRx8&J`9%wRyD>b!8^O+oy| zb!U+_JZScFP56kz%d5#*m?)A!$Ot3NM{1c=vr_L%ZrU@CBbi^~QN26MTy;g1P`BG1 zrjDokZ?n)n--UfVZg33xZ-Du`amtU$5`kY<56e`fs6MMmyO%yrqbvT?m8DKT9N&5V zb~-}UJk~}X@cn(-zXf^<fExq`C$DCM=3QBex_=kdh^{0j6onINzo8Li;($^ zp!OX<%V`lxYI!zQlopRY5@NMQK)V$apW$PcC?b~w$*^-~q<1C}tPq{bHn2)jp%*4x zufQI4*tzMeRnkg)VJ0thA2ZghfSlZDBsH_y{yjt%%W$g#i?!lL&dcwLn3zHG9`O(R zH$Q&JRD_N+G|E8e!DB2i`Tc1s{I0|9v@hl9N=;WbC|0%(van2n=X^@g5?8~Ov`3&- zGEG;+k0o+~Wp_idNgK2N)RF6tR+*7gI0{09jxkbV_aDqRnCeQA5a)fdXAv%pO%;R@ zZOh9TyXch+K^CFAAbRWN;93tvf zg&R&kbFB6HY+9gejsuwY!Zoz)bqVXm$v}dmq@W=N#L=+fK{Sqb=Yhyty-Azz_!u#0 zf6>oErg$w-$sk4ohA?R8Pq$GleWJrCAR-{b+aPqE9xw3A`k@jA%4HS<(h$3nQ}e)B zo5%K3sy>TQoKyTAERPE>lWR0dO!~=~+UB;(Yxms3@ z#vE6nMwA$kTuyNHN672El!rrtesU2d(bk4PQkLnyZD$uwN+<5Iz9@jMpF23mp%gOg zDFipv2ta4flDyf1g9Nz!W8Gf{9uzFk=6!^w;{P*MXXG?z&$G&4;*$j(KZX{7MAWY;G<^?UG&f4$X3=!axuG-S{d8ny< zGNmG3DnZ(Ax#xVRS`4}ehuJ7dJ1)^^Mw;}~bPYdvta9pXrkvJRS>E%`pHi^LY56ik z#9h}cLPfKr$K_MqWyX_=R6q8UF`Vsxryt!u_nbTZylpu`6&YHrAT)bq7wO2;7fYhT zAO{(4OdC#^afPW7z_BFaX9(6}BHaei1x2$|b3cW<2nmxSw^A*5>?#FRy2&I>(2j;y z)nEIoQ&aMNR?W2fk2#&ms8?3}K0Pyd>$Ww;Efrl^vGs5%)wnN`o zpvIP(z){hV+8SjTM}Rd;PxCXCx%7o@dXfybzlKqnoLuCBTT z*&Uc4bE5kt%g*<8S7_QmL5Qg7$@QPC{<6&L^5X6d(*hf&k?g4>1=bGwk2pj7!#HuN zfG2Ub^1Dp&f`PVOd+5nbTa0^&deaB9IQI4Utrc6Y?u>?fdG~#brlX>_Z&|G8Wxvtt zwAO@RtO(9189QJKyuTZyOj`JR)Z(j5GSRFsi~wg*-s)WUww=aDgS*d3w@h6wMr)!R z?$bm_`7ZU&H^*S-w5;*5{CE0dh|;9^%bP^g24#lNko~*xH;J(R5$uP8SIulN?nf$j zRl8X@U;MLvW1VMFw7dys$^f0cX`4m6dthBa;l@e5bA6HK3NOK{cljBQ{e+x%#Lzi5 zCoHMCQ@?cxSzj-d{N&*GydW$hgeuCRVFS6h8f?;1{%NsIkn@EJX0H7K z_r#9tult%ASvuIUS;Ol0%=m`B`_}p^%Dq+RZ&CKTED4+z-yGX-7Hn!N-;5p8K*g(e z$~E%|(l^U5Ef6n0ucE+M{|#tCz`{BA_=}nenJb(7!7-D23w!!^OYV%munub#=*~>D zUw9J&U8lK!E8P$C_7kYl@dGfY!%ytdYELvkt?0W4n8^o<%+unk*)OvZIh-z4?m=

&&;{76TL#il1jgkmcK+EAz3I&IcG}Z%AP*DbOH7URx#pzo~H=R}rrz0T^uC?&8$P+mia%Ngj&Z~4tyxY-X2%=%r zNtr({Aq1Fry&Kc`DCSI8OXsN=W6sT6Rnp>$ZaSn=t@Jr_g=ZRRHAH8oJA-d#&#s8p%Qp=tTVf z?0MaFjDj{)Sro&cBtPK$<`mv7%#Lz580rZIj;fm|=*<@iVT^S@;3Uhy)f6O}K`X{u zV0Y)$aeLK6NeUo}ZS*arU0mm`QdwObKczoZHE_CF)m=IdDX_Ld4@ES-b8&GHpqHCr zhH>-wzvKw&daHA>4GpbbRkUGhcWNff|a|Nd56gnw%gD~Wy{e5N_#^cW; z;YG{Ogk(h%!o>xSD#j*0nO%GXK&XcM(Q%WkoRfB37F=V9v(X>&8+LG<>rSefl>pBowcmTmR!9(@mXO)>|%Ecw2o&@Fp1SZuW&44hi>dpJcRYHHfVsB3b$^^ zj&~aNSWHm`44QEqi>2y2HF1Pu0SZ;ssko&g7qcpI0^%;uytJ)GFJ47ikZ8sig^rGF zWxMJ9B$F?%Care|6r)DGIRQEi-y}(XG3Dw?D1K-3>@K`C42maUZj3YfsV^4M7p%K^ z9v3A`-2Op2@*%8Xv5&tfvZ9GX#M^(oK>5FPl@{zSnRqH>5zYf;s&&o)^8K>KL-O6lp3Ne|3gduYTz^<%*HYF zkE{=lMwx3h+#pjvb3YF$6eLj&3aPJRc4qF zz~esF_Qe{d7$?A0d=E`EJ@kX1);vm6uMzN)m>bwsZ?xXwk_M9Vgyp}E`7Zo`RFf89 zk?$2bVyr8_4ZXt|0!=M9LZNqEbedG<&BiXRTi48Pw1Eiqe=X)h2-g~cpMEHe2ynay zU9Ogw&Zj2(T6g$7&&I{CYWLv7#w5fbSO2q(^a(fY?`JmOi}jZEa%wuP zktQjViB&r)YArh>Sic1N3Wgup4^?as;nGLrkSV$}exbt{dF=c4@8v|9V6xM>fFH%3 z=Cm3J$YLZWN`4&e1yXN!|7PAN6ZI^(4>ITcGp__5`OAKaL;Y1?7kZ`Cvk-%zdU=YLe6ga zR=!y171*+}>0Q-7AAQT}SGr{4ad z)8U0pro`7@@dF*0b`8LRbo93SEd*C9PCNsy=hC{Y4Ew2&n{?5Vu7bl zO6V+FI>q25#$DGj!npH3fn=}%JL5>T4m78^rNs-DF(CAPv8pBK=xLucG}GqP;F89P z6V16@q-tHM`_bXt&SC1k__=uUT!Z#3*YUZ^W6`07NMNNcN=@eK6_tIgRqpRklFrC9 zX%qfASt@^@8N*@@muH#MGNqc&Gqz(Zi7(HiM}<4?!lSTv=FZ>pUU`Yf#%_GJ0CA|; zXFP8H*5tZi&emMUUT$tErDMJ z+;i^VpFh^pJ5x)_dVDVIsN(2=e0s6aS&+T8RUuOPSS5crU(A8b-pAK`>S1vffJz}K zJcwzcf4of#i6(9RoeD*2a2~a*>zO*gSPj4i#dKg|0n43Z>iHW2+p(8+am0HB*`c0# z>SlXc5Bg7ZA5k@{bsC8d{D=d31qQ&44fSljqluhP>d}?rfaDCbkv2jhHd)x#1!Frqh zfJ4~{m!f<%*EBOvlkCSzulOB^?AW7x*Ucb1|Jx<+Pu4H{B`l;=p-uhgN9A;lZFFYT z<+o-7r=K30%s9Tp2g*DbLCf31A>?mD_0Ip$WUd+;2#w$F5FiB*$*4_^4 z^{+3r9~07E;{9#Xm7c%_E4ZQSekKX3l3XR%;U^$)x**r{HsEijjkf!|6f(`By6KzJ zmfiJG=i*9WY}9D1>VTNa&p)DT8w-^b_sE%(6|b9Y{cKNK=JiwmleoRT(HQDFXH#r7 zEce$+(MdMJt$C@uwVVTjl+>MkSn>(ATj;Ngm4GRhmCuLYGZb`diqlMDfi-vtap!7j zANsmy5(Ele4+mo;6e-W*G4h7HF~rr(la4cD@5uiAH&fvOg^!o_c*im~tVg`mW1Y!Z83M z$ocb~=%0Ck#}%yQ*8NHVRGcf#5pKuC&7a~~JHaeZF;t9uT~bC37F1y*|Mm}Cc*(24 zt%nqy@Pv)mdk{t6Bkj&X+l_sa*bpsLOoXockmq)`nyu1gdcUCD{csRoWcW7?DYG0Q zhIc0PyM8=pEefxhU#TS^znH~Gi3&+w1xmfkv1XlH;d4LWQoIKr=AQTj*pF*H&XF6K zD{?#Y`a{lVpA@UqDQ=x}n8Mg90W2Tv#Dd{I>k=4z0eMMg( zt4e~W!+j8l0Qa^RK)TNb5Q$c7Y{Bf?6}MV*`;SOC45M$=&jP|uPS@x;6DGH0GvS>) znVWrQMlS@Ez#PAOQEYWoc~kdm5gmi`i}1u*9~3`FK`1pH4E3o*%+m}ohX{ZN^$cPz zE?H3uEhSk7FJ&;WQ*wGuObGr)e6M5viQPd6+n-}tb8F2 zvgT>|IZgLZXsOT8a-aoP04YrYoL^Ii+>TD%n7S+POrXnA?>QUVXCT{)z|OK>A~N-Z zU6(_Zc-B9?B60m>k z&G|4&W~xhlZj(mNW_7oS^EKyso^OTDfNO@V>3FDI96Wxvi>QeSAa?Nz<4VASJ^bJ0 zgGW_``CJ;aF&NWS*;pA$XaiPRf@5Lao}hXDGtv-Ea5?Cbp-SB)kh zug8@OHpr&gD=nF7yj^}TTRV4)9>{($msrPOrNO-oqLD-oPr@Hr2QzPUhw!! zvS|jY!sw*?O~7r!%X|>tg+mp19O`PI=DAHd=FWY<8~26+835GUfgTq&k~B!%Op{Qwsp@(qs)7QWV*|G zOvje>TQ^e?ZkHlp06p4-wk`|!zBOdm=}QXS?bPSxgihg@W<~We!(QyeIR~bhZP0jq z^l63^;tiUwN<)u{np$Pf7?}1$ON2GxnHbky%k4{1CaG*)vN}A*{(XO{8GIko-;Ml1D85rY;?!$u{HqR(8(8V zpePzT-5Ewwcb{QJ=xO6tHB${z4@1%MtfXw7Q1@T z1=^3F3GWgX(7^ayEcA!eJ1{`SY_;NVg|L1AA|wXW7dywM^NpC<{+;iCTy?v`5k^br ziOBdFUu_~DBVua z6~FB}k|?r4sVXl1ddWr~HWvOFK>#wEipP@uJ2O5!ZUv{3<;y+wja2H6-X-@VmfkXy6De45 zxc90eU3^|1ANDdNJ-Jc#;8Ckm)twqyu^Wx*jgn~^(yNqHV%kYFp!#kRXo|}b^T^a? z9q><8!!G9d%gj1q3QnnWQR)EbGvV1au#k?D%h1(3ifk5-r;%`l*f??f)E~*Dl69=y zc{j0G3y0EB15D*yHPTtDcXQyxk!G5N7__VX8gL!fj+s^#ft5RGU!M4_nKmyHC_g|n z=8YC;aU-J49S)Y-zh5l!D%=7is#6X@*bDBz1hn4fFB@U}2zCpI$CbB0Ai|qnfKc&XH{QcT@aYDNk&6el4iD^3jy-ilk+iIv1MYIuYHHQ z_zed2`{Bdg$5ypjPLtv3w8TP&Re7~8GPRu5R-%L$5atQ|%Y+A-8{>1xtx}-SI`!}o z9v#BvV)pAe=hzj=w5FEcGF-U?D7OwCH_*4p1DEH+r4JaJp zaIRq)Dg3(K^qWj8*4zuBEiZX-P-{v#STNK4&;ikfRPi_1e4zD|@M!5G=2MImIq?eg zuO4PW&vT|Liz-V0oaCQVs%q$!vJXra?r&QmO%f)LPPv$2;|bKd#i);+S9f$^f~B2M z8bjX`LLLcwz13B{!=#aGH#S+3ovuEyW-4Nk^KW12(qhTTnW1lnYUSLG2qWLXMReM9 z5(mHym{E+>|8uXn@PoeQ_siH?XZei?gw_H{e0lgE3HhcO0gYS4Cop7JJtk;~2-SL8 zJt+Bgc32m<>RzZ1V(Nvl6Ioo(@9&==JIx6SoP#|7fEfmmod|~zOkYGuLo{IDYM`a@ zDzj}51Y#>C)6@{0Sw3~aJ@O;mJgOf0nX15rI@Z78A9YfGbz@Ydo2fYc3eOHpOB4UY z!f@;680-GQP zc3#e^?uWk~nbcQ)7|-wC{LdvR-6b*Bb7zj(I#6tHHxay2TTp{9g?g&WQty%)TB%O} z2gT~?S_|PLH?lAj3JqlLt66SKM)a28(Jvcn!>&j*ZA+Vg(c|Qs(d2$s{o_hz9(Z!i`(e(#uUp1FZX>3{Mp7>GzS( zt_gr{!J6mNSegynnbi~F*ye6$zyCDBbjU3ye*Tq}7Dy!X5urLm8eL@NMlU169aCr+ z0KftCfYIJ^2l7W(*){9>h1N~(HYmXD$1vOm%a~ia9f95^-L_oCRgQysilDV(`DJlI z?(&{l^0V8~hQR1cd^q5Q zmz#h@IcsSZ5bCAK?WcHfAVmG>o{5&cnYFwW0PchXA4~yd01F~gBW6VbCrT@Y3pAH? z%K`0QvrvdjXS<}9NVezg-ns^WX}-Du?1#WZ*t8k{nKDa>_z!I}V3JYb!_L(C>J0!# z%f3d+II`>d%2=(WFfT3+v@Ze!TAu)`-~Rn?XXpP@XK;GcC%W<_kB4;zxEm5HYZ(PO j5g}oL=g)+Mp9%9leJUfHLIE|ty*MR#b-6N`SMUE1oe>mv literal 0 HcmV?d00001 diff --git a/Web App/web/public/gsmnode-mark.svg b/Web App/web/public/gsmnode-mark.svg new file mode 100644 index 0000000..d8de483 --- /dev/null +++ b/Web App/web/public/gsmnode-mark.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Web App/web/public/icon-192.png b/Web App/web/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..616b0e0a7bf8a8f4a454485bd44234a70528d6b6 GIT binary patch literal 4246 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 literal 0 HcmV?d00001 diff --git a/Web App/web/public/icon-512.png b/Web App/web/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..2d94601849a7de6d5c4cda528a4963ded4ed2430 GIT binary patch literal 11170 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 + + diff --git a/Web App/web/src/api.js b/Web App/web/src/api.js new file mode 100644 index 0000000..d438201 --- /dev/null +++ b/Web App/web/src/api.js @@ -0,0 +1,68 @@ +// Thin fetch wrapper around the Web App BFF (same-origin /api, proxied to the +// API Server). Handles bearer-token auth and JSON encoding/decoding. + +const TOKEN_KEY = "sms_gw_token"; +const API_BASE_KEY = "sms_gw_api_base"; + +export function getToken() { + return localStorage.getItem(TOKEN_KEY) || ""; +} + +export function setToken(token) { + if (token) localStorage.setItem(TOKEN_KEY, token); + else localStorage.removeItem(TOKEN_KEY); +} + +// Optional API Server base URL. When set, the browser calls that server +// directly (the server sends permissive CORS). When blank, requests go to this +// site's own origin at /api, i.e. through the Web App BFF proxy. +export function getApiBase() { + return localStorage.getItem(API_BASE_KEY) || ""; +} + +export function setApiBase(url) { + const v = (url || "").trim().replace(/\/+$/, ""); + if (v) localStorage.setItem(API_BASE_KEY, v); + else localStorage.removeItem(API_BASE_KEY); +} + +export class ApiError extends Error { + constructor(status, message) { + super(message); + this.status = status; + } +} + +async function request(method, path, body) { + const headers = {}; + const token = getToken(); + if (token) headers.Authorization = "Bearer " + token; + if (body !== undefined) headers["Content-Type"] = "application/json"; + + const res = await fetch(getApiBase() + "/api" + path, { + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + + const text = await res.text(); + let data = null; + try { + data = text ? JSON.parse(text) : null; + } catch { + data = text; + } + + if (!res.ok) { + const msg = (data && data.error) || res.statusText || "Request failed"; + throw new ApiError(res.status, msg); + } + return data; +} + +export const api = { + get: (p) => request("GET", p), + post: (p, b) => request("POST", p, b), + patch: (p, b) => request("PATCH", p, b), + del: (p) => request("DELETE", p), +}; diff --git a/Web App/web/src/components/ApiStatus.vue b/Web App/web/src/components/ApiStatus.vue new file mode 100644 index 0000000..7e3f519 --- /dev/null +++ b/Web App/web/src/components/ApiStatus.vue @@ -0,0 +1,67 @@ + + + diff --git a/Web App/web/src/components/PageHeader.vue b/Web App/web/src/components/PageHeader.vue new file mode 100644 index 0000000..476d76b --- /dev/null +++ b/Web App/web/src/components/PageHeader.vue @@ -0,0 +1,13 @@ + + + diff --git a/Web App/web/src/components/StatusBadge.vue b/Web App/web/src/components/StatusBadge.vue new file mode 100644 index 0000000..7a82d8a --- /dev/null +++ b/Web App/web/src/components/StatusBadge.vue @@ -0,0 +1,25 @@ + + + diff --git a/Web App/web/src/main.js b/Web App/web/src/main.js new file mode 100644 index 0000000..01b3dd4 --- /dev/null +++ b/Web App/web/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; +import "./style.css"; + +createApp(App).use(router).mount("#app"); diff --git a/Web App/web/src/router.js b/Web App/web/src/router.js new file mode 100644 index 0000000..1011cec --- /dev/null +++ b/Web App/web/src/router.js @@ -0,0 +1,46 @@ +import { createRouter, createWebHistory } from "vue-router"; +import { auth } from "./store/auth"; + +import Login from "./views/Login.vue"; +import Layout from "./views/Layout.vue"; +import Devices from "./views/Devices.vue"; +import Send from "./views/Send.vue"; +import Call from "./views/Call.vue"; +import Messages from "./views/Messages.vue"; +import Inbox from "./views/Inbox.vue"; +import Webhooks from "./views/Webhooks.vue"; + +const routes = [ + { path: "/login", name: "login", component: Login }, + { + path: "/", + component: Layout, + meta: { requiresAuth: true }, + children: [ + { path: "", redirect: { name: "devices" } }, + { path: "devices", name: "devices", component: Devices }, + { path: "send", name: "send", component: Send }, + { path: "call", name: "call", component: Call }, + { path: "messages", name: "messages", component: Messages }, + { path: "inbox", name: "inbox", component: Inbox }, + { path: "webhooks", name: "webhooks", component: Webhooks }, + ], + }, + { path: "/:pathMatch(.*)*", redirect: "/" }, +]; + +const router = createRouter({ + history: createWebHistory(), + routes, +}); + +router.beforeEach((to) => { + if (to.meta.requiresAuth && !auth.isAuthenticated()) { + return { name: "login", query: { redirect: to.fullPath } }; + } + if (to.name === "login" && auth.isAuthenticated()) { + return { path: "/" }; + } +}); + +export default router; diff --git a/Web App/web/src/store/auth.js b/Web App/web/src/store/auth.js new file mode 100644 index 0000000..5f48ac7 --- /dev/null +++ b/Web App/web/src/store/auth.js @@ -0,0 +1,33 @@ +import { reactive, readonly } from "vue"; +import { api, setToken, getToken } from "../api"; + +const USER_KEY = "sms_gw_user"; + +const state = reactive({ + user: JSON.parse(localStorage.getItem(USER_KEY) || "null"), + token: getToken(), +}); + +export const auth = { + state: readonly(state), + + isAuthenticated() { + return !!state.token; + }, + + async login(email, password) { + const res = await api.post("/auth/login", { email, password }); + state.token = res.access_token; + state.user = res.user; + setToken(res.access_token); + localStorage.setItem(USER_KEY, JSON.stringify(res.user)); + return res.user; + }, + + logout() { + state.token = ""; + state.user = null; + setToken(""); + localStorage.removeItem(USER_KEY); + }, +}; diff --git a/Web App/web/src/style.css b/Web App/web/src/style.css new file mode 100644 index 0000000..1c712a9 --- /dev/null +++ b/Web App/web/src/style.css @@ -0,0 +1,336 @@ +/* gsmnode design system — tokens from Design/SMS Gateway logo design/tokens/*, + mapped into Tailwind v4. Signal-green + ink, developer-tool aesthetic. */ +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap'); +@import "tailwindcss"; + +/* ============================================================ + RAW RAMPS + SEMANTIC ALIASES (light) + ============================================================ */ +:root { + /* Signal green (brand) — shared chroma, stepped lightness */ + --green-100: oklch(0.95 0.04 152); + --green-200: oklch(0.88 0.07 152); + --green-300: oklch(0.78 0.11 152); + --green-500: oklch(0.60 0.14 152); /* primary */ + --green-600: oklch(0.52 0.13 152); + --green-700: oklch(0.44 0.11 152); + --green-on-dark: oklch(0.72 0.14 152); + + /* Ink & paper */ + --ink: #12161C; + --ink-900: #0A0D11; + --ink-800: #1A1F27; + --paper: #FAFAF9; + --white: #FFFFFF; + + /* Cool gray ramp */ + --gray-50: #F4F5F4; + --gray-100: #E9EBEA; + --gray-200: #DCDFDE; + --gray-300: #C2C7C6; + --gray-400: #9AA0A2; + --gray-500: #6B7278; + --gray-600: #4A5157; + --gray-700: #333A40; + + /* Semantic status */ + --success: oklch(0.60 0.14 152); /* green — also "delivered" */ + --warning: oklch(0.72 0.15 75); + --danger: oklch(0.60 0.17 25); + --info: oklch(0.62 0.13 245); + --success-x: oklch(0.95 0.04 152); + --warning-x: oklch(0.95 0.05 80); + --danger-x: oklch(0.95 0.05 25); + --info-x: oklch(0.95 0.04 245); + + /* Semantic aliases — reference these in components */ + --bg-page: var(--paper); + --bg-sunken: var(--gray-50); + --surface-card: var(--white); + --surface-raised: var(--white); + --surface-inverse:var(--ink); + --surface-code: var(--ink-900); + + --border-subtle: rgba(18, 22, 28, 0.08); + --border-strong: rgba(18, 22, 28, 0.20); + --border-focus: var(--green-500); + + --text-primary: var(--ink); + --text-secondary: var(--gray-700); + --text-muted: var(--gray-500); + --text-inverse: #F5F6F4; + --text-brand: var(--green-600); + --text-link: var(--green-600); + + --brand: var(--green-500); + --brand-hover: var(--green-600); + --brand-active: var(--green-700); + --brand-tint: var(--green-100); + --brand-contrast: var(--white); + --brand-on-dark: var(--green-on-dark); + + --success-tint: var(--success-x); + --warning-tint: var(--warning-x); + --danger-tint: var(--danger-x); + --info-tint: var(--info-x); + + --ring-focus: 0 0 0 3px oklch(0.60 0.14 152 / 0.45); + --ring-danger: 0 0 0 3px oklch(0.60 0.17 25 / 0.35); + + /* Flat, low-contrast elevation */ + --sh-xs: 0 1px 2px rgba(18, 22, 28, 0.04); + --sh-sm: 0 1px 2px rgba(18, 22, 28, 0.06), 0 1px 1px rgba(18, 22, 28, 0.04); + --sh-md: 0 4px 12px rgba(18, 22, 28, 0.08); + --sh-lg: 0 8px 24px rgba(18, 22, 28, 0.14); + --sh-xl: 0 16px 40px rgba(18, 22, 28, 0.18); + + --dur-instant: 100ms; + --dur-fast: 120ms; + --dur-base: 180ms; + --dur-slow: 260ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-entrance: cubic-bezier(0.16, 1, 0.3, 1); +} + +/* ============================================================ + DARK THEME — only the semantic layer remaps. + Namespaced data-gsm-theme so it matches the design system. + ============================================================ */ +[data-gsm-theme="dark"] { + --bg-page: var(--ink-900); + --bg-sunken: #0E1216; + --surface-card: #14191F; + --surface-raised: #1A2027; + --surface-inverse:var(--white); + --surface-code: #05070A; + + --border-subtle: rgba(255, 255, 255, 0.07); + --border-strong: rgba(255, 255, 255, 0.18); + --border-focus: var(--green-on-dark); + + --text-primary: #F5F6F4; + --text-secondary: var(--gray-200); + --text-muted: var(--gray-400); + --text-inverse: var(--ink); + --text-brand: var(--green-on-dark); + --text-link: var(--green-on-dark); + + --brand: var(--green-500); + --brand-hover: var(--green-on-dark); + --brand-active: var(--green-600); + --brand-tint: oklch(0.60 0.14 152 / 0.16); + --brand-contrast: var(--white); + + --success: var(--green-300); + --warning: oklch(0.80 0.14 80); + --danger: oklch(0.72 0.16 25); + --info: oklch(0.72 0.12 245); + --success-tint: oklch(0.60 0.14 152 / 0.16); + --warning-tint: oklch(0.72 0.15 75 / 0.16); + --danger-tint: oklch(0.60 0.17 25 / 0.18); + --info-tint: oklch(0.62 0.13 245 / 0.16); + + --ring-focus: 0 0 0 3px oklch(0.72 0.14 152 / 0.45); + + --sh-xs: 0 1px 2px rgba(0, 0, 0, 0.4); + --sh-sm: 0 1px 3px rgba(0, 0, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.4); + --sh-md: 0 4px 12px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.4); + --sh-lg: 0 8px 24px rgba(0, 0, 0, 0.6), 0 4px 8px rgba(0, 0, 0, 0.4); + --sh-xl: 0 16px 40px rgba(0, 0, 0, 0.7); + + color-scheme: dark; +} + +/* ============================================================ + TAILWIND THEME — utilities resolve to the semantic vars, + so everything flips automatically under data-gsm-theme="dark". + ============================================================ */ +@theme inline { + --color-*: initial; + + --color-page: var(--bg-page); + --color-sunken: var(--bg-sunken); + --color-card: var(--surface-card); + --color-raised: var(--surface-raised); + --color-inverse: var(--surface-inverse); + --color-code: var(--surface-code); + + --color-subtle: var(--border-subtle); + --color-strong: var(--border-strong); + + --color-primary: var(--text-primary); + --color-secondary: var(--text-secondary); + --color-muted: var(--text-muted); + --color-on-brand: var(--brand-contrast); + --color-link: var(--text-link); + + --color-brand: var(--brand); + --color-brand-hover: var(--brand-hover); + --color-brand-active: var(--brand-active); + --color-brand-tint: var(--brand-tint); + --color-brand-text: var(--text-brand); + + --color-success: var(--success); + --color-success-tint: var(--success-tint); + --color-warning: var(--warning); + --color-warning-tint: var(--warning-tint); + --color-danger: var(--danger); + --color-danger-tint: var(--danger-tint); + --color-info: var(--info); + --color-info-tint: var(--info-tint); + + --color-white: #FFFFFF; + --color-ink: var(--ink); + --color-green: var(--green-500); + + --font-display: 'Space Grotesk', ui-sans-serif, system-ui, 'Segoe UI', sans-serif; + --font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace; + + --radius-*: initial; + --radius-xs: 4px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 15px; + --radius-2xl: 18px; + --radius-full: 9999px; + + --shadow-*: initial; + --shadow-xs: var(--sh-xs); + --shadow-sm: var(--sh-sm); + --shadow-md: var(--sh-md); + --shadow-lg: var(--sh-lg); + --shadow-xl: var(--sh-xl); + --shadow-ring: var(--ring-focus); +} + +/* ============================================================ + BASE + ============================================================ */ +html, +body, +#app { + height: 100%; +} + +body { + font-family: var(--font-sans); + background: var(--bg-page); + color: var(--text-primary); + -webkit-font-smoothing: antialiased; +} + +/* Display face for titles & metrics */ +h1, h2, h3 { + font-family: var(--font-display); + letter-spacing: -0.02em; +} + +/* Shared control styles (inputs, selects, textareas) */ +@utility gn-input { + width: 100%; + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-card); + color: var(--text-primary); + padding: 0 12px; + height: 40px; + font-size: 0.875rem; + font-family: var(--font-sans); + transition: border-color var(--dur-fast) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); +} + +@utility gn-textarea { + width: 100%; + border: 1px solid var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-card); + color: var(--text-primary); + padding: 10px 12px; + font-size: 0.875rem; + font-family: var(--font-sans); + resize: vertical; + transition: border-color var(--dur-fast) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); +} + +.gn-input:focus, +.gn-textarea:focus { + outline: none; + border-color: var(--border-focus); + box-shadow: var(--ring-focus); +} + +.gn-input::placeholder, +.gn-textarea::placeholder { + color: var(--text-muted); +} + +/* Primary button — flat signal-green, no glow (infrastructure) */ +@utility gn-btn-pri { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 40px; + padding: 0 16px; + border-radius: var(--radius-md); + border: 1px solid transparent; + background: var(--brand); + color: var(--brand-contrast); + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease-standard), + transform var(--dur-fast) var(--ease-standard); +} + +.gn-btn-pri:hover:not(:disabled) { background: var(--brand-hover); } +.gn-btn-pri:active:not(:disabled) { background: var(--brand-active); transform: translateY(1px); } +.gn-btn-pri:focus-visible { outline: none; box-shadow: var(--ring-focus); } +.gn-btn-pri:disabled { opacity: 0.45; cursor: not-allowed; } + +/* Secondary button */ +@utility gn-btn-sec { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 40px; + padding: 0 16px; + border-radius: var(--radius-md); + border: 1px solid var(--border-strong); + background: var(--surface-card); + color: var(--text-primary); + font-family: var(--font-sans); + font-weight: 600; + font-size: 0.875rem; + cursor: pointer; + transition: background-color var(--dur-fast) var(--ease-standard), + transform var(--dur-fast) var(--ease-standard); +} + +.gn-btn-sec:hover:not(:disabled) { background: var(--bg-sunken); } +.gn-btn-sec:active:not(:disabled) { transform: translateY(1px); } +.gn-btn-sec:focus-visible { outline: none; box-shadow: var(--ring-focus); } +.gn-btn-sec:disabled { opacity: 0.45; cursor: not-allowed; } + +/* Small button size modifier */ +@utility gn-btn-sm { + height: 32px; + padding: 0 12px; + font-size: 0.75rem; + border-radius: var(--radius-sm); +} + +/* Mono eyebrow — uppercase, tracked out */ +@utility gn-eyebrow { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-muted); +} diff --git a/Web App/web/src/theme.js b/Web App/web/src/theme.js new file mode 100644 index 0000000..b8dae4d --- /dev/null +++ b/Web App/web/src/theme.js @@ -0,0 +1,30 @@ +import { ref } from "vue"; + +// Persisted light/dark theme, shared key with the design-system kits. +const KEY = "gsmnode-theme"; + +function initial() { + try { + return localStorage.getItem(KEY) === "dark" ? "dark" : "light"; + } catch { + return "light"; + } +} + +export const theme = ref(initial()); + +export function applyTheme(t) { + theme.value = t; + document.documentElement.setAttribute("data-gsm-theme", t); + try { + localStorage.setItem(KEY, t); + } catch { + /* private mode — theme just won't persist */ + } +} + +export function toggleTheme() { + applyTheme(theme.value === "dark" ? "light" : "dark"); +} + +applyTheme(theme.value); diff --git a/Web App/web/src/views/Call.vue b/Web App/web/src/views/Call.vue new file mode 100644 index 0000000..84e5196 --- /dev/null +++ b/Web App/web/src/views/Call.vue @@ -0,0 +1,94 @@ + + + diff --git a/Web App/web/src/views/Devices.vue b/Web App/web/src/views/Devices.vue new file mode 100644 index 0000000..5cb67bd --- /dev/null +++ b/Web App/web/src/views/Devices.vue @@ -0,0 +1,90 @@ + + + diff --git a/Web App/web/src/views/Inbox.vue b/Web App/web/src/views/Inbox.vue new file mode 100644 index 0000000..3d6b921 --- /dev/null +++ b/Web App/web/src/views/Inbox.vue @@ -0,0 +1,69 @@ + + + diff --git a/Web App/web/src/views/Layout.vue b/Web App/web/src/views/Layout.vue new file mode 100644 index 0000000..b056fff --- /dev/null +++ b/Web App/web/src/views/Layout.vue @@ -0,0 +1,99 @@ + + + diff --git a/Web App/web/src/views/Login.vue b/Web App/web/src/views/Login.vue new file mode 100644 index 0000000..e850e0e --- /dev/null +++ b/Web App/web/src/views/Login.vue @@ -0,0 +1,115 @@ + + + diff --git a/Web App/web/src/views/Messages.vue b/Web App/web/src/views/Messages.vue new file mode 100644 index 0000000..cd73cd6 --- /dev/null +++ b/Web App/web/src/views/Messages.vue @@ -0,0 +1,89 @@ + + + diff --git a/Web App/web/src/views/Send.vue b/Web App/web/src/views/Send.vue new file mode 100644 index 0000000..e1b9fd0 --- /dev/null +++ b/Web App/web/src/views/Send.vue @@ -0,0 +1,117 @@ + + + diff --git a/Web App/web/src/views/Webhooks.vue b/Web App/web/src/views/Webhooks.vue new file mode 100644 index 0000000..c7371ee --- /dev/null +++ b/Web App/web/src/views/Webhooks.vue @@ -0,0 +1,120 @@ + + + diff --git a/Web App/web/vite.config.js b/Web App/web/vite.config.js new file mode 100644 index 0000000..139e43a --- /dev/null +++ b/Web App/web/vite.config.js @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import tailwindcss from "@tailwindcss/vite"; + +// The production build is written into ../server/dist so the Go BFF can embed it. +// In dev, /api is proxied to the API Server directly. +export default defineConfig({ + plugins: [vue(), tailwindcss()], + build: { + outDir: "../server/dist", + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + "/api": { + target: process.env.API_BASE || "http://localhost:8080", + changeOrigin: true, + }, + }, + }, +});