a158 1 ヶ月 前
コミット
91b14c8bd7
100 ファイル変更6615 行追加0 行削除
  1. 6 0
      .gitignore
  2. 278 0
      API.md
  3. 63 0
      CMakeLists.txt
  4. 14 0
      br.sh
  5. 654 0
      data/www/assets/app.css
  6. 2586 0
      data/www/assets/app.js
  7. 2078 0
      data/www/assets/bootstrap-icons/bootstrap-icons.css
  8. BIN
      data/www/assets/bootstrap-icons/fonts/bootstrap-icons.woff
  9. BIN
      data/www/assets/bootstrap-icons/fonts/bootstrap-icons.woff2
  10. 4 0
      data/www/assets/bootstrap/css/bootstrap.min.css
  11. 5 0
      data/www/assets/bootstrap/js/bootstrap.bundle.min.js
  12. 6 0
      data/www/assets/chartjs/chart.umd.min.js
  13. BIN
      data/www/assets/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf
  14. 5 0
      data/www/assets/monaco/vs/base/worker/workerMain.js
  15. 7 0
      data/www/assets/monaco/vs/basic-languages/abap/abap.js
  16. 7 0
      data/www/assets/monaco/vs/basic-languages/apex/apex.js
  17. 10 0
      data/www/assets/monaco/vs/basic-languages/azcli/azcli.js
  18. 7 0
      data/www/assets/monaco/vs/basic-languages/bat/bat.js
  19. 11 0
      data/www/assets/monaco/vs/basic-languages/bicep/bicep.js
  20. 7 0
      data/www/assets/monaco/vs/basic-languages/cameligo/cameligo.js
  21. 7 0
      data/www/assets/monaco/vs/basic-languages/clojure/clojure.js
  22. 7 0
      data/www/assets/monaco/vs/basic-languages/coffee/coffee.js
  23. 7 0
      data/www/assets/monaco/vs/basic-languages/cpp/cpp.js
  24. 7 0
      data/www/assets/monaco/vs/basic-languages/csharp/csharp.js
  25. 10 0
      data/www/assets/monaco/vs/basic-languages/csp/csp.js
  26. 8 0
      data/www/assets/monaco/vs/basic-languages/css/css.js
  27. 7 0
      data/www/assets/monaco/vs/basic-languages/cypher/cypher.js
  28. 7 0
      data/www/assets/monaco/vs/basic-languages/dart/dart.js
  29. 7 0
      data/www/assets/monaco/vs/basic-languages/dockerfile/dockerfile.js
  30. 7 0
      data/www/assets/monaco/vs/basic-languages/ecl/ecl.js
  31. 7 0
      data/www/assets/monaco/vs/basic-languages/elixir/elixir.js
  32. 7 0
      data/www/assets/monaco/vs/basic-languages/flow9/flow9.js
  33. 9 0
      data/www/assets/monaco/vs/basic-languages/freemarker2/freemarker2.js
  34. 7 0
      data/www/assets/monaco/vs/basic-languages/fsharp/fsharp.js
  35. 7 0
      data/www/assets/monaco/vs/basic-languages/go/go.js
  36. 7 0
      data/www/assets/monaco/vs/basic-languages/graphql/graphql.js
  37. 7 0
      data/www/assets/monaco/vs/basic-languages/handlebars/handlebars.js
  38. 7 0
      data/www/assets/monaco/vs/basic-languages/hcl/hcl.js
  39. 7 0
      data/www/assets/monaco/vs/basic-languages/html/html.js
  40. 10 0
      data/www/assets/monaco/vs/basic-languages/ini/ini.js
  41. 7 0
      data/www/assets/monaco/vs/basic-languages/java/java.js
  42. 7 0
      data/www/assets/monaco/vs/basic-languages/javascript/javascript.js
  43. 7 0
      data/www/assets/monaco/vs/basic-languages/julia/julia.js
  44. 7 0
      data/www/assets/monaco/vs/basic-languages/kotlin/kotlin.js
  45. 7 0
      data/www/assets/monaco/vs/basic-languages/less/less.js
  46. 7 0
      data/www/assets/monaco/vs/basic-languages/lexon/lexon.js
  47. 7 0
      data/www/assets/monaco/vs/basic-languages/liquid/liquid.js
  48. 7 0
      data/www/assets/monaco/vs/basic-languages/lua/lua.js
  49. 7 0
      data/www/assets/monaco/vs/basic-languages/m3/m3.js
  50. 7 0
      data/www/assets/monaco/vs/basic-languages/markdown/markdown.js
  51. 7 0
      data/www/assets/monaco/vs/basic-languages/mdx/mdx.js
  52. 7 0
      data/www/assets/monaco/vs/basic-languages/mips/mips.js
  53. 7 0
      data/www/assets/monaco/vs/basic-languages/msdax/msdax.js
  54. 7 0
      data/www/assets/monaco/vs/basic-languages/mysql/mysql.js
  55. 7 0
      data/www/assets/monaco/vs/basic-languages/objective-c/objective-c.js
  56. 7 0
      data/www/assets/monaco/vs/basic-languages/pascal/pascal.js
  57. 7 0
      data/www/assets/monaco/vs/basic-languages/pascaligo/pascaligo.js
  58. 7 0
      data/www/assets/monaco/vs/basic-languages/perl/perl.js
  59. 7 0
      data/www/assets/monaco/vs/basic-languages/pgsql/pgsql.js
  60. 7 0
      data/www/assets/monaco/vs/basic-languages/php/php.js
  61. 7 0
      data/www/assets/monaco/vs/basic-languages/pla/pla.js
  62. 7 0
      data/www/assets/monaco/vs/basic-languages/postiats/postiats.js
  63. 7 0
      data/www/assets/monaco/vs/basic-languages/powerquery/powerquery.js
  64. 7 0
      data/www/assets/monaco/vs/basic-languages/powershell/powershell.js
  65. 8 0
      data/www/assets/monaco/vs/basic-languages/protobuf/protobuf.js
  66. 7 0
      data/www/assets/monaco/vs/basic-languages/pug/pug.js
  67. 7 0
      data/www/assets/monaco/vs/basic-languages/python/python.js
  68. 7 0
      data/www/assets/monaco/vs/basic-languages/qsharp/qsharp.js
  69. 7 0
      data/www/assets/monaco/vs/basic-languages/r/r.js
  70. 7 0
      data/www/assets/monaco/vs/basic-languages/razor/razor.js
  71. 7 0
      data/www/assets/monaco/vs/basic-languages/redis/redis.js
  72. 7 0
      data/www/assets/monaco/vs/basic-languages/redshift/redshift.js
  73. 7 0
      data/www/assets/monaco/vs/basic-languages/restructuredtext/restructuredtext.js
  74. 7 0
      data/www/assets/monaco/vs/basic-languages/ruby/ruby.js
  75. 7 0
      data/www/assets/monaco/vs/basic-languages/rust/rust.js
  76. 7 0
      data/www/assets/monaco/vs/basic-languages/sb/sb.js
  77. 7 0
      data/www/assets/monaco/vs/basic-languages/scala/scala.js
  78. 7 0
      data/www/assets/monaco/vs/basic-languages/scheme/scheme.js
  79. 8 0
      data/www/assets/monaco/vs/basic-languages/scss/scss.js
  80. 7 0
      data/www/assets/monaco/vs/basic-languages/shell/shell.js
  81. 7 0
      data/www/assets/monaco/vs/basic-languages/solidity/solidity.js
  82. 7 0
      data/www/assets/monaco/vs/basic-languages/sophia/sophia.js
  83. 7 0
      data/www/assets/monaco/vs/basic-languages/sparql/sparql.js
  84. 7 0
      data/www/assets/monaco/vs/basic-languages/sql/sql.js
  85. 7 0
      data/www/assets/monaco/vs/basic-languages/st/st.js
  86. 7 0
      data/www/assets/monaco/vs/basic-languages/swift/swift.js
  87. 7 0
      data/www/assets/monaco/vs/basic-languages/systemverilog/systemverilog.js
  88. 7 0
      data/www/assets/monaco/vs/basic-languages/tcl/tcl.js
  89. 7 0
      data/www/assets/monaco/vs/basic-languages/twig/twig.js
  90. 7 0
      data/www/assets/monaco/vs/basic-languages/typescript/typescript.js
  91. 7 0
      data/www/assets/monaco/vs/basic-languages/typespec/typespec.js
  92. 7 0
      data/www/assets/monaco/vs/basic-languages/vb/vb.js
  93. 307 0
      data/www/assets/monaco/vs/basic-languages/wgsl/wgsl.js
  94. 7 0
      data/www/assets/monaco/vs/basic-languages/xml/xml.js
  95. 7 0
      data/www/assets/monaco/vs/basic-languages/yaml/yaml.js
  96. 5 0
      data/www/assets/monaco/vs/editor/editor.main.css
  97. 5 0
      data/www/assets/monaco/vs/editor/editor.main.js
  98. 7 0
      data/www/assets/monaco/vs/language/css/cssMode.js
  99. 7 0
      data/www/assets/monaco/vs/language/css/cssWorker.js
  100. 7 0
      data/www/assets/monaco/vs/language/html/htmlMode.js

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+################################################################################
+# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
+################################################################################
+
+/out/build/x64-Debug
+/.vs

+ 278 - 0
API.md

@@ -0,0 +1,278 @@
+# NGS API Server
+
+基于 ylib `httpserver` 的 NGS 管理 API。默认监听 **`0.0.0.0:15665`**。
+
+内置可视化控制台目录:`data/www`,浏览器打开:
+
+```text
+http://127.0.0.1:15665/
+```
+
+## 启动
+
+```bash
+./br.sh
+# 或
+./build/ngs
+# 自定义监听
+./build/ngs --listen 0.0.0.0 --port 15665
+```
+
+面板路径可通过环境变量覆盖:`NGS_PANEL_WWW=/path/to/www`。
+
+数据存储:站点元数据、MySQL 账号/密码/权限、root 密码与监听地址保存在 SQLite:`data/database.db`(可用 `NGS_DATABASE` 覆盖)。Nginx 的 `www/conf/vhost/*.conf` 仍为文件。
+
+统一响应格式:
+
+```json
+{
+  "code": 200,
+  "msg": "ok",
+  "data": {}
+}
+```
+
+- `code = 200` 表示成功
+- 失败时 `code` 为负数或 HTTP 语义码(如 `404`/`405`/`401`),`msg` 为错误信息
+
+---
+
+## 登录鉴权
+
+管理面板与 `/api/*`(除登录相关)需登录。账号密码配置文件:
+
+```text
+www/conf/ngs.conf
+```
+
+首次启动若不存在会自动生成,默认:
+
+```text
+username=admin
+password=ngsadmin
+```
+
+修改配置后需**重启 ngs** 生效。浏览器使用 Cookie `ngs_session`。
+
+| 方法 | 路径 | 说明 | Body |
+|------|------|------|------|
+| POST | `/api/auth/login` | 登录 | `{"username":"admin","password":"..."}` |
+| POST | `/api/auth/logout` | 退出 | `{}` |
+| GET | `/api/auth/me` | 当前用户(未登录返回 401) | - |
+
+---
+
+## 系统
+
+### `GET /api/ping`
+
+健康检查。
+
+### `GET /api/status`
+ 
+汇总 Nginx / Fastweb / MySQL / Redis / 网站数量状态(含各组件 `conf` 配置路径)。
+
+### 任务队列
+
+长任务(安装/卸载)进入后台队列,立即返回 `task_id`。状态持久化到 `www/conf/tasks.json`,日志在 `www/conf/tasks/<id>.log`。
+
+| 方法 | 路径 | 说明 |
+|------|------|------|
+| GET | `/api/tasks` | 任务列表与 `active` 数量 |
+| GET | `/api/tasks/log?id=&offset=` | 增量拉取任务日志 |
+| POST | `/api/tasks/clear` | 清理已完成/失败任务 |
+
+安装类接口响应示例:
+
+```json
+{ "code": 200, "msg": "task queued", "data": { "task_id": "t...", "active": 1 } }
+```
+
+### `GET /api/system/metrics`
+
+当前瞬时 CPU / 内存 / 磁盘 / 网络。
+
+### `GET /api/system/metrics/history`
+
+后台每 2 秒采集并写入 `data/metrics.db`,按时间范围查询(默认 `10m`)。
+
+| 参数 | 说明 |
+|------|------|
+| `range` | `1m` / `10m` / `30m` / `1h` / `6h` / `1d` / `7d` / `30d` |
+
+```json
+{
+  "range": "10m",
+  "items": [
+    {
+      "time_ms": 1786430100000,
+      "cpu_percent": 12.5,
+      "cpu_cores": 8,
+      "mem_percent": 40.1,
+      "mem_used": 1000,
+      "mem_total": 2000,
+      "disk_percent": 20.0,
+      "disk_used": 100,
+      "disk_total": 500,
+      "net_rx_bps": 1024.5,
+      "net_tx_bps": 512.0
+    }
+  ]
+}
+```
+
+数据保留约 30 天;长区间会自动降采样。
+
+---
+
+## Redis
+
+| 方法 | 路径 | 说明 | Body |
+|------|------|------|------|
+| GET | `/api/redis/status` | 状态 | - |
+| POST | `/api/redis/install` | 源码安装 | `{"version":"7.2.5"}`(可选) |
+| POST | `/api/redis/uninstall` | 卸载 | - |
+| POST | `/api/redis/start` | 启动 | - |
+| POST | `/api/redis/stop` | 停止 | - |
+
+默认安装目录:`www/software/redis`。默认端口 `6379`,占用时回退 `6380`。
+
+---
+
+## 文件管理
+
+全盘路径,根目录从 `/` 起。
+
+| 方法 | 路径 | 说明 | Body / Query |
+|------|------|------|------|
+| GET | `/api/files/list` | 列目录 | `?path=/` |
+| POST | `/api/files/mkdir` | 新建目录 | `{"path":"/tmp/a"}` |
+| POST | `/api/files/write` | 写文本/新建文件 | `{"path":"/tmp/a.txt","content":"..."}` |
+| GET | `/api/files/read` | 读文本(最大 **10MB**,超限拒绝) | `?path=/tmp/a.txt` |
+| GET | `/api/files/tail` | 读文件尾部(默认 256KB) | `?path=/tmp/a.log&bytes=262144` |
+| POST | `/api/files/rename` | 重命名/移动 | `{"from":"...","to":"..."}` |
+| POST | `/api/files/delete` | 删除 | `{"path":"...","recursive":false}` |
+| POST | `/api/files/upload` | 上传 | `{"path":"...","content_base64":"..."}` 或 `content` |
+| GET | `/api/files/download` | 下载文件 | `?path=/tmp/a.txt`(直接文件流) |
+
+目录删除需 `recursive=true` 才会递归;默认仅删空目录或文件。
+
+在线编辑/预览使用 Monaco(VS Code 同款编辑器),仅支持 **≤10MB** 文本文件;超过 10MB 不可预览、不可编辑(仍可下载)。`POST /api/files/write` 同样拒绝超过 10MB 的内容。
+
+---
+
+## Nginx
+
+| 方法 | 路径 | 说明 | Body |
+|------|------|------|------|
+| GET | `/api/nginx/status` | 状态 | - |
+| POST | `/api/nginx/install` | 安装 | `{"version":"1.26.3"}`(可选) |
+| POST | `/api/nginx/uninstall` | 卸载 | - |
+| POST | `/api/nginx/start` | 启动 | - |
+| POST | `/api/nginx/stop` | 停止 | - |
+| POST | `/api/nginx/reload` | 重载配置 | - |
+
+---
+
+## Fastweb
+
+| 方法 | 路径 | 说明 | Body |
+|------|------|------|------|
+| GET | `/api/fastweb/status` | 状态 | - |
+| POST | `/api/fastweb/install` | 安装 | `{"debug":false}`(可选) |
+| POST | `/api/fastweb/uninstall` | 卸载 | - |
+
+---
+
+## MySQL
+
+| 方法 | 路径 | 说明 | Body |
+|------|------|------|------|
+| GET | `/api/mysql/status` | 状态 | - |
+| POST | `/api/mysql/install` | 安装 | `{"version":"8.0.35"}`(可选) |
+| POST | `/api/mysql/uninstall` | 卸载 | - |
+| POST | `/api/mysql/start` | 启动 | - |
+| POST | `/api/mysql/stop` | 停止 | - |
+| POST | `/api/mysql/root/password` | 修改 root 密码 | `{"password":"..."}` |
+| GET | `/api/mysql/databases` | 数据库列表 | - |
+| POST | `/api/mysql/databases` | 创建数据库 | `{"name":"db","user":"u","password":"p"}` |
+| POST | `/api/mysql/databases/drop` | 删除数据库 | `{"name":"db"}` |
+| POST | `/api/mysql/databases/access` | 设置访问权限 | 见下 |
+
+权限设置 Body:
+
+```json
+{
+  "name": "db",
+  "mode": "local|fixed_ip|any",
+  "host": "1.2.3.4"
+}
+```
+
+- `local`:仅本地
+- `fixed_ip`:固定 IP(需 `host`)
+- `any`:允许外网(`%`)
+
+默认 MySQL 端口:`3307`。
+
+---
+
+## 网站
+
+| 方法 | 路径 | 说明 | Body |
+|------|------|------|------|
+| GET | `/api/websites` | 网站列表 | - |
+| POST | `/api/websites` | 创建网站 | 见下 |
+| POST | `/api/websites/update` | 更新域名/端口/证书 | `{"name","domain"|"domains","listen_port",ssl_*}` |
+| POST | `/api/websites/ssl/apply` | 申请 Let's Encrypt(任务队列,多域名一张证;邮箱自动生成) | `{"name","domains":["a.com","www.a.com"]}` |
+| POST | `/api/websites/start` | 启动 | `{"name":"demo"}` |
+| POST | `/api/websites/stop` | 停止 | `{"name":"demo"}` |
+| POST | `/api/websites/restart` | 重启 | `{"name":"demo"}` |
+| POST | `/api/websites/delete` | 删除网站 | `{"name":"demo","delete_files":false}` |
+| GET | `/api/websites/analytics` | 站点日志分析(SQLite 聚合) | `?name=demo&range=24h`(`1h\|6h\|24h\|7d\|30d\|all`) |
+| GET | `/api/websites/logs` | 访问/错误日志(每站点 SQLite) | `?name=demo&kind=access|error&limit=100&before_id=&status=&ip=&level=` |
+
+访问日志库路径:`data/log/<name>.db`。表 `access_log` / `error_log`;后台从 `www/log/<name>/access.log`、`error.log` 增量入库。
+
+`access_log` 主要字段:`time`(`YYYY-MM-DD HH:MM:SS`)、`time_ms`(毫秒时间戳)、`request_time`(耗时秒)、`bytes_recv`(接收)、`bytes_sent`(发送)。
+
+`delete_files=true` 时同时删除 `wwwroot/<name>` 目录;默认 `false` 仅删配置并保留目录。删除站点时会移除对应 `data/log/<name>.db`。
+
+创建网站 Body:
+
+```json
+{
+  "name": "demo",
+  "type": "static|proxy|fastweb",
+  "domain": "demo.local www.demo.local",
+  "domains": ["demo.local", "www.demo.local"],
+  "listen_port": 8080,
+  "upstream": "127.0.0.1:9000"
+}
+```
+
+- `domain` / `domains` 可省略,默认等于 `name`;支持多个,格式为 `域名` 或 `域名:端口`(默认 80)
+- `listen_port` 可省略,由域名条目解析;未写端口时默认 80
+- `type=proxy` 时 `upstream` 必填
+- `type=fastweb` 时会自动分配后端端口并生成站点配置
+- SSL:`POST /api/websites/ssl/apply` 使用 Let's Encrypt(HTTP-01),域名需解析到本机且 **80 端口可达**
+
+列表示例字段:`name` / `type` / `domain` / `domains` / `listen_port` / `upstream` / `running` / `root` / `log_dir` / `access_log` / `error_log` / `log_db` / `conf` / `ssl_*`
+
+---
+
+## 示例
+
+```bash
+curl -s http://127.0.0.1:15665/api/ping
+curl -s http://127.0.0.1:15665/api/status
+
+curl -s -X POST http://127.0.0.1:15665/api/nginx/install \
+  -H 'Content-Type: application/json' -d '{}'
+
+curl -s -X POST http://127.0.0.1:15665/api/websites \
+  -H 'Content-Type: application/json' \
+  -d '{"name":"demo","type":"static","domain":"demo.local"}'
+
+curl -s http://127.0.0.1:15665/api/websites
+```

+ 63 - 0
CMakeLists.txt

@@ -0,0 +1,63 @@
+cmake_minimum_required(VERSION 3.16)
+project(ngs LANGUAGES CXX)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+add_definitions(-DHPSOCKET_STATIC_LIB)
+
+add_executable(ngs
+    src/main.cpp
+    src/utils.cpp
+    src/api/api_server.cpp
+    src/store/store.cpp
+    src/system/metrics.cpp
+    src/system/metrics_history.cpp
+    src/files/files.cpp
+    src/tasks/tasks.cpp
+    src/software/software.cpp
+    src/software/nginx/nginx.cpp
+    src/software/fastweb/fastweb.cpp
+    src/software/mysql/mysql.cpp
+    src/software/redis/redis.cpp
+    src/website/website.cpp
+    src/ssl/acme.cpp
+    src/auth/auth.cpp
+    src/weblog/weblog.cpp
+)
+
+target_include_directories(ngs PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/src
+    /usr/local/include
+)
+
+target_compile_options(ngs PRIVATE -Wall -Wextra)
+target_compile_definitions(ngs PRIVATE NGS_SOURCE_DIR="${CMAKE_SOURCE_DIR}")
+
+link_directories(/usr/local/lib /usr/lib/x86_64-linux-gnu)
+
+target_link_libraries(ngs PRIVATE
+    ylib
+    hpsocket
+    leveldb
+    sqlite3
+    ssl
+    crypto
+    z
+    pthread
+    dl
+    rt
+    stdc++fs
+)
+
+install(TARGETS ngs RUNTIME DESTINATION bin)
+
+add_custom_target(install-deps
+    COMMAND sudo apt-get update
+    COMMAND sudo DEBIAN_FRONTEND=noninteractive apt-get install -y
+            build-essential libpcre3-dev zlib1g-dev libssl-dev curl tar
+            libleveldb-dev
+    COMMENT "Install build dependencies"
+    VERBATIM
+)

+ 14 - 0
br.sh

@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$ROOT"
+
+BUILD_DIR="${BUILD_DIR:-build}"
+JOBS="${JOBS:-$(nproc 2>/dev/null || echo 4)}"
+
+rm -rf "$BUILD_DIR"
+cmake -S . -B "$BUILD_DIR"
+cmake --build "$BUILD_DIR" -j"$JOBS"
+
+exec "$BUILD_DIR/ngs" "$@"

+ 654 - 0
data/www/assets/app.css

@@ -0,0 +1,654 @@
+:root {
+  --rail: #1e293b;
+  --rail-ink: #e2e8f0;
+  --accent: #0d9488;
+}
+
+html, body {
+  height: 100%;
+  margin: 0;
+  overflow: hidden;
+  background: #f1f5f9;
+}
+
+.app {
+  height: 100%;
+  width: 100%;
+  min-height: 100%;
+}
+
+.rail {
+  width: 220px;
+  background: var(--rail);
+  color: var(--rail-ink);
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  position: sticky;
+  top: 0;
+  flex-shrink: 0;
+}
+
+.brand-mark {
+  width: 32px;
+  height: 32px;
+  border-radius: 8px;
+  display: grid;
+  place-items: center;
+  font-weight: 800;
+  color: #083f3b;
+  background: linear-gradient(145deg, #5eead4, #0f766e);
+}
+
+.nav-item {
+  color: rgba(226, 232, 240, 0.78) !important;
+  border-radius: 8px;
+  padding: 0.45rem 0.75rem !important;
+  border: 0;
+  background: transparent;
+  font-size: 0.92rem;
+}
+.nav-item:hover { background: rgba(255,255,255,.06); color: #fff !important; }
+.nav-item.is-active {
+  background: rgba(13, 148, 136, 0.28) !important;
+  color: #fff !important;
+}
+
+.live .pulse {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #94a3b8;
+  display: inline-block;
+}
+.live.is-ok .pulse { background: #34d399; box-shadow: 0 0 0 3px rgba(52,211,153,.2); }
+.live.is-bad .pulse { background: #f87171; }
+
+.main {
+  flex: 1 1 auto;
+  min-width: 0;
+  width: 100%;
+  max-width: none;
+  height: 100%;
+  padding: 1rem 1.25rem;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.top {
+  flex-shrink: 0;
+  margin-bottom: 0.85rem !important;
+}
+
+.view { display: none; }
+.view.is-active {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  min-height: 0;
+  gap: 0.75rem;
+  overflow: hidden;
+}
+
+/* List pages: stretch card + scroll table */
+.view.is-active > .card {
+  flex: 1 1 auto;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  margin-bottom: 0 !important;
+}
+.view.is-active > .card > .card-header {
+  flex-shrink: 0;
+}
+.view.is-active > .card > .table-responsive {
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow: auto;
+}
+.view.is-active > .card > .card-body {
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow: auto;
+}
+
+/* Console: full width, scroll if needed; cards share space */
+#view-console.is-active {
+  overflow: hidden;
+}
+#view-console.is-active > .card {
+  flex: 1 1 auto;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+#view-console .card-body.min-h-0,
+.min-h-0 {
+  min-height: 0;
+}
+
+#view-software.is-active {
+  overflow: auto;
+}
+#view-software.is-active > .row {
+  width: 100%;
+  margin-left: 0;
+  margin-right: 0;
+}
+
+.metrics-range {
+  width: auto;
+  min-width: 5.5rem;
+  font-size: 0.8rem;
+  padding-top: 0.15rem;
+  padding-bottom: 0.15rem;
+}
+.analytics-stat {
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-radius: 10px;
+  padding: 0.7rem 0.85rem;
+  height: 100%;
+}
+.analytics-stat .label {
+  font-size: 0.75rem;
+  color: #64748b;
+  margin-bottom: 0.2rem;
+}
+.analytics-stat .value {
+  font-size: 1.05rem;
+  font-weight: 700;
+  color: #0f172a;
+  word-break: break-all;
+}
+.analytics-panel {
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  padding: 0.85rem 1rem;
+  background: #fff;
+  height: 100%;
+}
+.analytics-panel-title {
+  font-weight: 600;
+  font-size: 0.9rem;
+  margin-bottom: 0.65rem;
+}
+.analytics-chart {
+  position: relative;
+  height: 220px;
+}
+.analytics-chart-sm {
+  height: 200px;
+}
+.monitor-card {
+  height: 100%;
+  min-height: 220px;
+  padding: 0.85rem 1rem;
+  background: #fff;
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  display: flex;
+  flex-direction: column;
+}
+.monitor-chart {
+  position: relative;
+  flex: 1 1 auto;
+  min-height: 160px;
+}
+.monitor-chart canvas {
+  width: 100% !important;
+  height: 100% !important;
+}
+
+.soft-card .card-body { padding: 0.75rem 0.9rem; }
+.soft-card .meta {
+  font-size: 0.8rem;
+  color: #64748b;
+  margin: 0;
+  padding-left: 1rem;
+}
+.soft-card .btn { --bs-btn-padding-y: 0.2rem; --bs-btn-padding-x: 0.5rem; --bs-btn-font-size: 0.8rem; }
+
+.table > :not(caption) > * > * {
+  padding: 0.4rem 0.65rem;
+  font-size: 0.875rem;
+}
+.table thead th {
+  font-size: 0.78rem;
+  text-transform: uppercase;
+  letter-spacing: 0.02em;
+  color: #64748b;
+  background: #f8fafc;
+}
+
+.file-icon {
+  width: 1.15rem;
+  display: inline-flex;
+  justify-content: center;
+  margin-right: 0.35rem;
+  color: #64748b;
+}
+.file-icon.dir { color: #ca8a04; }
+.file-icon.code { color: #2563eb; }
+.file-icon.web { color: #ea580c; }
+.file-icon.img { color: #db2777; }
+.file-icon.cfg { color: #0f766e; }
+.file-icon.doc { color: #475569; }
+
+.files-name {
+  cursor: pointer;
+  color: #0f172a;
+  text-decoration: none;
+}
+.files-name:hover { color: var(--accent); text-decoration: underline; }
+
+.crumbs a, .crumbs button {
+  appearance: none;
+  border: 0;
+  background: transparent;
+  color: var(--accent);
+  font: inherit;
+  padding: 0;
+  cursor: pointer;
+}
+.crumbs span { color: #94a3b8; margin: 0 0.2rem; }
+
+.toast-host {
+  z-index: 120;
+  pointer-events: none;
+}
+.toast-host .toast-item {
+  pointer-events: none;
+  background: #0f172a;
+  color: #fff;
+  border-radius: 8px;
+  padding: 0.55rem 0.85rem;
+  margin-top: 0.4rem;
+  font-size: 0.85rem;
+  box-shadow: 0 8px 24px rgba(15,23,42,.25);
+  animation: toast-in 0.18s ease;
+}
+.toast-host .toast-item.ok {
+  background: #0f766e;
+}
+.toast-host .toast-item.err { background: #b91c1c; }
+@keyframes toast-in {
+  from { opacity: 0; transform: translateY(6px); }
+  to { opacity: 1; transform: none; }
+}
+
+.task-panel[hidden] { display: none !important; }
+.task-panel { position: fixed; inset: 0; z-index: 70; }
+.task-panel-backdrop {
+  position: absolute; inset: 0;
+  background: rgba(15, 23, 42, 0.45);
+}
+.task-panel-sheet {
+  position: absolute; top: 0; left: 0; bottom: 0;
+  width: min(720px, 100vw);
+  background: #0f172a; color: #e2e8f0;
+  display: flex; flex-direction: column;
+  box-shadow: 16px 0 40px rgba(0,0,0,.35);
+}
+.task-panel-head {
+  padding: 0.9rem 1rem;
+  border-bottom: 1px solid rgba(255,255,255,.08);
+}
+.task-panel-body {
+  flex: 1; min-height: 0;
+  display: grid;
+  grid-template-columns: 240px 1fr;
+}
+.task-side {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  border-right: 1px solid rgba(255,255,255,.08);
+}
+.task-list {
+  overflow: auto;
+  padding: 0.4rem;
+  flex: 1;
+  min-height: 0;
+}
+.task-item {
+  width: 100%;
+  appearance: none; border: 0; background: transparent; color: inherit;
+  text-align: left; padding: 0.55rem 0.65rem; border-radius: 8px;
+  cursor: pointer; display: grid; gap: 2px; font: inherit; font-size: 0.85rem;
+}
+.task-item:hover { background: rgba(255,255,255,.05); }
+.task-item.is-active { background: rgba(13,148,136,.35); }
+.task-item.is-running {
+  border: 1px solid rgba(251, 191, 36, 0.55);
+  background: rgba(251, 191, 36, 0.12);
+}
+.task-item.is-running.is-active {
+  background: rgba(251, 191, 36, 0.22);
+  border-color: #fbbf24;
+}
+.task-item .task-running-dot {
+  width: 7px; height: 7px; border-radius: 50%;
+  background: #fbbf24; display: inline-block;
+  margin-right: 0.35rem;
+  box-shadow: 0 0 0 3px rgba(251,191,36,.2);
+  animation: task-pulse 1.2s ease infinite;
+}
+@keyframes task-pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.45; }
+}
+.task-side-foot .btn.d-none { display: none !important; }
+.task-log-wrap { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
+.task-log-head { padding: 0.55rem 0.85rem; border-bottom: 1px solid rgba(255,255,255,.08); }
+.task-log {
+  margin: 0; flex: 1; overflow: auto; padding: 0.75rem;
+  font: 0.78rem/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+  white-space: pre-wrap; word-break: break-word; background: #020617; color: #cbd5e1;
+}
+
+.code-editor {
+  position: fixed; inset: 0; z-index: 80;
+  display: flex; align-items: center; justify-content: center;
+  padding: 1.25rem;
+}
+.code-editor[hidden] { display: none !important; }
+.code-editor-backdrop {
+  position: absolute; inset: 0;
+  background: rgba(15, 23, 42, 0.45);
+  backdrop-filter: blur(2px);
+}
+.code-editor-window {
+  position: relative;
+  z-index: 1;
+  width: min(1100px, 92vw);
+  height: min(720px, 82vh);
+  display: flex; flex-direction: column;
+  background: #1e1e1e; color: #d4d4d4;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 24px 80px rgba(0,0,0,.45);
+  border: 1px solid #333;
+}
+.code-editor-bar {
+  display: flex; align-items: center; justify-content: space-between; gap: 12px;
+  padding: 0.45rem 0.65rem 0.45rem 0.85rem; background: #252526; border-bottom: 1px solid #333; flex-shrink: 0;
+}
+.code-editor-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
+.code-editor-meta span {
+  font-size: 0.78rem; color: #9ca3af;
+  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.code-editor-x {
+  appearance: none; border: 0; background: transparent;
+  color: #9ca3af; font-size: 1.45rem; line-height: 1;
+  width: 2rem; height: 2rem; border-radius: 6px; cursor: pointer;
+}
+.code-editor-x:hover { background: rgba(255,255,255,.08); color: #fff; }
+.code-editor-main {
+  flex: 1; min-height: 0;
+  display: grid;
+  grid-template-columns: 220px 1fr;
+}
+.code-editor-tree {
+  display: flex; flex-direction: column; min-height: 0;
+  background: #181818; border-right: 1px solid #333;
+}
+.code-editor-tree-head {
+  display: flex; align-items: center; gap: 6px;
+  padding: 0.4rem 0.5rem; border-bottom: 1px solid #333; flex-shrink: 0;
+}
+.code-editor-tree-path {
+  flex: 1; min-width: 0; font-size: 0.75rem; color: #9ca3af;
+  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+.code-editor-tree-body {
+  flex: 1; overflow: auto; padding: 0.25rem;
+  font-size: 0.82rem;
+}
+.editor-tree-item {
+  width: 100%;
+  appearance: none; border: 0; background: transparent; color: #d4d4d4;
+  text-align: left; padding: 0.28rem 0.45rem; border-radius: 4px;
+  cursor: pointer; display: flex; align-items: center; gap: 0.35rem;
+  font: inherit; font-size: 0.82rem;
+}
+.editor-tree-item:hover { background: rgba(255,255,255,.06); }
+.editor-tree-item.is-open { background: rgba(13,148,136,.28); }
+.code-editor-workspace {
+  display: flex; flex-direction: column; min-width: 0; min-height: 0;
+}
+.code-editor-tabs {
+  display: flex; gap: 2px; overflow-x: auto; flex-shrink: 0;
+  background: #252526; border-bottom: 1px solid #333; padding: 0.2rem 0.25rem 0;
+  min-height: 34px;
+}
+.editor-tab {
+  appearance: none; border: 0; background: #2d2d2d; color: #ccc;
+  padding: 0.35rem 0.55rem; border-radius: 6px 6px 0 0;
+  display: inline-flex; align-items: center; gap: 0.4rem;
+  cursor: pointer; font: inherit; font-size: 0.8rem; white-space: nowrap;
+  border: 1px solid transparent; border-bottom: 0;
+}
+.editor-tab.is-active {
+  background: #1e1e1e; color: #fff; border-color: #333;
+}
+.editor-tab.is-dirty .editor-tab-name::after {
+  content: " •";
+  color: #fbbf24;
+}
+.editor-tab-close {
+  appearance: none; border: 0; background: transparent; color: #999;
+  padding: 0 2px; line-height: 1; cursor: pointer; border-radius: 3px;
+}
+.editor-tab-close:hover { background: rgba(255,255,255,.12); color: #fff; }
+.monaco-host { flex: 1; min-height: 0; }
+
+.upload-dropzone {
+  border: 1.5px dashed #cbd5e1;
+  border-radius: 10px;
+  background: #f8fafc;
+  padding: 1.25rem;
+  text-align: center;
+  transition: border-color 0.15s ease, background 0.15s ease;
+  outline: none;
+}
+.upload-dropzone.is-dragover {
+  border-color: var(--accent);
+  background: rgba(13, 148, 136, 0.08);
+}
+.upload-dropzone-inner { pointer-events: none; }
+.upload-dropzone-inner .btn-group { pointer-events: auto; }
+.upload-drop-icon {
+  font-size: 1.75rem;
+  color: var(--accent);
+  display: block;
+  margin-bottom: 0.35rem;
+}
+.upload-queue {
+  max-height: 260px;
+  overflow: auto;
+  border: 1px solid #e2e8f0;
+  border-radius: 8px;
+  background: #fff;
+}
+.upload-queue:empty {
+  display: none;
+}
+.upload-queue-item {
+  display: grid;
+  grid-template-columns: 1fr auto auto;
+  gap: 0.5rem;
+  align-items: center;
+  padding: 0.45rem 0.65rem;
+  border-bottom: 1px solid #f1f5f9;
+  font-size: 0.82rem;
+}
+.upload-queue-item:last-child { border-bottom: 0; }
+.upload-queue-item .path {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+.upload-queue-item .size { color: #64748b; }
+.upload-queue-item .st { min-width: 3.5rem; text-align: right; color: #64748b; }
+.upload-queue-item.is-ok .st { color: #0f766e; }
+.upload-queue-item.is-err .st { color: #b91c1c; }
+.upload-queue-item.is-run .st { color: #0369a1; }
+.progress-bar.bg-teal { background-color: var(--accent) !important; }
+
+.site-settings-body { min-height: 420px; }
+.site-settings-layout {
+  display: grid;
+  grid-template-columns: 168px 1fr;
+  min-height: 420px;
+}
+.site-settings-nav {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  padding: 0.75rem 0.5rem;
+  background: #f8fafc;
+  border-right: 1px solid #e2e8f0;
+}
+.site-settings-tab {
+  appearance: none; border: 0; background: transparent;
+  text-align: left; padding: 0.55rem 0.75rem; border-radius: 8px;
+  font: inherit; font-size: 0.88rem; color: #334155; cursor: pointer;
+}
+.site-settings-tab:hover { background: #e2e8f0; }
+.site-settings-tab.is-active {
+  background: rgba(13, 148, 136, 0.14);
+  color: #0f766e;
+  font-weight: 600;
+}
+.site-settings-panels { padding: 1rem 1.15rem; min-width: 0; }
+.site-settings-panel { display: none; }
+.site-settings-panel.is-active { display: block; }
+.site-conf-editor {
+  font-size: 0.8rem;
+  line-height: 1.45;
+  resize: vertical;
+  min-height: 280px;
+}
+.site-log-view {
+  margin: 0;
+  max-height: 360px;
+  overflow: auto;
+  padding: 0.75rem;
+  background: #0f172a;
+  color: #cbd5e1;
+  border-radius: 8px;
+  font: 0.75rem/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.site-log-view.table-mode {
+  padding: 0;
+  white-space: normal;
+  font: inherit;
+}
+.site-log-view.table-mode table {
+  width: 100%;
+  margin: 0;
+  color: #e2e8f0;
+  font-size: 0.75rem;
+  border-collapse: collapse;
+}
+.site-log-view.table-mode th,
+.site-log-view.table-mode td {
+  padding: 0.4rem 0.55rem;
+  border-bottom: 1px solid #1e293b;
+  vertical-align: top;
+  white-space: nowrap;
+}
+.site-log-view.table-mode th {
+  position: sticky;
+  top: 0;
+  background: #1e293b;
+  color: #94a3b8;
+  font-weight: 600;
+  z-index: 1;
+}
+.site-log-view.table-mode td.uri {
+  white-space: normal;
+  word-break: break-all;
+  max-width: 220px;
+}
+.site-log-view.table-mode .status-ok { color: #4ade80; }
+.site-log-view.table-mode .status-redir { color: #38bdf8; }
+.site-log-view.table-mode .status-client { color: #fbbf24; }
+.site-log-view.table-mode .status-server { color: #f87171; }
+.site-log-empty {
+  padding: 1rem;
+  color: #94a3b8;
+}
+.sites-link {
+  color: var(--accent);
+  text-decoration: none;
+  cursor: pointer;
+}
+.sites-link:hover { text-decoration: underline; }
+.site-ssl-domain-list {
+  max-height: 180px;
+  overflow: auto;
+}
+.site-ssl-domain-list .form-check {
+  margin-bottom: 0.35rem;
+}
+.site-ssl-domain-list .form-check:last-child {
+  margin-bottom: 0;
+}
+.site-ssl-domain-list .host {
+  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+  font-size: 0.82rem;
+}
+.site-ssl-domain-list .port-hint {
+  color: #64748b;
+  font-size: 0.75rem;
+  margin-left: 0.35rem;
+}
+
+#task-badge .badge { font-size: 0.65rem; }
+#task-badge.has-active { border-color: #fbbf24; }
+
+@media (max-width: 900px) {
+  html, body {
+    height: auto;
+    overflow: auto;
+  }
+  .app {
+    flex-direction: column;
+    height: auto;
+    min-height: 100vh;
+  }
+  .rail {
+    width: 100%;
+    height: auto;
+    position: relative;
+  }
+  .nav { flex-direction: row; flex-wrap: wrap; }
+  .main {
+    height: auto;
+    overflow: visible;
+  }
+  .view.is-active {
+    overflow: visible;
+    height: auto;
+  }
+  .view.is-active > .card {
+    flex: none;
+    overflow: visible;
+  }
+  .view.is-active > .card > .table-responsive,
+  .view.is-active > .card > .card-body {
+    overflow: visible;
+  }
+  .task-panel-body { grid-template-columns: 1fr; }
+  .task-side { border-right: 0; border-bottom: 1px solid rgba(255,255,255,.08); max-height: 220px; }
+}

+ 2586 - 0
data/www/assets/app.js

@@ -0,0 +1,2586 @@
+(() => {
+  const $ = (sel, root = document) => root.querySelector(sel);
+  const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
+
+  const METRICS_INTERVAL_MS = 2000;
+  const MAX_EDIT_BYTES = 10 * 1024 * 1024;
+
+  const state = {
+    status: null,
+    sites: [],
+    databases: [],
+    busy: false,
+    filesPath: "/",
+    files: [],
+    metricsTimer: null,
+    metricsRange: "10m",
+    metricCharts: null,
+    editor: null,
+    editorTabs: [], // { path, model, readonly, dirty, savedValue }
+    editorActive: "",
+    editorTreePath: "/",
+    editorTreeEntries: [],
+    editorBound: false,
+    monacoReady: null,
+    tasks: [],
+    taskActive: 0,
+    selectedTaskId: "",
+    taskLogOffset: 0,
+    taskPanelOpen: false,
+    taskTab: "active", // active | history
+    taskStatusMap: {},
+    bsModal: null,
+    uploadModal: null,
+    uploadQueue: [], // { id, relativePath, file, status, error }
+    uploadBusy: false,
+    siteSettingsModal: null,
+    siteSettings: null, // current site object
+    siteLogKind: "access",
+    siteSslUploadTarget: "", // cert | key
+    analyticsModal: null,
+    analyticsSite: "",
+    analyticsCharts: [],
+  };
+
+  const titles = {
+    console: { title: "控制台", eyebrow: "Console" },
+    software: { title: "软件管理", eyebrow: "Software" },
+    sites: { title: "网站管理", eyebrow: "Websites" },
+    mysql: { title: "数据库", eyebrow: "MySQL" },
+    files: { title: "文件管理", eyebrow: "Files" },
+  };
+
+  function toast(msg, type = "ok") {
+    const el = document.createElement("div");
+    el.className = `toast-item ${type === "err" ? "err" : "ok"}`;
+    el.textContent = msg;
+    $("#toast-host").appendChild(el);
+    setTimeout(() => {
+      el.style.opacity = "0";
+      el.style.transition = "opacity 0.25s ease";
+      setTimeout(() => el.remove(), 260);
+    }, type === "ok" ? 1600 : 3200);
+  }
+
+  async function api(path, options = {}) {
+    const opts = {
+      method: options.method || "GET",
+      credentials: "same-origin",
+      headers: { Accept: "application/json" },
+      ...options,
+    };
+    if (opts.body && typeof opts.body === "object") {
+      opts.headers = { ...(opts.headers || {}), "Content-Type": "application/json", Accept: "application/json" };
+      opts.body = JSON.stringify(opts.body);
+    }
+    const res = await fetch(path, opts);
+    let json = null;
+    try {
+      json = await res.json();
+    } catch (_) {
+      throw new Error(`无效响应 (${res.status})`);
+    }
+    if (json && Number(json.code) === 401) {
+      location.replace("/login.html");
+      throw new Error(json.msg || "未登录");
+    }
+    if (!json || json.code !== 200) {
+      throw new Error((json && json.msg) || `请求失败 (${res.status})`);
+    }
+    return json.data;
+  }
+
+  async function withBusy(fn, btn) {
+    if (state.busy) return;
+    state.busy = true;
+    if (btn) btn.disabled = true;
+    try {
+      await fn();
+    } catch (err) {
+      toast(err.message || String(err), "err");
+    } finally {
+      state.busy = false;
+      if (btn) btn.disabled = false;
+    }
+  }
+
+  function setView(name) {
+    if (name === "overview" || name === "monitor") name = "console";
+    $$(".nav-item").forEach((b) => b.classList.toggle("is-active", b.dataset.view === name));
+    $$(".view").forEach((v) => v.classList.toggle("is-active", v.dataset.viewPanel === name));
+    const t = titles[name] || titles.console;
+    $("#page-title").textContent = t.title;
+    $("#page-eyebrow").textContent = t.eyebrow;
+    history.replaceState(null, "", `#${name}`);
+    if (name === "console") startMetrics();
+    else stopMetrics();
+    if (name === "files") loadFiles(state.filesPath);
+  }
+
+  function escapeHtml(s) {
+    return String(s ?? "")
+      .replace(/&/g, "&amp;")
+      .replace(/</g, "&lt;")
+      .replace(/>/g, "&gt;")
+      .replace(/"/g, "&quot;");
+  }
+
+  function formatBytes(n) {
+    const v = Number(n) || 0;
+    if (v < 1024) return `${v} B`;
+    const units = ["KB", "MB", "GB", "TB"];
+    let x = v / 1024;
+    let i = 0;
+    while (x >= 1024 && i < units.length - 1) {
+      x /= 1024;
+      i += 1;
+    }
+    return `${x.toFixed(x >= 10 ? 0 : 1)} ${units[i]}`;
+  }
+
+  function formatTime(sec) {
+    if (!sec) return "—";
+    const d = new Date(Number(sec) * 1000);
+    if (Number.isNaN(d.getTime())) return "—";
+    return d.toLocaleString();
+  }
+
+  function joinPath(base, name) {
+    if (base === "/") return `/${name}`;
+    return `${base.replace(/\/+$/, "")}/${name}`;
+  }
+
+  function parentPath(p) {
+    if (!p || p === "/") return "/";
+    const parts = p.replace(/\/+$/, "").split("/");
+    parts.pop();
+    return parts.length <= 1 ? "/" : parts.join("/");
+  }
+
+  function fileIcon(name, isDir) {
+    if (isDir) return `<i class="bi bi-folder-fill file-icon dir"></i>`;
+    const ext = (String(name).includes(".") ? String(name).split(".").pop() : "").toLowerCase();
+    const map = {
+      js: ["bi-filetype-js", "code"],
+      mjs: ["bi-filetype-js", "code"],
+      ts: ["bi-filetype-tsx", "code"],
+      tsx: ["bi-filetype-tsx", "code"],
+      jsx: ["bi-filetype-jsx", "code"],
+      py: ["bi-filetype-py", "code"],
+      go: ["bi-filetype-jsx", "code"],
+      rs: ["bi-filetype-jsx", "code"],
+      java: ["bi-filetype-java", "code"],
+      c: ["bi-filetype-c", "code"],
+      h: ["bi-filetype-h", "code"],
+      cpp: ["bi-filetype-cpp", "code"],
+      hpp: ["bi-filetype-hpp", "code"],
+      php: ["bi-filetype-php", "code"],
+      rb: ["bi-filetype-rb", "code"],
+      sh: ["bi-terminal", "code"],
+      bash: ["bi-terminal", "code"],
+      html: ["bi-filetype-html", "web"],
+      htm: ["bi-filetype-html", "web"],
+      css: ["bi-filetype-css", "web"],
+      scss: ["bi-filetype-scss", "web"],
+      json: ["bi-filetype-json", "code"],
+      xml: ["bi-filetype-xml", "code"],
+      yaml: ["bi-filetype-yml", "cfg"],
+      yml: ["bi-filetype-yml", "cfg"],
+      md: ["bi-markdown", "doc"],
+      txt: ["bi-filetype-txt", "doc"],
+      log: ["bi-journal-text", "doc"],
+      conf: ["bi-gear", "cfg"],
+      cfg: ["bi-gear", "cfg"],
+      ini: ["bi-gear", "cfg"],
+      sql: ["bi-filetype-sql", "code"],
+      png: ["bi-filetype-png", "img"],
+      jpg: ["bi-filetype-jpg", "img"],
+      jpeg: ["bi-filetype-jpg", "img"],
+      gif: ["bi-filetype-gif", "img"],
+      svg: ["bi-filetype-svg", "img"],
+      webp: ["bi-file-earmark-image", "img"],
+      zip: ["bi-file-earmark-zip", "doc"],
+      gz: ["bi-file-earmark-zip", "doc"],
+      tar: ["bi-file-earmark-zip", "doc"],
+      pdf: ["bi-filetype-pdf", "doc"],
+    };
+    const [icon, cls] = map[ext] || ["bi-file-earmark", "doc"];
+    return `<i class="bi ${icon} file-icon ${cls}"></i>`;
+  }
+
+  function langFromPath(path) {
+    const name = String(path || "").split("/").pop() || "";
+    const ext = (name.includes(".") ? name.split(".").pop() : "").toLowerCase();
+    const map = {
+      js: "javascript", mjs: "javascript", cjs: "javascript",
+      ts: "typescript", tsx: "typescript", jsx: "javascript",
+      json: "json", html: "html", htm: "html", css: "css", scss: "scss",
+      md: "markdown", py: "python", go: "go", rs: "rust", java: "java",
+      c: "c", h: "c", cpp: "cpp", cc: "cpp", hpp: "cpp", php: "php",
+      sh: "shell", bash: "shell", sql: "sql", xml: "xml",
+      yaml: "yaml", yml: "yaml", toml: "ini", ini: "ini", conf: "ini",
+      cfg: "ini", txt: "plaintext", log: "plaintext",
+    };
+    return map[ext] || "plaintext";
+  }
+
+  async function ensureMonaco() {
+    if (window.monaco) return Promise.resolve(window.monaco);
+    if (state.monacoReady) return state.monacoReady;
+    state.monacoReady = new Promise((resolve, reject) => {
+      if (typeof require === "undefined") {
+        reject(new Error("Monaco loader 未加载"));
+        return;
+      }
+      window.require.config({ paths: { vs: "/assets/monaco/vs" } });
+      window.require(["vs/editor/editor.main"], () => {
+        if (window.monaco) resolve(window.monaco);
+        else reject(new Error("Monaco 初始化失败"));
+      }, reject);
+    });
+    return state.monacoReady;
+  }
+
+  function fileBaseName(path) {
+    const parts = String(path || "").split("/").filter(Boolean);
+    return parts.length ? parts[parts.length - 1] : path || "/";
+  }
+
+  function findTab(path) {
+    return state.editorTabs.find((t) => t.path === path);
+  }
+
+  function activeTab() {
+    return findTab(state.editorActive);
+  }
+
+  function syncEditorChrome() {
+    const tab = activeTab();
+    $("#code-editor-path").textContent = tab ? tab.path : "";
+    $("#code-editor-mode").textContent = tab
+      ? tab.readonly
+        ? "预览"
+        : tab.dirty
+          ? "编辑 *"
+          : "编辑"
+      : "编辑器";
+  }
+
+  function renderEditorTabs() {
+    const host = $("#editor-tabs");
+    if (!state.editorTabs.length) {
+      host.innerHTML = `<div class="small text-secondary px-2 py-1">未打开文件</div>`;
+      return;
+    }
+    host.innerHTML = state.editorTabs
+      .map((t) => {
+        const active = t.path === state.editorActive ? " is-active" : "";
+        const dirty = t.dirty ? " is-dirty" : "";
+        return `<div class="editor-tab${active}${dirty}" data-tab-path="${escapeHtml(t.path)}" title="${escapeHtml(t.path)}">
+          <span class="editor-tab-name">${escapeHtml(fileBaseName(t.path))}</span>
+          <button type="button" class="editor-tab-close" data-close-tab="${escapeHtml(t.path)}" title="关闭">×</button>
+        </div>`;
+      })
+      .join("");
+  }
+
+  function renderEditorTree() {
+    $("#editor-tree-path").textContent = state.editorTreePath || "/";
+    const host = $("#editor-tree");
+    const entries = state.editorTreeEntries || [];
+    if (!entries.length) {
+      host.innerHTML = `<div class="text-secondary px-2 py-2 small">空目录</div>`;
+      return;
+    }
+    host.innerHTML = entries
+      .map((e) => {
+        const full = joinPath(state.editorTreePath, e.name);
+        const open = full === state.editorActive ? " is-open" : "";
+        return `<button type="button" class="editor-tree-item${open}" data-tree-path="${escapeHtml(full)}" data-tree-dir="${e.is_dir ? "1" : "0"}" data-tree-size="${Number(e.size) || 0}">
+          ${fileIcon(e.name, !!e.is_dir)}<span>${escapeHtml(e.name)}</span>
+        </button>`;
+      })
+      .join("");
+  }
+
+  async function loadEditorTree(path) {
+    const data = await api(`/api/files/list?path=${encodeURIComponent(path || "/")}`);
+    state.editorTreePath = (data && data.path) || path || "/";
+    state.editorTreeEntries = (data && data.entries) || [];
+    renderEditorTree();
+  }
+
+  async function ensureEditorInstance() {
+    const monaco = await ensureMonaco();
+    if (state.editor) return monaco;
+    const host = $("#monaco-host");
+    host.innerHTML = "";
+    state.editor = monaco.editor.create(host, {
+      theme: "vs-dark",
+      automaticLayout: true,
+      minimap: { enabled: true },
+      fontSize: 14,
+      wordWrap: "on",
+      scrollBeyondLastLine: false,
+      tabSize: 2,
+    });
+    state.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
+      withBusy(async () => {
+        await saveActiveTab();
+      });
+    });
+    return monaco;
+  }
+
+  async function switchEditorTab(path) {
+    const tab = findTab(path);
+    if (!tab || !state.editor) return;
+    const prev = activeTab();
+    if (prev && prev.model && state.editor) {
+      prev.viewState = state.editor.saveViewState();
+    }
+    state.editorActive = path;
+    state.editor.setModel(tab.model);
+    state.editor.updateOptions({ readOnly: !!tab.readonly });
+    if (tab.viewState) state.editor.restoreViewState(tab.viewState);
+    state.editor.focus();
+    syncEditorChrome();
+    renderEditorTabs();
+    renderEditorTree();
+  }
+
+  async function closeEditorTab(path) {
+    const idx = state.editorTabs.findIndex((t) => t.path === path);
+    if (idx < 0) return;
+    const tab = state.editorTabs[idx];
+    if (tab.dirty && !confirm(`「${fileBaseName(path)}」未保存,确认关闭?`)) return;
+    if (tab.model) tab.model.dispose();
+    state.editorTabs.splice(idx, 1);
+    if (state.editorActive === path) {
+      const next = state.editorTabs[idx] || state.editorTabs[idx - 1] || null;
+      if (next) await switchEditorTab(next.path);
+      else {
+        state.editorActive = "";
+        if (state.editor) state.editor.setModel(null);
+        syncEditorChrome();
+        renderEditorTabs();
+        renderEditorTree();
+      }
+    } else {
+      renderEditorTabs();
+      renderEditorTree();
+    }
+  }
+
+  async function openCodeEditor({ path, content, readonly }) {
+    $("#code-editor").hidden = false;
+    const monaco = await ensureEditorInstance();
+    let tab = findTab(path);
+    if (!tab) {
+      const model = monaco.editor.createModel(content || "", langFromPath(path));
+      tab = {
+        path,
+        model,
+        readonly: !!readonly,
+        dirty: false,
+        savedValue: content || "",
+        viewState: null,
+      };
+      model.onDidChangeContent(() => {
+        tab.dirty = model.getValue() !== tab.savedValue;
+        syncEditorChrome();
+        renderEditorTabs();
+      });
+      state.editorTabs.push(tab);
+    } else if (!readonly && content != null && !tab.dirty) {
+      tab.model.setValue(content);
+      tab.savedValue = content;
+      tab.dirty = false;
+    }
+    if (!state.editorTreePath || state.editorTreePath === "/") {
+      await loadEditorTree(parentPath(path) || "/").catch(() => loadEditorTree("/"));
+    } else {
+      renderEditorTree();
+    }
+    await switchEditorTab(path);
+  }
+
+  function closeCodeEditor() {
+    const dirty = state.editorTabs.some((t) => t.dirty);
+    if (dirty && !confirm("有未保存的文件,确认关闭编辑器?")) return;
+    state.editorTabs.forEach((t) => {
+      if (t.model) t.model.dispose();
+    });
+    state.editorTabs = [];
+    state.editorActive = "";
+    if (state.editor) {
+      state.editor.dispose();
+      state.editor = null;
+    }
+    $("#monaco-host").innerHTML = "";
+    $("#editor-tabs").innerHTML = "";
+    $("#editor-tree").innerHTML = "";
+    $("#code-editor").hidden = true;
+  }
+
+  async function saveActiveTab() {
+    const tab = activeTab();
+    if (!tab || tab.readonly || !tab.model) return;
+    const content = tab.model.getValue();
+    if (new TextEncoder().encode(content).length > MAX_EDIT_BYTES) {
+      throw new Error("内容超过 10MB,无法保存");
+    }
+    await api("/api/files/write", {
+      method: "POST",
+      body: { path: tab.path, content },
+    });
+    tab.savedValue = content;
+    tab.dirty = false;
+    syncEditorChrome();
+    renderEditorTabs();
+    toast("保存成功");
+    if ((location.hash || "").slice(1) === "files") {
+      await loadFiles(state.filesPath).catch(() => {});
+    }
+  }
+
+  async function openFileInEditor(path, size, readonly) {
+    if (Number(size) > MAX_EDIT_BYTES) {
+      throw new Error("文件超过 10MB,不支持在线编辑/预览");
+    }
+    const existing = findTab(path);
+    if (existing) {
+      $("#code-editor").hidden = false;
+      await ensureEditorInstance();
+      await switchEditorTab(path);
+      await loadEditorTree(parentPath(path) || state.editorTreePath || "/").catch(() => {});
+      return;
+    }
+    const data = await api(`/api/files/read?path=${encodeURIComponent(path)}`);
+    await openCodeEditor({
+      path,
+      content: (data && data.content) || "",
+      readonly: !!readonly,
+    });
+    await loadEditorTree(parentPath(path) || "/").catch(() => loadEditorTree("/"));
+  }
+
+  async function openConfigFile(path) {
+    if (!path) throw new Error("配置文件路径未知");
+    await openFileInEditor(path, 0, false);
+  }
+
+  function formatRate(bps) {
+    const v = Math.max(0, Number(bps) || 0);
+    if (v < 1024) return `${Math.round(v)} B/s`;
+    let x = v / 1024;
+    const units = ["KB/s", "MB/s", "GB/s", "TB/s"];
+    let i = 0;
+    while (x >= 1024 && i < units.length - 1) {
+      x /= 1024;
+      i += 1;
+    }
+    const digits = x >= 100 ? 0 : x >= 10 ? 1 : 2;
+    return `${x.toFixed(digits)} ${units[i]}`;
+  }
+
+  function formatMetricLabel(timeMs, range) {
+    const d = new Date(Number(timeMs) || 0);
+    const hh = String(d.getHours()).padStart(2, "0");
+    const mm = String(d.getMinutes()).padStart(2, "0");
+    const ss = String(d.getSeconds()).padStart(2, "0");
+    if (range === "1m" || range === "10m" || range === "30m") {
+      return `${hh}:${mm}:${ss}`;
+    }
+    if (range === "1h" || range === "6h") {
+      return `${hh}:${mm}`;
+    }
+    const mo = String(d.getMonth() + 1).padStart(2, "0");
+    const day = String(d.getDate()).padStart(2, "0");
+    return `${mo}-${day} ${hh}:${mm}`;
+  }
+
+  function chartDefaults() {
+    return {
+      responsive: true,
+      maintainAspectRatio: false,
+      animation: { duration: 280 },
+      interaction: { mode: "index", intersect: false },
+      plugins: {
+        legend: { display: false },
+        tooltip: {
+          backgroundColor: "rgba(15,23,42,0.92)",
+          titleFont: { size: 11 },
+          bodyFont: { size: 12 },
+          padding: 10,
+          cornerRadius: 8,
+        },
+      },
+      scales: {
+        x: {
+          grid: { display: false },
+          ticks: {
+            maxRotation: 0,
+            autoSkip: true,
+            maxTicksLimit: 6,
+            color: "#94a3b8",
+            font: { size: 10 },
+          },
+          border: { display: false },
+        },
+        y: {
+          beginAtZero: true,
+          grid: { color: "rgba(148,163,184,0.18)" },
+          ticks: { color: "#94a3b8", font: { size: 10 } },
+          border: { display: false },
+        },
+      },
+      elements: {
+        line: { tension: 0.35, borderWidth: 2 },
+        point: { radius: 0, hoverRadius: 3 },
+      },
+    };
+  }
+
+  function makePercentChart(canvas, label, color) {
+    const fill = color.replace(")", ", 0.14)").replace("rgb", "rgba");
+    return new Chart(canvas, {
+      type: "line",
+      data: {
+        labels: [],
+        datasets: [{
+          label,
+          data: [],
+          borderColor: color,
+          backgroundColor: fill,
+          fill: true,
+        }],
+      },
+      options: {
+        ...chartDefaults(),
+        scales: {
+          ...chartDefaults().scales,
+          y: {
+            ...chartDefaults().scales.y,
+            suggestedMax: 100,
+            ticks: {
+              ...chartDefaults().scales.y.ticks,
+              callback: (v) => `${Number(v).toFixed(0)}%`,
+            },
+          },
+        },
+        plugins: {
+          ...chartDefaults().plugins,
+          tooltip: {
+            ...chartDefaults().plugins.tooltip,
+            callbacks: {
+              label: (ctx) => `${ctx.dataset.label}: ${Number(ctx.parsed.y).toFixed(1)}%`,
+            },
+          },
+        },
+      },
+    });
+  }
+
+  function makeNetChart(canvas) {
+    return new Chart(canvas, {
+      type: "line",
+      data: {
+        labels: [],
+        datasets: [
+          {
+            label: "下行",
+            data: [],
+            borderColor: "#0d9488",
+            backgroundColor: "rgba(13, 148, 136, 0.14)",
+            fill: true,
+          },
+          {
+            label: "上行",
+            data: [],
+            borderColor: "#ea580c",
+            backgroundColor: "rgba(234, 88, 12, 0.12)",
+            fill: true,
+          },
+        ],
+      },
+      options: {
+        ...chartDefaults(),
+        plugins: {
+          ...chartDefaults().plugins,
+          legend: {
+            display: true,
+            position: "top",
+            align: "end",
+            labels: { boxWidth: 10, boxHeight: 10, font: { size: 11 }, color: "#64748b" },
+          },
+          tooltip: {
+            ...chartDefaults().plugins.tooltip,
+            callbacks: {
+              label: (ctx) => `${ctx.dataset.label}: ${formatRate(ctx.parsed.y)}`,
+            },
+          },
+        },
+        scales: {
+          ...chartDefaults().scales,
+          y: {
+            ...chartDefaults().scales.y,
+            ticks: {
+              ...chartDefaults().scales.y.ticks,
+              callback: (v) => formatRate(v),
+            },
+          },
+        },
+      },
+    });
+  }
+
+  function ensureMetricCharts() {
+    if (state.metricCharts || typeof Chart === "undefined") return state.metricCharts;
+    state.metricCharts = {
+      cpu: makePercentChart($("#chart-cpu"), "CPU", "rgb(15, 118, 110)"),
+      mem: makePercentChart($("#chart-mem"), "内存", "rgb(3, 105, 161)"),
+      disk: makePercentChart($("#chart-disk"), "磁盘", "rgb(180, 83, 9)"),
+      net: makeNetChart($("#chart-net")),
+    };
+    return state.metricCharts;
+  }
+
+  function applyHistoryToCharts(items, range) {
+    const charts = ensureMetricCharts();
+    if (!charts) return;
+    const labels = items.map((p) => formatMetricLabel(p.time_ms, range));
+    const cpu = items.map((p) => Number(p.cpu_percent) || 0);
+    const mem = items.map((p) => Number(p.mem_percent) || 0);
+    const disk = items.map((p) => Number(p.disk_percent) || 0);
+    const rx = items.map((p) => Number(p.net_rx_bps) || 0);
+    const tx = items.map((p) => Number(p.net_tx_bps) || 0);
+    const applyOne = (chart, data) => {
+      chart.data.labels = labels;
+      chart.data.datasets[0].data = data;
+      chart.update("none");
+    };
+    applyOne(charts.cpu, cpu);
+    applyOne(charts.mem, mem);
+    applyOne(charts.disk, disk);
+    charts.net.data.labels = labels;
+    charts.net.data.datasets[0].data = rx;
+    charts.net.data.datasets[1].data = tx;
+    charts.net.update("none");
+
+    const last = items.length ? items[items.length - 1] : null;
+    if (!last) {
+      $("#cpu-badge").textContent = "—";
+      $("#mem-badge").textContent = "—";
+      $("#disk-badge").textContent = "—";
+      $("#net-badge").textContent = "—";
+      return;
+    }
+    $("#cpu-badge").textContent =
+      `${(Number(last.cpu_percent) || 0).toFixed(1)}% · ${last.cpu_cores ?? "—"} 核`;
+    $("#mem-badge").textContent =
+      `${formatBytes(last.mem_used)} / ${formatBytes(last.mem_total)} · ${(Number(last.mem_percent) || 0).toFixed(1)}%`;
+    $("#disk-badge").textContent =
+      `${formatBytes(last.disk_used)} / ${formatBytes(last.disk_total)} · ${(Number(last.disk_percent) || 0).toFixed(1)}%`;
+    $("#net-badge").innerHTML =
+      `<span style="color:#0d9488">↓ ${formatRate(last.net_rx_bps)}</span>` +
+      ` · ` +
+      `<span style="color:#ea580c">↑ ${formatRate(last.net_tx_bps)}</span>`;
+  }
+
+  async function pollMetrics() {
+    try {
+      const range = state.metricsRange || "10m";
+      const data = await api(`/api/system/metrics/history?range=${encodeURIComponent(range)}`);
+      applyHistoryToCharts((data && data.items) || [], range);
+    } catch (_) {}
+  }
+
+  function startMetrics() {
+    if (state.metricsTimer) return;
+    const sel = $("#metrics-range");
+    if (sel) {
+      state.metricsRange = sel.value || "10m";
+      sel.onchange = () => {
+        state.metricsRange = sel.value || "10m";
+        pollMetrics();
+      };
+    }
+    ensureMetricCharts();
+    pollMetrics();
+    state.metricsTimer = setInterval(pollMetrics, METRICS_INTERVAL_MS);
+  }
+
+  function stopMetrics() {
+    if (state.metricsTimer) {
+      clearInterval(state.metricsTimer);
+      state.metricsTimer = null;
+    }
+  }
+
+  function statusPill(installed, running) {
+    if (!installed) return `<span class="badge text-bg-secondary">未安装</span>`;
+    if (running === true) return `<span class="badge text-bg-success">运行中</span>`;
+    if (running === false) return `<span class="badge text-bg-warning">已停止</span>`;
+    return `<span class="badge text-bg-primary">已安装</span>`;
+  }
+
+  function softCard(name, info, withActions) {
+    const key = name.toLowerCase();
+    const installed = !!info.installed;
+    const running = info.running;
+    const bits = [`版本 ${escapeHtml(info.version || "—")}`];
+    if (info.port) bits.push(`端口 ${escapeHtml(info.port)}`);
+    let actions = "";
+    if (withActions) {
+      const btns = [];
+      if (!installed) {
+        btns.push(`<button type="button" class="btn btn-sm btn-primary" data-act="${key}-install">安装</button>`);
+      } else {
+        if (key === "nginx" || key === "mysql" || key === "redis") {
+          if (running) {
+            btns.push(`<button type="button" class="btn btn-sm btn-outline-secondary" data-act="${key}-stop">停止</button>`);
+            if (key === "nginx") btns.push(`<button type="button" class="btn btn-sm btn-outline-secondary" data-act="${key}-reload">重载</button>`);
+          } else {
+            btns.push(`<button type="button" class="btn btn-sm btn-primary" data-act="${key}-start">启动</button>`);
+          }
+          if (info.conf) {
+            btns.push(`<button type="button" class="btn btn-sm btn-outline-primary" data-act="${key}-config" data-conf="${escapeHtml(info.conf)}">配置</button>`);
+          }
+        }
+        btns.push(`<button type="button" class="btn btn-sm btn-outline-danger" data-act="${key}-uninstall">卸载</button>`);
+      }
+      actions = `<div class="d-flex flex-wrap gap-1 mt-2">${btns.join("")}</div>`;
+    }
+    return `<div class="col-md-6 col-xl-3">
+      <div class="card soft-card h-100" data-soft="${key}">
+        <div class="card-body">
+          <div class="d-flex justify-content-between align-items-start gap-2">
+            <div>
+              <div class="fw-semibold">${name}</div>
+              <div class="small text-secondary">${installed ? (running === true ? "服务可用" : running === false ? "已安装未运行" : "已就绪") : "尚未安装"}</div>
+            </div>
+            ${statusPill(installed, running)}
+          </div>
+          <div class="small text-secondary mt-2">${bits.map((b) => escapeHtml(b)).join(" · ")}</div>
+          ${actions}
+        </div>
+      </div>
+    </div>`;
+  }
+
+  function renderConsole() {
+    const s = state.status || {};
+    $("#overview-sites").textContent = `网站 ${s.sites ?? 0} · API 15665`;
+    $("#top-meta").textContent = `站点 ${s.sites ?? 0}`;
+  }
+
+  function renderSoftware() {
+    const s = state.status || {};
+    $("#software-grid").innerHTML = [
+      softCard("Nginx", s.nginx || {}, true),
+      softCard("Fastweb", s.fastweb || {}, true),
+      softCard("MySQL", s.mysql || {}, true),
+      softCard("Redis", s.redis || {}, true),
+    ].join("");
+  }
+
+  function renderSites() {
+    const tbody = $("#sites-table tbody");
+    if (!state.sites.length) {
+      tbody.innerHTML = `<tr><td colspan="7" class="text-secondary text-center py-4">暂无网站</td></tr>`;
+      return;
+    }
+    tbody.innerHTML = state.sites
+      .map((s) => {
+        const running = !!s.running;
+        const st = running
+          ? `<span class="badge text-bg-success">运行中</span>`
+          : `<span class="badge text-bg-warning">已停止</span>`;
+        const startDis = running ? " disabled" : "";
+        const stopDis = running ? "" : " disabled";
+        const domains = Array.isArray(s.domains) && s.domains.length
+          ? s.domains
+          : String(s.domain || "").split(/[\s,;]+/).filter(Boolean);
+        const primaryTok = domains[0] || "";
+        const primary = parseDomainToken(primaryTok);
+        const domainText = primaryTok
+          ? primaryTok + (domains.length > 1 ? ` +${domains.length - 1}` : "")
+          : "—";
+        const openUrl = s.ssl_enable && Number(s.ssl_port)
+          ? `https://${primary.host}:${s.ssl_port}`
+          : `http://${primary.host}:${primary.port || 80}`;
+        const root = s.root || "";
+        return `<tr>
+          <td><strong>${escapeHtml(s.name)}</strong></td>
+          <td>${escapeHtml(s.type)}</td>
+          <td><a class="sites-link" href="${escapeHtml(openUrl)}" target="_blank" rel="noopener" title="${escapeHtml(domains.join(", "))}">${escapeHtml(domainText)}</a></td>
+          <td><button type="button" class="btn btn-link btn-sm sites-link sites-path p-0" data-site-path="${escapeHtml(root)}" title="${escapeHtml(root)}">${escapeHtml(root || "—")}</button></td>
+          <td>${escapeHtml(s.upstream || "—")}</td>
+          <td>${st}</td>
+          <td class="text-end">
+            <div class="btn-group btn-group-sm">
+              <button type="button" class="btn btn-outline-primary" data-site="${escapeHtml(s.name)}" data-act="site-start"${startDis}>启动</button>
+              <button type="button" class="btn btn-outline-primary" data-site="${escapeHtml(s.name)}" data-act="site-stop"${stopDis}>停止</button>
+              <button type="button" class="btn btn-outline-primary" data-site="${escapeHtml(s.name)}" data-act="site-restart"${stopDis}>重启</button>
+              <button type="button" class="btn btn-outline-primary" data-site="${escapeHtml(s.name)}" data-act="site-config">配置</button>
+              <button type="button" class="btn btn-outline-primary" data-site="${escapeHtml(s.name)}" data-act="site-analyze">分析</button>
+              <button type="button" class="btn btn-outline-danger" data-site="${escapeHtml(s.name)}" data-act="site-delete">删除</button>
+            </div>
+          </td>
+        </tr>`;
+      })
+      .join("");
+  }
+
+  function renderDatabases() {
+    const tbody = $("#db-table tbody");
+    if (!state.databases.length) {
+      tbody.innerHTML = `<tr><td colspan="6" class="text-secondary text-center py-4">暂无数据库记录</td></tr>`;
+      return;
+    }
+    tbody.innerHTML = state.databases
+      .map((d) => `<tr>
+        <td><strong>${escapeHtml(d.name)}</strong></td>
+        <td>${escapeHtml(d.user)}</td>
+        <td><code>${escapeHtml(d.password)}</code></td>
+        <td>${escapeHtml(d.access)}</td>
+        <td>${escapeHtml(d.host || "—")}</td>
+        <td class="text-end">
+          <div class="btn-group btn-group-sm">
+            <button type="button" class="btn btn-outline-secondary" data-db="${escapeHtml(d.name)}" data-act="db-access">权限</button>
+            <button type="button" class="btn btn-outline-danger" data-db="${escapeHtml(d.name)}" data-act="db-drop">删除</button>
+          </div>
+        </td>
+      </tr>`)
+      .join("");
+  }
+
+  function renderCrumbs(path) {
+    const parts = path === "/" ? [] : path.replace(/^\/+/, "").split("/");
+    let cur = "";
+    const bits = [`<button type="button" data-path="/">/</button>`];
+    parts.forEach((p) => {
+      cur += `/${p}`;
+      bits.push(`<span>/</span><button type="button" data-path="${escapeHtml(cur)}">${escapeHtml(p)}</button>`);
+    });
+    $("#files-crumbs").innerHTML = bits.join("");
+    $("#files-path-label").textContent = path;
+  }
+
+  function renderFiles() {
+    renderCrumbs(state.filesPath);
+    const tbody = $("#files-table tbody");
+    if (!state.files.length) {
+      tbody.innerHTML = `<tr><td colspan="5" class="text-secondary text-center py-4">空目录</td></tr>`;
+      return;
+    }
+    tbody.innerHTML = state.files
+      .map((f) => {
+        const isDir = !!f.is_dir;
+        const fullPath = joinPath(state.filesPath, f.name);
+        const full = escapeHtml(fullPath);
+        const size = Number(f.size) || 0;
+        const tooBig = !isDir && size > MAX_EDIT_BYTES;
+        const editBtn = isDir
+          ? ""
+          : tooBig
+            ? `<button type="button" class="btn btn-outline-secondary btn-sm" disabled title="超过 10MB">编辑</button>`
+            : `<button type="button" class="btn btn-outline-secondary btn-sm" data-act="file-edit" data-path="${full}" data-size="${size}">编辑</button>`;
+        return `<tr>
+          <td>
+            <span class="files-name" data-open="${full}" data-dir="${isDir ? "1" : "0"}" data-size="${size}" ${tooBig ? 'data-too-big="1"' : ""}>
+              ${fileIcon(f.name, isDir)}${escapeHtml(f.name)}
+            </span>
+          </td>
+          <td>${isDir ? "目录" : "文件"}</td>
+          <td>${isDir ? "—" : formatBytes(size)}</td>
+          <td>${formatTime(f.mtime)}</td>
+          <td class="text-end">
+            <div class="btn-group btn-group-sm">
+              ${editBtn}
+              ${isDir ? "" : `<a class="btn btn-outline-secondary btn-sm" href="/api/files/download?path=${encodeURIComponent(fullPath)}">下载</a>`}
+              <button type="button" class="btn btn-outline-secondary btn-sm" data-act="file-rename" data-path="${full}" data-name="${escapeHtml(f.name)}">重命名</button>
+              <button type="button" class="btn btn-outline-danger btn-sm" data-act="file-delete" data-path="${full}" data-dir="${isDir ? "1" : "0"}">删除</button>
+            </div>
+          </td>
+        </tr>`;
+      })
+      .join("");
+  }
+
+  async function loadFiles(path) {
+    try {
+      const data = await api(`/api/files/list?path=${encodeURIComponent(path || "/")}`);
+      state.filesPath = (data && data.path) || path || "/";
+      state.files = (data && data.entries) || [];
+      renderFiles();
+    } catch (err) {
+      toast(err.message || String(err), "err");
+    }
+  }
+
+  function openModal({ title, bodyHtml, onSubmit }) {
+    $("#modal-title").textContent = title;
+    $("#modal-body").innerHTML = bodyHtml;
+    const form = $("#modal-form");
+    const cleanup = () => {
+      form.onsubmit = null;
+      state.bsModal.hide();
+    };
+    $("#modal-close").onclick = cleanup;
+    $("#modal-cancel").onclick = cleanup;
+    form.onsubmit = async (e) => {
+      e.preventDefault();
+      await withBusy(async () => {
+        await onSubmit(new FormData(form));
+        cleanup();
+        await refresh();
+      }, $("#modal-ok"));
+    };
+    state.bsModal.show();
+  }
+
+  function syncSiteFormFields(root) {
+    const type = root.querySelector('[name="type"]')?.value || "static";
+    const up = root.querySelector(".field-upstream");
+    const tip = root.querySelector(".field-upstream-tip");
+    if (up) up.classList.toggle("d-none", type !== "proxy");
+    if (tip) {
+      tip.textContent =
+        type === "proxy"
+          ? "反向代理需填写上游地址,例如 127.0.0.1:9000"
+          : type === "fastweb"
+            ? "Fastweb 站点会自动分配后端端口"
+            : "静态站仅需名称/域名/端口";
+    }
+  }
+
+  async function refresh() {
+    try {
+      const [status, sites] = await Promise.all([api("/api/status"), api("/api/websites")]);
+      state.status = status;
+      state.sites = Array.isArray(sites) ? sites : [];
+      try {
+        state.databases = (await api("/api/mysql/databases")) || [];
+      } catch (_) {
+        state.databases = [];
+      }
+      $("#live-dot").className = "live is-ok small d-flex align-items-center gap-2";
+      $("#live-label").textContent = "在线";
+      renderConsole();
+      renderSoftware();
+      renderSites();
+      renderDatabases();
+      const hash = (location.hash || "#console").slice(1);
+      if (hash === "files") await loadFiles(state.filesPath);
+    } catch (err) {
+      $("#live-dot").className = "live is-bad small d-flex align-items-center gap-2";
+      $("#live-label").textContent = "离线";
+      toast(err.message || String(err), "err");
+    }
+  }
+
+  async function softAction(act, btn) {
+    if (act.endsWith("-config")) {
+      const conf = btn.dataset.conf;
+      await withBusy(async () => openConfigFile(conf), btn);
+      return;
+    }
+    const map = {
+      "nginx-install": ["/api/nginx/install", {}],
+      "nginx-uninstall": ["/api/nginx/uninstall", {}],
+      "nginx-start": ["/api/nginx/start", {}],
+      "nginx-stop": ["/api/nginx/stop", {}],
+      "nginx-reload": ["/api/nginx/reload", {}],
+      "fastweb-install": ["/api/fastweb/install", { debug: false }],
+      "fastweb-uninstall": ["/api/fastweb/uninstall", {}],
+      "mysql-install": ["/api/mysql/install", {}],
+      "mysql-uninstall": ["/api/mysql/uninstall", {}],
+      "mysql-start": ["/api/mysql/start", {}],
+      "mysql-stop": ["/api/mysql/stop", {}],
+      "redis-install": ["/api/redis/install", {}],
+      "redis-uninstall": ["/api/redis/uninstall", {}],
+      "redis-start": ["/api/redis/start", {}],
+      "redis-stop": ["/api/redis/stop", {}],
+    };
+    const item = map[act];
+    if (!item) return;
+    if (act.endsWith("-uninstall") && !confirm("确认卸载?")) return;
+    const queued = act.endsWith("-install") || act.endsWith("-uninstall");
+    await withBusy(async () => {
+      if (!queued) toast("执行中…");
+      const data = await api(item[0], { method: "POST", body: item[1] });
+      if (queued && data && data.task_id) {
+        toast("已加入任务队列");
+        await refreshTasks();
+        openTaskPanel(data.task_id);
+      } else {
+        toast("完成");
+        await refresh();
+      }
+    }, btn);
+  }
+
+  function updateTaskBadge() {
+    const n = state.taskActive || 0;
+    $("#task-badge-count").textContent = String(n);
+    $("#task-badge").classList.toggle("has-active", n > 0);
+  }
+
+  function isActiveTask(t) {
+    return t && (t.status === "running" || t.status === "pending");
+  }
+
+  function visibleTasks() {
+    if (state.taskTab === "history") {
+      return state.tasks.filter((t) => !isActiveTask(t));
+    }
+    return state.tasks.filter(isActiveTask);
+  }
+
+  function pickDefaultTaskId(preferId) {
+    const list = visibleTasks();
+    if (preferId && list.some((t) => t.id === preferId)) return preferId;
+    const running = list.find((t) => t.status === "running");
+    if (running) return running.id;
+    if (list.length) return list[0].id;
+    return "";
+  }
+
+  function syncTaskTabs() {
+    const activeBtn = $("#task-tab-active");
+    const histBtn = $("#task-tab-history");
+    const clearBtn = $("#task-clear-finished");
+    const onActive = state.taskTab === "active";
+    activeBtn.className = onActive ? "btn btn-warning" : "btn btn-outline-light";
+    histBtn.className = !onActive ? "btn btn-light" : "btn btn-outline-light";
+    clearBtn.classList.toggle("d-none", onActive);
+    const nActive = state.tasks.filter(isActiveTask).length;
+    const nHist = state.tasks.length - nActive;
+    activeBtn.textContent = nActive ? `进行中 (${nActive})` : "进行中";
+    histBtn.textContent = nHist ? `历史任务 (${nHist})` : "历史任务";
+  }
+
+  function renderTaskList() {
+    const host = $("#task-list");
+    syncTaskTabs();
+    const list = visibleTasks();
+    if (!list.length) {
+      host.innerHTML = `<div class="p-3 text-secondary small">${
+        state.taskTab === "history" ? "暂无历史任务" : "当前没有进行中的任务"
+      }</div>`;
+      return;
+    }
+    host.innerHTML = list
+      .map((t) => {
+        const selected = t.id === state.selectedTaskId ? " is-active" : "";
+        const running = t.status === "running" ? " is-running" : "";
+        const st =
+          t.status === "running" ? "运行中" :
+          t.status === "pending" ? "排队" :
+          t.status === "success" ? "成功" :
+          t.status === "failed" ? "失败" : (t.status || "—");
+        const dot = t.status === "running" ? `<span class="task-running-dot"></span>` : "";
+        return `<button type="button" class="task-item${selected}${running}" data-task-id="${escapeHtml(t.id)}">
+          <strong>${dot}${escapeHtml(t.title || t.type)}</strong>
+          <span class="small opacity-75">${escapeHtml(st)} · ${escapeHtml(t.type || "")}</span>
+        </button>`;
+      })
+      .join("");
+  }
+
+  function selectTask(id, resetLog) {
+    if (!id) {
+      state.selectedTaskId = "";
+      $("#task-log").textContent = "";
+      $("#task-log-title").textContent = "日志";
+      $("#task-log-status").textContent = "—";
+      $("#task-log-status").className = "badge text-bg-secondary";
+      return;
+    }
+    if (resetLog || id !== state.selectedTaskId) {
+      state.selectedTaskId = id;
+      state.taskLogOffset = 0;
+      $("#task-log").textContent = "";
+    } else {
+      state.selectedTaskId = id;
+    }
+  }
+
+  async function refreshTasks() {
+    try {
+      const data = await api("/api/tasks");
+      const tasks = (data && data.tasks) || [];
+      let softChanged = false;
+      for (const t of tasks) {
+        const prev = state.taskStatusMap[t.id];
+        const now = t.status || "";
+        if (
+          prev &&
+          (prev === "running" || prev === "pending") &&
+          (now === "success" || now === "failed") &&
+          /\.(install|uninstall)$/.test(t.type || "")
+        ) {
+          softChanged = true;
+        }
+        state.taskStatusMap[t.id] = now;
+      }
+      state.tasks = tasks;
+      state.taskActive = Number((data && data.active) || 0);
+      updateTaskBadge();
+      if (state.taskPanelOpen) {
+        const nextId = pickDefaultTaskId(state.selectedTaskId);
+        if (nextId !== state.selectedTaskId) selectTask(nextId, true);
+        else if (!nextId) selectTask("", true);
+        renderTaskList();
+        if (state.selectedTaskId) await pollTaskLog(false);
+      }
+      if (softChanged) await refresh();
+    } catch (_) {}
+  }
+
+  async function pollTaskLog(forceBottom) {
+    if (!state.selectedTaskId) return;
+    const id = state.selectedTaskId;
+    try {
+      const data = await api(`/api/tasks/log?id=${encodeURIComponent(id)}&offset=${state.taskLogOffset}`);
+      const pre = $("#task-log");
+      const lines = (data && data.lines) || [];
+      if (lines.length) {
+        const atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 48;
+        pre.textContent += (pre.textContent ? "\n" : "") + lines.join("\n");
+        state.taskLogOffset = Number(data.next_offset || state.taskLogOffset);
+        if (forceBottom || atBottom) pre.scrollTop = pre.scrollHeight;
+      } else if (data && typeof data.next_offset === "number") {
+        state.taskLogOffset = data.next_offset;
+      }
+      const meta = state.tasks.find((t) => t.id === id) || {};
+      $("#task-log-title").textContent = meta.title || id;
+      const st = $("#task-log-status");
+      const status = (data && data.status) || meta.status || "—";
+      st.className = "badge " + (
+        status === "running" || status === "success" ? "text-bg-success" :
+        status === "pending" ? "text-bg-warning" :
+        status === "failed" ? "text-bg-danger" : "text-bg-secondary"
+      );
+      st.textContent =
+        status === "running" ? "运行中" :
+        status === "pending" ? "排队" :
+        status === "success" ? "成功" :
+        status === "failed" ? "失败" : status;
+    } catch (_) {}
+  }
+
+  function setTaskTab(tab) {
+    state.taskTab = tab === "history" ? "history" : "active";
+    const nextId = pickDefaultTaskId("");
+    selectTask(nextId, true);
+    renderTaskList();
+    if (state.selectedTaskId) pollTaskLog(true);
+  }
+
+  function openTaskPanel(taskId) {
+    state.taskPanelOpen = true;
+    $("#task-panel").hidden = false;
+    if (taskId) {
+      const t = state.tasks.find((x) => x.id === taskId);
+      state.taskTab = t && !isActiveTask(t) ? "history" : "active";
+      selectTask(taskId, true);
+    } else {
+      state.taskTab = "active";
+    }
+    refreshTasks();
+  }
+
+  function closeTaskPanel() {
+    state.taskPanelOpen = false;
+    $("#task-panel").hidden = true;
+  }
+
+  function fileToBase64(file) {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = () => {
+        const s = String(reader.result || "");
+        const i = s.indexOf(",");
+        resolve(i >= 0 ? s.slice(i + 1) : s);
+      };
+      reader.onerror = () => reject(new Error("读取文件失败"));
+      reader.readAsDataURL(file);
+    });
+  }
+
+  const MAX_UPLOAD_BYTES = 48 * 1024 * 1024;
+
+  function normalizeRelPath(p) {
+    return String(p || "")
+      .replace(/\\/g, "/")
+      .replace(/^\/+/, "")
+      .replace(/\/{2,}/g, "/");
+  }
+
+  function renderUploadQueue() {
+    const host = $("#upload-queue");
+    const summary = $("#upload-queue-summary");
+    const clearBtn = $("#upload-clear-queue");
+    const startBtn = $("#upload-start");
+    const q = state.uploadQueue;
+    if (!q.length) {
+      host.innerHTML = "";
+      summary.textContent = "未选择文件";
+      clearBtn.hidden = true;
+      startBtn.disabled = true;
+      return;
+    }
+    const total = q.reduce((n, it) => n + (it.file ? it.file.size : 0), 0);
+    summary.textContent = `${q.length} 项 · ${formatBytes(total)}`;
+    clearBtn.hidden = state.uploadBusy;
+    startBtn.disabled = state.uploadBusy || !q.some((it) => it.status === "pending");
+    host.innerHTML = q
+      .map((it) => {
+        const st =
+          it.status === "ok"
+            ? "完成"
+            : it.status === "err"
+              ? "失败"
+              : it.status === "uploading"
+                ? "上传中"
+                : it.status === "skip"
+                  ? "跳过"
+                  : "待传";
+        const cls =
+          it.status === "ok"
+            ? "is-ok"
+            : it.status === "err"
+              ? "is-err"
+              : it.status === "uploading"
+                ? "is-run"
+                : "";
+        return `<div class="upload-queue-item ${cls}" title="${escapeHtml(it.error || it.relativePath)}">
+          <span class="path">${escapeHtml(it.relativePath)}</span>
+          <span class="size">${it.file ? formatBytes(it.file.size) : "—"}</span>
+          <span class="st">${st}</span>
+        </div>`;
+      })
+      .join("");
+  }
+
+  function addUploadItems(items) {
+    const seen = new Set(state.uploadQueue.map((it) => it.relativePath));
+    let added = 0;
+    for (const it of items) {
+      const relativePath = normalizeRelPath(it.relativePath);
+      if (!relativePath || seen.has(relativePath)) continue;
+      if (relativePath.split("/").includes("..")) continue;
+      if (it.file && it.file.size > MAX_UPLOAD_BYTES) {
+        state.uploadQueue.push({
+          id: `${Date.now()}-${added}-${relativePath}`,
+          relativePath,
+          file: it.file,
+          status: "err",
+          error: "单文件超过 48MB",
+        });
+      } else {
+        state.uploadQueue.push({
+          id: `${Date.now()}-${added}-${relativePath}`,
+          relativePath,
+          file: it.file || null,
+          isDir: !!it.isDir,
+          status: "pending",
+          error: "",
+        });
+      }
+      seen.add(relativePath);
+      added += 1;
+    }
+    renderUploadQueue();
+  }
+
+  function itemsFromFileList(fileList, fromDirPicker) {
+    const out = [];
+    for (const file of fileList || []) {
+      let rel = fromDirPicker
+        ? file.webkitRelativePath || file.name
+        : file.webkitRelativePath || file.name;
+      rel = normalizeRelPath(rel);
+      if (!rel) continue;
+      out.push({ relativePath: rel, file });
+    }
+    return out;
+  }
+
+  function readAllDirectoryEntries(reader) {
+    return new Promise((resolve, reject) => {
+      const all = [];
+      const readBatch = () => {
+        reader.readEntries((entries) => {
+          if (!entries.length) {
+            resolve(all);
+            return;
+          }
+          all.push(...entries);
+          readBatch();
+        }, reject);
+      };
+      readBatch();
+    });
+  }
+
+  async function traverseFileSystemEntry(entry, prefix = "") {
+    const name = entry.name;
+    const base = prefix ? `${prefix}${name}` : name;
+    if (entry.isFile) {
+      const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
+      return [{ relativePath: base, file }];
+    }
+    if (entry.isDirectory) {
+      const reader = entry.createReader();
+      const children = await readAllDirectoryEntries(reader);
+      if (!children.length) {
+        return [{ relativePath: base, file: null, isDir: true }];
+      }
+      const out = [];
+      for (const child of children) {
+        out.push(...(await traverseFileSystemEntry(child, `${base}/`)));
+      }
+      return out;
+    }
+    return [];
+  }
+
+  async function itemsFromDataTransfer(dt) {
+    const items = dt && dt.items ? [...dt.items] : [];
+    if (items.some((it) => typeof it.webkitGetAsEntry === "function" && it.webkitGetAsEntry())) {
+      const out = [];
+      for (const it of items) {
+        if (it.kind !== "file") continue;
+        const entry = it.webkitGetAsEntry && it.webkitGetAsEntry();
+        if (!entry) continue;
+        out.push(...(await traverseFileSystemEntry(entry)));
+      }
+      return out;
+    }
+    return itemsFromFileList(dt.files, false);
+  }
+
+  function setUploadProgress(doneBytes, totalBytes, label) {
+    const wrap = $("#upload-progress");
+    wrap.hidden = false;
+    const pct = totalBytes > 0 ? Math.min(100, Math.round((doneBytes / totalBytes) * 100)) : 0;
+    $("#upload-progress-bar").style.width = `${pct}%`;
+    $("#upload-progress-pct").textContent = `${pct}%`;
+    $("#upload-progress-label").textContent = label || "";
+  }
+
+  function uploadFileWithProgress(path, file, onProgress) {
+    return fileToBase64(file).then(
+      (content_base64) =>
+        new Promise((resolve, reject) => {
+          const xhr = new XMLHttpRequest();
+          xhr.open("POST", "/api/files/upload");
+          xhr.setRequestHeader("Content-Type", "application/json");
+          xhr.setRequestHeader("Accept", "application/json");
+          xhr.upload.onprogress = (e) => {
+            if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total);
+          };
+          xhr.onload = () => {
+            let json = null;
+            try {
+              json = JSON.parse(xhr.responseText || "{}");
+            } catch (_) {
+              reject(new Error(`无效响应 (${xhr.status})`));
+              return;
+            }
+            if (json && Number(json.code) === 401) {
+              location.replace("/login.html");
+              reject(new Error(json.msg || "未登录"));
+              return;
+            }
+            if (!json || json.code !== 200) {
+              reject(new Error((json && json.msg) || `上传失败 (${xhr.status})`));
+              return;
+            }
+            resolve(json.data);
+          };
+          xhr.onerror = () => reject(new Error("网络错误"));
+          xhr.send(JSON.stringify({ path, content_base64 }));
+        })
+    );
+  }
+
+  function openUploadModal() {
+    state.uploadQueue = [];
+    state.uploadBusy = false;
+    $("#upload-target-path").textContent = state.filesPath || "/";
+    $("#upload-progress").hidden = true;
+    $("#upload-progress-bar").style.width = "0%";
+    $("#upload-progress-pct").textContent = "0%";
+    $("#upload-progress-label").textContent = "";
+    renderUploadQueue();
+    state.uploadModal.show();
+  }
+
+  function closeUploadModal() {
+    if (state.uploadBusy) return;
+    state.uploadModal.hide();
+  }
+
+  async function startUploadQueue() {
+    if (state.uploadBusy) return;
+    const pending = state.uploadQueue.filter((it) => it.status === "pending");
+    if (!pending.length) return;
+    state.uploadBusy = true;
+    renderUploadQueue();
+    $("#upload-start").disabled = true;
+    $("#upload-modal-cancel").disabled = true;
+    $("#upload-modal-close").disabled = true;
+    $("#upload-clear-queue").hidden = true;
+    $("#upload-pick-files").disabled = true;
+    $("#upload-pick-dir").disabled = true;
+
+    const totalBytes = pending.reduce((n, it) => n + (it.file ? it.file.size : 0), 0) || 1;
+    let completedBytes = 0;
+    let ok = 0;
+    let fail = 0;
+
+    try {
+      for (const it of pending) {
+        it.status = "uploading";
+        renderUploadQueue();
+        const dest = joinPath(state.filesPath || "/", it.relativePath);
+        setUploadProgress(
+          completedBytes,
+          totalBytes,
+          `正在上传 ${it.relativePath}`
+        );
+        try {
+          if (it.isDir && !it.file) {
+            await api("/api/files/mkdir", { method: "POST", body: { path: dest } }).catch((err) => {
+              const msg = err.message || String(err);
+              if (!/已存在|exists/i.test(msg)) throw err;
+            });
+          } else {
+            setUploadProgress(completedBytes, totalBytes, `读取 ${it.relativePath}`);
+            await uploadFileWithProgress(dest, it.file, (loaded, total) => {
+              const filePct = total > 0 ? loaded / total : 1;
+              setUploadProgress(
+                completedBytes + (it.file.size * filePct),
+                totalBytes,
+                `正在上传 ${it.relativePath}`
+              );
+            });
+            completedBytes += it.file.size;
+          }
+          it.status = "ok";
+          ok += 1;
+        } catch (err) {
+          it.status = "err";
+          it.error = err.message || String(err);
+          fail += 1;
+          if (it.file) completedBytes += it.file.size;
+        }
+        renderUploadQueue();
+      }
+      setUploadProgress(totalBytes, totalBytes, fail ? `完成:成功 ${ok},失败 ${fail}` : `全部上传成功(${ok})`);
+      if (ok) toast(fail ? `上传完成(成功 ${ok},失败 ${fail})` : "上传完成");
+      else if (fail) toast(`上传失败(${fail})`, "err");
+      await loadFiles(state.filesPath);
+    } finally {
+      state.uploadBusy = false;
+      $("#upload-modal-cancel").disabled = false;
+      $("#upload-modal-close").disabled = false;
+      $("#upload-pick-files").disabled = false;
+      $("#upload-pick-dir").disabled = false;
+      renderUploadQueue();
+    }
+  }
+
+  function bindUploadModal() {
+    state.uploadModal = new bootstrap.Modal($("#upload-modal"), { backdrop: "static" });
+    const zone = $("#upload-dropzone");
+
+    $("#btn-files-upload").addEventListener("click", () => openUploadModal());
+    $("#upload-modal-close").addEventListener("click", () => closeUploadModal());
+    $("#upload-modal-cancel").addEventListener("click", () => closeUploadModal());
+    $("#upload-clear-queue").addEventListener("click", () => {
+      if (state.uploadBusy) return;
+      state.uploadQueue = [];
+      renderUploadQueue();
+    });
+    $("#upload-start").addEventListener("click", () => startUploadQueue());
+    $("#upload-pick-files").addEventListener("click", () => $("#upload-input-files").click());
+    $("#upload-pick-dir").addEventListener("click", () => $("#upload-input-dir").click());
+    $("#upload-input-files").addEventListener("change", (e) => {
+      addUploadItems(itemsFromFileList(e.target.files, false));
+      e.target.value = "";
+    });
+    $("#upload-input-dir").addEventListener("change", (e) => {
+      addUploadItems(itemsFromFileList(e.target.files, true));
+      e.target.value = "";
+    });
+
+    ["dragenter", "dragover"].forEach((ev) => {
+      zone.addEventListener(ev, (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        zone.classList.add("is-dragover");
+      });
+    });
+    ["dragleave", "drop"].forEach((ev) => {
+      zone.addEventListener(ev, (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (ev === "dragleave") zone.classList.remove("is-dragover");
+      });
+    });
+    zone.addEventListener("drop", async (e) => {
+      zone.classList.remove("is-dragover");
+      if (state.uploadBusy) return;
+      try {
+        const items = await itemsFromDataTransfer(e.dataTransfer);
+        addUploadItems(items);
+      } catch (err) {
+        toast(err.message || String(err), "err");
+      }
+    });
+  }
+
+  function setSiteSettingsTab(tab) {
+    $$(".site-settings-tab").forEach((b) => b.classList.toggle("is-active", b.dataset.siteTab === tab));
+    $$(".site-settings-panel").forEach((p) => p.classList.toggle("is-active", p.dataset.sitePanel === tab));
+    if (tab === "cert") renderSslDomainChecklist(selectedSslHosts());
+    if (tab === "nginx") loadSiteNginxConf().catch((err) => toast(err.message || String(err), "err"));
+    if (tab === "logs") loadSiteLog().catch((err) => toast(err.message || String(err), "err"));
+  }
+
+  function domainsFromText(text) {
+    return String(text || "")
+      .split(/[\s,;]+/)
+      .map((s) => s.trim())
+      .filter(Boolean);
+  }
+
+  function parseDomainToken(tok) {
+    const s = String(tok || "").trim();
+    const m = s.match(/^(.*):(\d{1,5})$/);
+    if (m) {
+      const port = Number(m[2]);
+      if (port >= 1 && port <= 65535) return { host: m[1], port };
+    }
+    return { host: s, port: 80 };
+  }
+
+  function formatDomainToken(host, port) {
+    const p = Number(port) || 80;
+    return p === 80 ? host : `${host}:${p}`;
+  }
+
+  function renderSslDomainChecklist(selectedHosts) {
+    const host = $("#site-ssl-domain-list");
+    if (!host) return;
+    const tokens = domainsFromText($("#site-set-domain")?.value || "")
+      .concat(Array.isArray(state.siteSettings?.domains) ? state.siteSettings.domains : []);
+    const uniq = [];
+    const seen = new Set();
+    for (const tok of tokens) {
+      const { host: h, port } = parseDomainToken(tok);
+      if (!h || seen.has(h)) continue;
+      seen.add(h);
+      uniq.push({ host: h, port });
+    }
+    // Prefer domain textarea order if present
+    const fromText = domainsFromText($("#site-set-domain")?.value || "");
+    const ordered = [];
+    const orderedSeen = new Set();
+    for (const tok of fromText) {
+      const { host: h, port } = parseDomainToken(tok);
+      if (!h || orderedSeen.has(h)) continue;
+      orderedSeen.add(h);
+      ordered.push({ host: h, port });
+    }
+    if (!ordered.length) ordered.push(...uniq);
+
+    const selected = new Set(
+      (selectedHosts || ordered.map((x) => x.host)).map((h) => parseDomainToken(h).host)
+    );
+    if (!ordered.length) {
+      host.innerHTML = `<div class="small text-secondary px-1 py-1">请先在「域名」中配置域名</div>`;
+      return;
+    }
+    host.innerHTML = ordered
+      .map((item, i) => {
+        const id = `ssl-dom-${i}`;
+        const checked = selected.has(item.host) ? " checked" : "";
+        const hint = item.port && item.port !== 80 ? `<span class="port-hint">:${item.port}</span>` : "";
+        return `<div class="form-check">
+          <input class="form-check-input" type="checkbox" value="${escapeHtml(item.host)}" id="${id}"${checked} />
+          <label class="form-check-label" for="${id}"><span class="host">${escapeHtml(item.host)}</span>${hint}</label>
+        </div>`;
+      })
+      .join("");
+  }
+
+  function selectedSslHosts() {
+    return $$("#site-ssl-domain-list input[type=checkbox]:checked").map((el) => el.value).filter(Boolean);
+  }
+
+  function fillSiteSettingsForm(site) {
+    state.siteSettings = site;
+    $("#site-settings-name").textContent = site.name || "";
+    $("#site-settings-sub").textContent = `${site.type || ""} · ${site.root || ""}`;
+    const domains = Array.isArray(site.domains) && site.domains.length
+      ? site.domains
+      : domainsFromText(site.domain);
+    $("#site-set-domain").value = domains.join("\n");
+    $("#site-set-ssl-enable").checked = !!site.ssl_enable;
+    $("#site-set-ssl-port").value = site.ssl_port || 443;
+    $("#site-set-ssl-cert").value = site.ssl_cert || "";
+    $("#site-set-ssl-key").value = site.ssl_key || "";
+    $("#site-set-conf-path").textContent = site.conf || "";
+    $("#site-set-nginx-conf").value = "";
+    state.siteLogKind = "access";
+    $("#site-log-tab-access").classList.add("active");
+    $("#site-log-tab-error").classList.remove("active");
+    $("#site-log-view").textContent = "加载中…";
+    renderSslDomainChecklist();
+  }
+
+  async function openSiteSettings(name) {
+    const site = state.sites.find((s) => s.name === name);
+    if (!site) throw new Error("未找到网站");
+    fillSiteSettingsForm(site);
+    setSiteSettingsTab("domain");
+    state.siteSettingsModal.show();
+  }
+
+  async function loadSiteNginxConf() {
+    const site = state.siteSettings;
+    if (!site || !site.conf) throw new Error("配置路径未知");
+    const data = await api(`/api/files/read?path=${encodeURIComponent(site.conf)}`);
+    $("#site-set-nginx-conf").value = (data && data.content) || "";
+  }
+
+  async function saveSiteNginxConf() {
+    const site = state.siteSettings;
+    if (!site || !site.conf) throw new Error("配置路径未知");
+    await api("/api/files/write", {
+      method: "POST",
+      body: { path: site.conf, content: $("#site-set-nginx-conf").value },
+    });
+    await api("/api/nginx/reload", { method: "POST", body: {} });
+    toast("Nginx 配置已保存并重载");
+  }
+
+  async function loadSiteLog() {
+    const site = state.siteSettings;
+    if (!site) return;
+    const view = $("#site-log-view");
+    const esc = (s) =>
+      String(s ?? "")
+        .replace(/&/g, "&amp;")
+        .replace(/</g, "&lt;")
+        .replace(/>/g, "&gt;")
+        .replace(/"/g, "&quot;");
+
+    if (state.siteLogKind === "error") {
+      view.classList.add("table-mode");
+      try {
+        const qs = new URLSearchParams({ name: site.name, kind: "error", limit: "200" });
+        const data = await api(`/api/websites/logs?${qs.toString()}`);
+        const items = (data && data.items) || [];
+        if (!items.length) {
+          view.innerHTML = `<div class="site-log-empty">暂无错误日志</div>`;
+          return;
+        }
+        const rows = items
+          .map((r) => `<tr>
+            <td>${esc(r.time)}</td>
+            <td>${esc(r.level)}</td>
+            <td class="uri">${esc(r.message || r.raw)}</td>
+          </tr>`)
+          .join("");
+        view.innerHTML = `<table>
+          <thead><tr><th>时间</th><th>级别</th><th>内容</th></tr></thead>
+          <tbody>${rows}</tbody>
+        </table>`;
+        view.scrollTop = 0;
+      } catch (err) {
+        view.classList.remove("table-mode");
+        view.textContent = err.message || String(err);
+      }
+      return;
+    }
+
+    view.classList.add("table-mode");
+    const qs = new URLSearchParams({ name: site.name, kind: "access", limit: "200" });
+    try {
+      const data = await api(`/api/websites/logs?${qs.toString()}`);
+      const items = (data && data.items) || [];
+      if (!items.length) {
+        view.innerHTML = `<div class="site-log-empty">暂无访问日志(写入 access.log 后会自动入库)</div>`;
+        return;
+      }
+      const statusClass = (code) => {
+        const n = Number(code);
+        if (n >= 500) return "status-server";
+        if (n >= 400) return "status-client";
+        if (n >= 300) return "status-redir";
+        return "status-ok";
+      };
+      const rows = items
+        .map((r) => {
+          const uri = esc(r.uri || "") + (r.args ? "?" + esc(r.args) : "");
+          const rt = r.request_time != null ? Number(r.request_time).toFixed(3) : "";
+          return `<tr>
+            <td title="time_ms=${esc(r.time_ms)}">${esc(r.time)}</td>
+            <td>${esc(r.ip)}</td>
+            <td>${esc(r.method)}</td>
+            <td class="${statusClass(r.status)}">${esc(r.status)}</td>
+            <td class="uri">${uri}</td>
+            <td>${esc(rt)}</td>
+            <td>${esc(formatBytes(Number(r.bytes_recv) || 0))}</td>
+            <td>${esc(formatBytes(Number(r.bytes_sent) || 0))}</td>
+          </tr>`;
+        })
+        .join("");
+      view.innerHTML = `<table>
+        <thead><tr>
+          <th>时间</th><th>IP</th><th>方法</th><th>状态</th><th>URI</th>
+          <th>耗时(s)</th><th>接收</th><th>发送</th>
+        </tr></thead>
+        <tbody>${rows}</tbody>
+      </table>`;
+      view.scrollTop = 0;
+    } catch (err) {
+      view.classList.remove("table-mode");
+      view.textContent = err.message || String(err);
+    }
+  }
+
+  async function saveSiteDomain() {
+    const site = state.siteSettings;
+    if (!site) return;
+    const domains = domainsFromText($("#site-set-domain").value);
+    if (!domains.length) throw new Error("至少填写一个域名");
+    const updated = await api("/api/websites/update", {
+      method: "POST",
+      body: { name: site.name, domains },
+    });
+    Object.assign(site, updated || {});
+    fillSiteSettingsForm(site);
+    toast("域名已保存");
+    await refresh();
+  }
+
+  async function saveSiteCert() {
+    const site = state.siteSettings;
+    if (!site) return;
+    const domains = domainsFromText($("#site-set-domain").value);
+    const ssl_enable = $("#site-set-ssl-enable").checked;
+    const ssl_port = Number($("#site-set-ssl-port").value || 443);
+    const ssl_cert = $("#site-set-ssl-cert").value.trim();
+    const ssl_key = $("#site-set-ssl-key").value.trim();
+    const updated = await api("/api/websites/update", {
+      method: "POST",
+      body: {
+        name: site.name,
+        domains: domains.length ? domains : domainsFromText(site.domain),
+        ssl_enable,
+        ssl_port,
+        ssl_cert,
+        ssl_key,
+      },
+    });
+    Object.assign(site, updated || {});
+    fillSiteSettingsForm(site);
+    toast("证书设置已保存");
+    await refresh();
+  }
+
+  async function applyLetsEncrypt() {
+    const site = state.siteSettings;
+    if (!site) return;
+    await saveSiteDomain();
+    renderSslDomainChecklist(selectedSslHosts());
+    const hosts = selectedSslHosts();
+    if (!hosts.length) throw new Error("请至少勾选一个要申请证书的域名");
+    const data = await api("/api/websites/ssl/apply", {
+      method: "POST",
+      body: { name: site.name, domains: hosts },
+    });
+    toast(`已加入任务队列(${hosts.length} 个域名,一张证书)`);
+    if (data && data.task_id) {
+      openTaskPanel(data.task_id);
+    } else {
+      openTaskPanel();
+    }
+  }
+
+  async function uploadSiteSslFile(kind, file) {
+    const site = state.siteSettings;
+    if (!site || !site.root) throw new Error("网站目录未知");
+    const sslDir = joinPath(site.root, "ssl");
+    await api("/api/files/mkdir", { method: "POST", body: { path: sslDir } }).catch(() => {});
+    const dest = joinPath(sslDir, file.name);
+    await api("/api/files/upload", {
+      method: "POST",
+      body: { path: dest, content_base64: await fileToBase64(file) },
+    });
+    if (kind === "cert") $("#site-set-ssl-cert").value = dest;
+    else $("#site-set-ssl-key").value = dest;
+    toast("证书文件已上传");
+  }
+
+  function destroyAnalyticsCharts() {
+    (state.analyticsCharts || []).forEach((c) => {
+      try { c.destroy(); } catch (_) {}
+    });
+    state.analyticsCharts = [];
+  }
+
+  function analyticsLabel(ms) {
+    const d = new Date(Number(ms) || 0);
+    const mo = String(d.getMonth() + 1).padStart(2, "0");
+    const day = String(d.getDate()).padStart(2, "0");
+    const hh = String(d.getHours()).padStart(2, "0");
+    const mm = String(d.getMinutes()).padStart(2, "0");
+    return `${mo}-${day} ${hh}:${mm}`;
+  }
+
+  function renderAnalytics(data) {
+    const body = $("#analytics-body");
+    if (!body) return;
+    destroyAnalyticsCharts();
+    const s = data.summary || {};
+    const cc = data.cc || {};
+    const errors = data.errors || {};
+    const esc = escapeHtml;
+
+    const cards = [
+      ["请求数", s.requests ?? 0],
+      ["独立 IP", s.unique_ips ?? 0],
+      ["接收", formatBytes(s.bytes_recv)],
+      ["发送", formatBytes(s.bytes_sent)],
+      ["平均耗时", `${Number(s.avg_request_time || 0).toFixed(3)} s`],
+      ["错误率", `${Number(s.error_rate || 0).toFixed(2)}%`],
+      ["4xx", s.status_4xx ?? 0],
+      ["5xx", s.status_5xx ?? 0],
+    ]
+      .map(
+        ([k, v]) => `<div class="col-6 col-md-3">
+        <div class="analytics-stat">
+          <div class="label">${esc(k)}</div>
+          <div class="value">${esc(v)}</div>
+        </div>
+      </div>`
+      )
+      .join("");
+
+    const suspects = Array.isArray(cc.suspects) ? cc.suspects : [];
+    const suspectHtml = suspects.length
+      ? `<div class="table-responsive"><table class="table table-sm align-middle mb-0">
+          <thead><tr><th>IP</th><th>请求</th><th>错误</th><th>占比</th><th>原因</th></tr></thead>
+          <tbody>${suspects
+            .map(
+              (r) => `<tr>
+            <td><code>${esc(r.ip)}</code></td>
+            <td>${esc(r.count)}</td>
+            <td>${esc(r.errors)}</td>
+            <td>${Number(r.share || 0).toFixed(1)}%</td>
+            <td class="text-warning">${esc(r.reason || "")}</td>
+          </tr>`
+            )
+            .join("")}</tbody></table></div>`
+      : `<div class="small text-secondary">未发现明显异常 IP</div>`;
+
+    const topIpRows = (data.top_ips || [])
+      .map(
+        (r) => `<tr>
+        <td><code>${esc(r.ip)}</code></td>
+        <td>${esc(r.count)}</td>
+        <td>${esc(r.errors)}</td>
+        <td>${esc(formatBytes(r.bytes_recv))}</td>
+        <td>${esc(formatBytes(r.bytes_sent))}</td>
+        <td>${Number(r.share || 0).toFixed(1)}%</td>
+      </tr>`
+      )
+      .join("");
+
+    const topUriRows = (data.top_uris || [])
+      .map(
+        (r) => `<tr>
+        <td class="text-break">${esc(r.uri)}</td>
+        <td>${esc(r.count)}</td>
+        <td>${esc(formatBytes(r.bytes_sent))}</td>
+        <td>${Number(r.avg_request_time || 0).toFixed(3)}s</td>
+      </tr>`
+      )
+      .join("");
+
+    const slowRows = (data.slow_uris || [])
+      .map(
+        (r) => `<tr>
+        <td class="text-break">${esc(r.uri)}</td>
+        <td>${esc(r.count)}</td>
+        <td>${Number(r.avg_request_time || 0).toFixed(3)}s</td>
+        <td>${Number(r.max_request_time || 0).toFixed(3)}s</td>
+      </tr>`
+      )
+      .join("");
+
+    const errRecent = (errors.recent || [])
+      .map(
+        (r) => `<tr>
+        <td>${esc(r.time)}</td>
+        <td>${esc(r.level)}</td>
+        <td class="text-break">${esc(r.message)}</td>
+      </tr>`
+      )
+      .join("");
+
+    body.innerHTML = `
+      <div class="row g-2 mb-3">${cards}</div>
+      <div class="row g-3 mb-3">
+        <div class="col-lg-8">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">流量趋势(请求 / 带宽)</div>
+            <div class="analytics-chart"><canvas id="an-chart-traffic"></canvas></div>
+          </div>
+        </div>
+        <div class="col-lg-4">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">状态码分布</div>
+            <div class="analytics-chart analytics-chart-sm"><canvas id="an-chart-status"></canvas></div>
+          </div>
+        </div>
+      </div>
+      <div class="row g-3 mb-3">
+        <div class="col-md-6">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">收发流量趋势</div>
+            <div class="analytics-chart"><canvas id="an-chart-bytes"></canvas></div>
+          </div>
+        </div>
+        <div class="col-md-6">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">请求方法</div>
+            <div class="analytics-chart analytics-chart-sm"><canvas id="an-chart-method"></canvas></div>
+          </div>
+        </div>
+      </div>
+      <div class="analytics-panel mb-3">
+        <div class="analytics-panel-title">CC / 异常 IP 分析
+          <span class="small text-secondary ms-2">嫌疑 ${esc(cc.suspect_count ?? 0)} · 峰值桶请求 ${esc(cc.peak_bucket_requests ?? 0)}</span>
+        </div>
+        ${suspectHtml}
+      </div>
+      <div class="row g-3 mb-3">
+        <div class="col-lg-6">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">Top IP</div>
+            <div class="table-responsive"><table class="table table-sm align-middle mb-0">
+              <thead><tr><th>IP</th><th>请求</th><th>错误</th><th>接收</th><th>发送</th><th>占比</th></tr></thead>
+              <tbody>${topIpRows || `<tr><td colspan="6" class="text-secondary">暂无数据</td></tr>`}</tbody>
+            </table></div>
+          </div>
+        </div>
+        <div class="col-lg-6">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">热门 URI</div>
+            <div class="table-responsive"><table class="table table-sm align-middle mb-0">
+              <thead><tr><th>URI</th><th>次数</th><th>发送</th><th>均耗时</th></tr></thead>
+              <tbody>${topUriRows || `<tr><td colspan="4" class="text-secondary">暂无数据</td></tr>`}</tbody>
+            </table></div>
+          </div>
+        </div>
+      </div>
+      <div class="row g-3 mb-3">
+        <div class="col-lg-6">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">最慢 URI</div>
+            <div class="table-responsive"><table class="table table-sm align-middle mb-0">
+              <thead><tr><th>URI</th><th>次数</th><th>平均</th><th>最大</th></tr></thead>
+              <tbody>${slowRows || `<tr><td colspan="4" class="text-secondary">暂无数据</td></tr>`}</tbody>
+            </table></div>
+          </div>
+        </div>
+        <div class="col-lg-6">
+          <div class="analytics-panel">
+            <div class="analytics-panel-title">错误日志 · ${esc(errors.total ?? 0)}</div>
+            <div class="table-responsive"><table class="table table-sm align-middle mb-0">
+              <thead><tr><th>时间</th><th>级别</th><th>内容</th></tr></thead>
+              <tbody>${errRecent || `<tr><td colspan="3" class="text-secondary">暂无错误</td></tr>`}</tbody>
+            </table></div>
+          </div>
+        </div>
+      </div>
+      <div class="small text-secondary">数据源:${esc(data.log_db || "")}</div>
+    `;
+
+    if (typeof Chart === "undefined") return;
+    const traffic = data.traffic || [];
+    const labels = traffic.map((t) => analyticsLabel(t.time_ms));
+    const reqs = traffic.map((t) => Number(t.requests) || 0);
+    const recv = traffic.map((t) => Number(t.bytes_recv) || 0);
+    const sent = traffic.map((t) => Number(t.bytes_sent) || 0);
+
+    const mkLine = (el, datasets, yFmt) => {
+      if (!el) return null;
+      const chart = new Chart(el, {
+        type: "line",
+        data: { labels, datasets },
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          animation: false,
+          interaction: { mode: "index", intersect: false },
+          plugins: { legend: { display: datasets.length > 1, position: "top", align: "end", labels: { boxWidth: 10, font: { size: 11 } } } },
+          scales: {
+            x: { ticks: { maxTicksLimit: 8, color: "#94a3b8", font: { size: 10 } }, grid: { display: false } },
+            y: { beginAtZero: true, ticks: { color: "#94a3b8", font: { size: 10 }, callback: yFmt }, grid: { color: "rgba(148,163,184,.15)" } },
+          },
+          elements: { line: { tension: 0.3, borderWidth: 2 }, point: { radius: 0 } },
+        },
+      });
+      state.analyticsCharts.push(chart);
+      return chart;
+    };
+
+    mkLine(
+      $("#an-chart-traffic"),
+      [{ label: "请求数", data: reqs, borderColor: "#0d9488", backgroundColor: "rgba(13,148,136,.12)", fill: true }],
+      (v) => v
+    );
+    mkLine(
+      $("#an-chart-bytes"),
+      [
+        { label: "接收", data: recv, borderColor: "#0d9488", backgroundColor: "rgba(13,148,136,.10)", fill: true },
+        { label: "发送", data: sent, borderColor: "#ea580c", backgroundColor: "rgba(234,88,12,.10)", fill: true },
+      ],
+      (v) => formatBytes(v)
+    );
+
+    const status = data.status || [];
+    const stEl = $("#an-chart-status");
+    if (stEl && status.length) {
+      const chart = new Chart(stEl, {
+        type: "doughnut",
+        data: {
+          labels: status.map((x) => String(x.status)),
+          datasets: [{
+            data: status.map((x) => Number(x.count) || 0),
+            backgroundColor: ["#0d9488", "#0369a1", "#ca8a04", "#ea580c", "#dc2626", "#7c3aed", "#64748b"],
+          }],
+        },
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          plugins: { legend: { position: "bottom", labels: { boxWidth: 10, font: { size: 11 } } } },
+        },
+      });
+      state.analyticsCharts.push(chart);
+    }
+
+    const methods = data.methods || [];
+    const mEl = $("#an-chart-method");
+    if (mEl && methods.length) {
+      const chart = new Chart(mEl, {
+        type: "bar",
+        data: {
+          labels: methods.map((x) => x.method || ""),
+          datasets: [{
+            data: methods.map((x) => Number(x.count) || 0),
+            backgroundColor: "#0369a1",
+            borderRadius: 6,
+          }],
+        },
+        options: {
+          responsive: true,
+          maintainAspectRatio: false,
+          plugins: { legend: { display: false } },
+          scales: {
+            x: { ticks: { color: "#94a3b8", font: { size: 10 } }, grid: { display: false } },
+            y: { beginAtZero: true, ticks: { color: "#94a3b8", font: { size: 10 } }, grid: { color: "rgba(148,163,184,.15)" } },
+          },
+        },
+      });
+      state.analyticsCharts.push(chart);
+    }
+  }
+
+  async function loadSiteAnalytics() {
+    const name = state.analyticsSite;
+    if (!name) return;
+    const range = $("#analytics-range")?.value || "24h";
+    $("#analytics-body").innerHTML = `<div class="text-secondary small py-4 text-center">分析中…</div>`;
+    const data = await api(
+      `/api/websites/analytics?name=${encodeURIComponent(name)}&range=${encodeURIComponent(range)}`
+    );
+    renderAnalytics(data || {});
+  }
+
+  async function openSiteAnalytics(name) {
+    state.analyticsSite = name;
+    $("#analytics-site-name").textContent = name;
+    if (!state.analyticsModal) {
+      state.analyticsModal = new bootstrap.Modal($("#site-analytics-modal"));
+    }
+    state.analyticsModal.show();
+    await loadSiteAnalytics();
+  }
+
+  function bindSiteSettingsModal() {
+    state.siteSettingsModal = new bootstrap.Modal($("#site-settings-modal"));
+    $("#site-settings-close").addEventListener("click", () => state.siteSettingsModal.hide());
+    $$(".site-settings-tab").forEach((btn) => {
+      btn.addEventListener("click", () => setSiteSettingsTab(btn.dataset.siteTab));
+    });
+    $("#site-set-domain-save").addEventListener("click", (e) => {
+      withBusy(async () => saveSiteDomain(), e.currentTarget);
+    });
+    $("#site-set-cert-save").addEventListener("click", (e) => {
+      withBusy(async () => saveSiteCert(), e.currentTarget);
+    });
+    $("#site-set-ssl-apply").addEventListener("click", (e) => {
+      withBusy(async () => applyLetsEncrypt(), e.currentTarget);
+    });
+    $("#site-ssl-domains-all").addEventListener("click", () => {
+      $$("#site-ssl-domain-list input[type=checkbox]").forEach((el) => { el.checked = true; });
+    });
+    $("#site-ssl-domains-none").addEventListener("click", () => {
+      $$("#site-ssl-domain-list input[type=checkbox]").forEach((el) => { el.checked = false; });
+    });
+    $("#site-set-domain").addEventListener("change", () => renderSslDomainChecklist(selectedSslHosts()));
+    $("#site-set-nginx-save").addEventListener("click", (e) => {
+      withBusy(async () => saveSiteNginxConf(), e.currentTarget);
+    });
+    $("#site-set-nginx-reload").addEventListener("click", (e) => {
+      withBusy(async () => {
+        await loadSiteNginxConf();
+        toast("已重新加载");
+      }, e.currentTarget);
+    });
+    $("#site-log-tab-access").addEventListener("click", () => {
+      state.siteLogKind = "access";
+      $("#site-log-tab-access").classList.add("active");
+      $("#site-log-tab-error").classList.remove("active");
+      loadSiteLog();
+    });
+    $("#site-log-tab-error").addEventListener("click", () => {
+      state.siteLogKind = "error";
+      $("#site-log-tab-error").classList.add("active");
+      $("#site-log-tab-access").classList.remove("active");
+      loadSiteLog();
+    });
+    $("#site-log-refresh").addEventListener("click", () => loadSiteLog());
+    $("#site-log-open-dir").addEventListener("click", () => {
+      const site = state.siteSettings;
+      if (!site || !site.log_dir) return;
+      state.siteSettingsModal.hide();
+      state.filesPath = site.log_dir;
+      setView("files");
+    });
+    $("#site-set-ssl-cert-upload").addEventListener("click", () => {
+      state.siteSslUploadTarget = "cert";
+      $("#site-ssl-file-input").click();
+    });
+    $("#site-set-ssl-key-upload").addEventListener("click", () => {
+      state.siteSslUploadTarget = "key";
+      $("#site-ssl-file-input").click();
+    });
+    $("#site-ssl-file-input").addEventListener("change", async (e) => {
+      const file = e.target.files && e.target.files[0];
+      const kind = state.siteSslUploadTarget;
+      e.target.value = "";
+      if (!file || !kind) return;
+      await withBusy(async () => uploadSiteSslFile(kind, file));
+    });
+  }
+
+  function bindEvents() {
+    state.bsModal = new bootstrap.Modal($("#modal"));
+    bindUploadModal();
+    bindSiteSettingsModal();
+
+    $("#analytics-close")?.addEventListener("click", () => state.analyticsModal?.hide());
+    $("#analytics-range")?.addEventListener("change", () => {
+      withBusy(async () => loadSiteAnalytics());
+    });
+    $("#site-analytics-modal")?.addEventListener("hidden.bs.modal", () => destroyAnalyticsCharts());
+
+    $("#btn-logout")?.addEventListener("click", () => {
+      withBusy(async () => {
+        await api("/api/auth/logout", { method: "POST", body: {} });
+        location.replace("/login.html");
+      });
+    });
+
+    $$(".nav-item").forEach((btn) => {
+      btn.addEventListener("click", () => setView(btn.dataset.view));
+    });
+
+    $("#task-badge").addEventListener("click", () => openTaskPanel());
+    $("#task-panel-close").addEventListener("click", closeTaskPanel);
+    $("#task-panel-backdrop").addEventListener("click", closeTaskPanel);
+    $("#task-tab-active").addEventListener("click", () => setTaskTab("active"));
+    $("#task-tab-history").addEventListener("click", () => setTaskTab("history"));
+    $("#task-clear-finished").addEventListener("click", () => {
+      withBusy(async () => {
+        await api("/api/tasks/clear", { method: "POST", body: {} });
+        selectTask("", true);
+        await refreshTasks();
+        toast("已清理历史任务");
+      });
+    });
+    $("#task-list").addEventListener("click", (e) => {
+      const btn = e.target.closest("[data-task-id]");
+      if (!btn) return;
+      selectTask(btn.dataset.taskId, true);
+      renderTaskList();
+      pollTaskLog(true);
+    });
+
+    $("#software-grid").addEventListener("click", (e) => {
+      const btn = e.target.closest("button[data-act]");
+      if (!btn) return;
+      softAction(btn.dataset.act, btn);
+    });
+
+    $("#sites-table").addEventListener("click", (e) => {
+      const pathBtn = e.target.closest("[data-site-path]");
+      if (pathBtn) {
+        const path = pathBtn.dataset.sitePath;
+        if (!path) return;
+        state.filesPath = path;
+        setView("files");
+        return;
+      }
+      const btn = e.target.closest("button[data-act]");
+      if (!btn) return;
+      const name = btn.dataset.site;
+      const act = btn.dataset.act;
+      if (act === "site-config") {
+        withBusy(async () => openSiteSettings(name), btn);
+        return;
+      }
+      if (act === "site-analyze") {
+        withBusy(async () => openSiteAnalytics(name), btn);
+        return;
+      }
+      if (act === "site-delete") {
+        openModal({
+          title: `删除网站 · ${name}`,
+          bodyHtml: `
+            <p class="small text-secondary">将删除配置记录与 Nginx 虚拟主机。</p>
+            <div class="form-check">
+              <input class="form-check-input" type="checkbox" name="delete_files" value="1" id="delFiles">
+              <label class="form-check-label" for="delFiles">同时删除网站目录(不可恢复)</label>
+            </div>`,
+          onSubmit: async (fd) => {
+            await api("/api/websites/delete", {
+              method: "POST",
+              body: { name, delete_files: fd.get("delete_files") === "1" },
+            });
+            toast("已删除");
+          },
+        });
+        return;
+      }
+      withBusy(async () => {
+        if (act === "site-start") await api("/api/websites/start", { method: "POST", body: { name } });
+        else if (act === "site-stop") await api("/api/websites/stop", { method: "POST", body: { name } });
+        else if (act === "site-restart") await api("/api/websites/restart", { method: "POST", body: { name } });
+        toast("已完成");
+        await refresh();
+      }, btn);
+    });
+
+    $("#db-table").addEventListener("click", (e) => {
+      const btn = e.target.closest("button[data-act]");
+      if (!btn) return;
+      const name = btn.dataset.db;
+      if (btn.dataset.act === "db-drop") {
+        withBusy(async () => {
+          if (!confirm(`确认删除数据库「${name}」?`)) return;
+          await api("/api/mysql/databases/drop", { method: "POST", body: { name } });
+          toast("已删除");
+          await refresh();
+        }, btn);
+        return;
+      }
+      if (btn.dataset.act === "db-access") {
+        openModal({
+          title: `权限 · ${name}`,
+          bodyHtml: `
+            <div class="mb-2">
+              <label class="form-label">模式</label>
+              <select class="form-select form-select-sm" name="mode">
+                <option value="local">仅本地</option>
+                <option value="fixed_ip">固定 IP</option>
+                <option value="any">外网 (%)</option>
+              </select>
+            </div>
+            <div class="mb-2">
+              <label class="form-label">Host(fixed_ip 时必填)</label>
+              <input class="form-control form-control-sm" name="host" placeholder="1.2.3.4" />
+            </div>`,
+          onSubmit: async (fd) => {
+            await api("/api/mysql/databases/access", {
+              method: "POST",
+              body: { name, mode: fd.get("mode"), host: fd.get("host") || "" },
+            });
+            toast("权限已更新");
+          },
+        });
+      }
+    });
+
+    $("#btn-site-create").addEventListener("click", () => {
+      openModal({
+        title: "新建网站",
+        bodyHtml: `
+          <div class="mb-2">
+            <label class="form-label">名称</label>
+            <input class="form-control form-control-sm" name="name" required placeholder="demo" />
+          </div>
+          <div class="mb-2">
+            <label class="form-label">类型</label>
+            <select class="form-select form-select-sm" name="type" id="site-type">
+              <option value="static">静态</option>
+              <option value="proxy">反向代理</option>
+              <option value="fastweb">Fastweb</option>
+            </select>
+            <div class="form-text field-upstream-tip">静态站仅需名称/域名/端口</div>
+          </div>
+          <div class="mb-2">
+            <label class="form-label">域名(可空,支持多个,每行一个)</label>
+            <textarea class="form-control form-control-sm" name="domain" rows="3" placeholder="默认等于名称&#10;www.example.com:8080"></textarea>
+            <div class="form-text">可写域名或 域名:端口,不写端口默认 80</div>
+          </div>
+          <div class="mb-2 field-upstream d-none">
+            <label class="form-label">上游(proxy 必填)</label>
+            <input class="form-control form-control-sm" name="upstream" placeholder="127.0.0.1:9000" />
+          </div>`,
+        onSubmit: async (fd) => {
+          const type = fd.get("type");
+          const domains = domainsFromText(String(fd.get("domain") || ""));
+          const body = {
+            name: String(fd.get("name") || "").trim(),
+            type,
+            domains,
+            domain: domains.join(" "),
+            upstream: type === "proxy" ? String(fd.get("upstream") || "").trim() : "",
+          };
+          if (type === "proxy" && !body.upstream) throw new Error("反向代理需填写上游");
+          await api("/api/websites", { method: "POST", body });
+          toast("网站已创建");
+        },
+      });
+      const body = $("#modal-body");
+      syncSiteFormFields(body);
+      body.querySelector('[name="type"]').addEventListener("change", () => syncSiteFormFields(body));
+    });
+
+    $("#btn-db-create").addEventListener("click", () => {
+      openModal({
+        title: "新建数据库",
+        bodyHtml: `
+          <div class="mb-2"><label class="form-label">库名</label><input class="form-control form-control-sm" name="name" required /></div>
+          <div class="mb-2"><label class="form-label">用户</label><input class="form-control form-control-sm" name="user" required /></div>
+          <div class="mb-2"><label class="form-label">密码</label><input class="form-control form-control-sm" name="password" required /></div>`,
+        onSubmit: async (fd) => {
+          await api("/api/mysql/databases", {
+            method: "POST",
+            body: {
+              name: String(fd.get("name") || "").trim(),
+              user: String(fd.get("user") || "").trim(),
+              password: String(fd.get("password") || ""),
+            },
+          });
+          toast("数据库已创建");
+        },
+      });
+    });
+
+    $("#btn-mysql-password").addEventListener("click", () => {
+      openModal({
+        title: "修改 root 密码",
+        bodyHtml: `<div class="mb-2"><label class="form-label">新密码</label><input class="form-control form-control-sm" name="password" type="password" required /></div>`,
+        onSubmit: async (fd) => {
+          await api("/api/mysql/root/password", {
+            method: "POST",
+            body: { password: String(fd.get("password") || "") },
+          });
+          toast("root 密码已更新");
+        },
+      });
+    });
+
+    $("#files-crumbs").addEventListener("click", (e) => {
+      const btn = e.target.closest("button[data-path]");
+      if (!btn) return;
+      loadFiles(btn.dataset.path);
+    });
+    $("#btn-files-up").addEventListener("click", () => loadFiles(parentPath(state.filesPath)));
+    $("#btn-files-mkdir").addEventListener("click", () => {
+      openModal({
+        title: "新建目录",
+        bodyHtml: `<div class="mb-2"><label class="form-label">目录名</label><input class="form-control form-control-sm" name="name" required /></div>`,
+        onSubmit: async (fd) => {
+          const name = String(fd.get("name") || "").trim();
+          if (!name) throw new Error("目录名不能为空");
+          await api("/api/files/mkdir", { method: "POST", body: { path: joinPath(state.filesPath, name) } });
+          toast("目录已创建");
+          await loadFiles(state.filesPath);
+        },
+      });
+    });
+    $("#btn-files-newfile").addEventListener("click", () => {
+      openModal({
+        title: "新建文件",
+        bodyHtml: `<div class="mb-2"><label class="form-label">文件名</label><input class="form-control form-control-sm" name="name" required placeholder="demo.txt" /></div>`,
+        onSubmit: async (fd) => {
+          const name = String(fd.get("name") || "").trim();
+          if (!name) throw new Error("文件名不能为空");
+          await openCodeEditor({ path: joinPath(state.filesPath, name), content: "", readonly: false });
+        },
+      });
+    });
+
+    $("#files-table").addEventListener("click", (e) => {
+      const open = e.target.closest("[data-open]");
+      if (open) {
+        if (open.dataset.dir === "1") loadFiles(open.dataset.open);
+        return;
+      }
+      const btn = e.target.closest("button[data-act]");
+      if (!btn) return;
+      const path = btn.dataset.path;
+      const act = btn.dataset.act;
+      if (act === "file-rename") {
+        openModal({
+          title: "重命名",
+          bodyHtml: `<div class="mb-2"><label class="form-label">新名称</label><input class="form-control form-control-sm" name="name" value="${escapeHtml(btn.dataset.name || "")}" required /></div>`,
+          onSubmit: async (fd) => {
+            const name = String(fd.get("name") || "").trim();
+            if (!name) throw new Error("名称不能为空");
+            await api("/api/files/rename", { method: "POST", body: { from: path, to: joinPath(parentPath(path), name) } });
+            toast("已重命名");
+            await loadFiles(state.filesPath);
+          },
+        });
+        return;
+      }
+      if (act === "file-delete") {
+        const isDir = btn.dataset.dir === "1";
+        openModal({
+          title: "删除确认",
+          bodyHtml: `
+            <p class="small text-secondary mb-2">将删除:${escapeHtml(path)}</p>
+            ${isDir ? `<div class="form-check"><input class="form-check-input" type="checkbox" name="recursive" value="1" id="rec"><label class="form-check-label" for="rec">递归删除目录内容</label></div>` : ""}`,
+          onSubmit: async (fd) => {
+            await api("/api/files/delete", {
+              method: "POST",
+              body: { path, recursive: fd.get("recursive") === "1" },
+            });
+            toast("已删除");
+            await loadFiles(state.filesPath);
+          },
+        });
+        return;
+      }
+      if (act === "file-edit") {
+        withBusy(async () => {
+          await openFileInEditor(path, Number(btn.dataset.size) || 0, false);
+        }, btn);
+      }
+    });
+
+    $("#files-table").addEventListener("dblclick", (e) => {
+      const open = e.target.closest("[data-open]");
+      if (!open || open.dataset.dir === "1") return;
+      e.preventDefault();
+      withBusy(async () => {
+        if (open.dataset.tooBig === "1") throw new Error("文件超过 10MB,不支持在线编辑/预览");
+        await openFileInEditor(open.dataset.open, Number(open.dataset.size) || 0, false);
+      });
+    });
+
+    $("#code-editor-cancel").addEventListener("click", () => closeCodeEditor());
+    $("#code-editor-backdrop").addEventListener("click", () => closeCodeEditor());
+    $("#editor-tree-up").addEventListener("click", () => {
+      withBusy(async () => {
+        await loadEditorTree(parentPath(state.editorTreePath));
+      });
+    });
+    $("#editor-tree-refresh").addEventListener("click", () => {
+      withBusy(async () => {
+        await loadEditorTree(state.editorTreePath || "/");
+      });
+    });
+    $("#editor-tree").addEventListener("click", (e) => {
+      const item = e.target.closest("[data-tree-path]");
+      if (!item) return;
+      const path = item.dataset.treePath;
+      if (item.dataset.treeDir === "1") {
+        withBusy(async () => loadEditorTree(path));
+        return;
+      }
+      withBusy(async () => {
+        await openFileInEditor(path, Number(item.dataset.treeSize) || 0, false);
+      });
+    });
+    $("#editor-tabs").addEventListener("click", (e) => {
+      const closeBtn = e.target.closest("[data-close-tab]");
+      if (closeBtn) {
+        e.stopPropagation();
+        withBusy(async () => closeEditorTab(closeBtn.dataset.closeTab));
+        return;
+      }
+      const tab = e.target.closest("[data-tab-path]");
+      if (!tab) return;
+      withBusy(async () => switchEditorTab(tab.dataset.tabPath));
+    });
+
+    document.addEventListener("keydown", (e) => {
+      if ($("#code-editor").hidden) return;
+      if (e.key === "Escape") {
+        // don't close whole IDE on Esc; only if no tabs? skip for safety
+        return;
+      }
+      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
+        e.preventDefault();
+        e.stopPropagation();
+        withBusy(async () => {
+          await saveActiveTab();
+        });
+      }
+      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "w") {
+        const tab = activeTab();
+        if (!tab) return;
+        e.preventDefault();
+        withBusy(async () => closeEditorTab(tab.path));
+      }
+    }, true);
+  }
+
+  async function boot() {
+    try {
+      await api("/api/auth/me");
+    } catch (_) {
+      location.replace("/login.html");
+      return;
+    }
+    bindEvents();
+    let hash = (location.hash || "#console").slice(1);
+    if (hash === "overview" || hash === "monitor") hash = "console";
+    setView(titles[hash] ? hash : "console");
+    refresh();
+    refreshTasks();
+    setInterval(refresh, 15000);
+    setInterval(refreshTasks, 2000);
+  }
+
+  boot();
+})();

+ 2078 - 0
data/www/assets/bootstrap-icons/bootstrap-icons.css

@@ -0,0 +1,2078 @@
+/*!
+ * Bootstrap Icons v1.11.3 (https://icons.getbootstrap.com/)
+ * Copyright 2019-2024 The Bootstrap Authors
+ * Licensed under MIT (https://github.com/twbs/icons/blob/main/LICENSE)
+ */
+
+@font-face {
+  font-display: block;
+  font-family: "bootstrap-icons";
+  src: url("/assets/bootstrap-icons/fonts/bootstrap-icons.woff2?dd67030699838ea613ee6dbda90effa6") format("woff2"),
+url("/assets/bootstrap-icons/fonts/bootstrap-icons.woff?dd67030699838ea613ee6dbda90effa6") format("woff");
+}
+
+.bi::before,
+[class^="bi-"]::before,
+[class*=" bi-"]::before {
+  display: inline-block;
+  font-family: bootstrap-icons !important;
+  font-style: normal;
+  font-weight: normal !important;
+  font-variant: normal;
+  text-transform: none;
+  line-height: 1;
+  vertical-align: -.125em;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.bi-123::before { content: "\f67f"; }
+.bi-alarm-fill::before { content: "\f101"; }
+.bi-alarm::before { content: "\f102"; }
+.bi-align-bottom::before { content: "\f103"; }
+.bi-align-center::before { content: "\f104"; }
+.bi-align-end::before { content: "\f105"; }
+.bi-align-middle::before { content: "\f106"; }
+.bi-align-start::before { content: "\f107"; }
+.bi-align-top::before { content: "\f108"; }
+.bi-alt::before { content: "\f109"; }
+.bi-app-indicator::before { content: "\f10a"; }
+.bi-app::before { content: "\f10b"; }
+.bi-archive-fill::before { content: "\f10c"; }
+.bi-archive::before { content: "\f10d"; }
+.bi-arrow-90deg-down::before { content: "\f10e"; }
+.bi-arrow-90deg-left::before { content: "\f10f"; }
+.bi-arrow-90deg-right::before { content: "\f110"; }
+.bi-arrow-90deg-up::before { content: "\f111"; }
+.bi-arrow-bar-down::before { content: "\f112"; }
+.bi-arrow-bar-left::before { content: "\f113"; }
+.bi-arrow-bar-right::before { content: "\f114"; }
+.bi-arrow-bar-up::before { content: "\f115"; }
+.bi-arrow-clockwise::before { content: "\f116"; }
+.bi-arrow-counterclockwise::before { content: "\f117"; }
+.bi-arrow-down-circle-fill::before { content: "\f118"; }
+.bi-arrow-down-circle::before { content: "\f119"; }
+.bi-arrow-down-left-circle-fill::before { content: "\f11a"; }
+.bi-arrow-down-left-circle::before { content: "\f11b"; }
+.bi-arrow-down-left-square-fill::before { content: "\f11c"; }
+.bi-arrow-down-left-square::before { content: "\f11d"; }
+.bi-arrow-down-left::before { content: "\f11e"; }
+.bi-arrow-down-right-circle-fill::before { content: "\f11f"; }
+.bi-arrow-down-right-circle::before { content: "\f120"; }
+.bi-arrow-down-right-square-fill::before { content: "\f121"; }
+.bi-arrow-down-right-square::before { content: "\f122"; }
+.bi-arrow-down-right::before { content: "\f123"; }
+.bi-arrow-down-short::before { content: "\f124"; }
+.bi-arrow-down-square-fill::before { content: "\f125"; }
+.bi-arrow-down-square::before { content: "\f126"; }
+.bi-arrow-down-up::before { content: "\f127"; }
+.bi-arrow-down::before { content: "\f128"; }
+.bi-arrow-left-circle-fill::before { content: "\f129"; }
+.bi-arrow-left-circle::before { content: "\f12a"; }
+.bi-arrow-left-right::before { content: "\f12b"; }
+.bi-arrow-left-short::before { content: "\f12c"; }
+.bi-arrow-left-square-fill::before { content: "\f12d"; }
+.bi-arrow-left-square::before { content: "\f12e"; }
+.bi-arrow-left::before { content: "\f12f"; }
+.bi-arrow-repeat::before { content: "\f130"; }
+.bi-arrow-return-left::before { content: "\f131"; }
+.bi-arrow-return-right::before { content: "\f132"; }
+.bi-arrow-right-circle-fill::before { content: "\f133"; }
+.bi-arrow-right-circle::before { content: "\f134"; }
+.bi-arrow-right-short::before { content: "\f135"; }
+.bi-arrow-right-square-fill::before { content: "\f136"; }
+.bi-arrow-right-square::before { content: "\f137"; }
+.bi-arrow-right::before { content: "\f138"; }
+.bi-arrow-up-circle-fill::before { content: "\f139"; }
+.bi-arrow-up-circle::before { content: "\f13a"; }
+.bi-arrow-up-left-circle-fill::before { content: "\f13b"; }
+.bi-arrow-up-left-circle::before { content: "\f13c"; }
+.bi-arrow-up-left-square-fill::before { content: "\f13d"; }
+.bi-arrow-up-left-square::before { content: "\f13e"; }
+.bi-arrow-up-left::before { content: "\f13f"; }
+.bi-arrow-up-right-circle-fill::before { content: "\f140"; }
+.bi-arrow-up-right-circle::before { content: "\f141"; }
+.bi-arrow-up-right-square-fill::before { content: "\f142"; }
+.bi-arrow-up-right-square::before { content: "\f143"; }
+.bi-arrow-up-right::before { content: "\f144"; }
+.bi-arrow-up-short::before { content: "\f145"; }
+.bi-arrow-up-square-fill::before { content: "\f146"; }
+.bi-arrow-up-square::before { content: "\f147"; }
+.bi-arrow-up::before { content: "\f148"; }
+.bi-arrows-angle-contract::before { content: "\f149"; }
+.bi-arrows-angle-expand::before { content: "\f14a"; }
+.bi-arrows-collapse::before { content: "\f14b"; }
+.bi-arrows-expand::before { content: "\f14c"; }
+.bi-arrows-fullscreen::before { content: "\f14d"; }
+.bi-arrows-move::before { content: "\f14e"; }
+.bi-aspect-ratio-fill::before { content: "\f14f"; }
+.bi-aspect-ratio::before { content: "\f150"; }
+.bi-asterisk::before { content: "\f151"; }
+.bi-at::before { content: "\f152"; }
+.bi-award-fill::before { content: "\f153"; }
+.bi-award::before { content: "\f154"; }
+.bi-back::before { content: "\f155"; }
+.bi-backspace-fill::before { content: "\f156"; }
+.bi-backspace-reverse-fill::before { content: "\f157"; }
+.bi-backspace-reverse::before { content: "\f158"; }
+.bi-backspace::before { content: "\f159"; }
+.bi-badge-3d-fill::before { content: "\f15a"; }
+.bi-badge-3d::before { content: "\f15b"; }
+.bi-badge-4k-fill::before { content: "\f15c"; }
+.bi-badge-4k::before { content: "\f15d"; }
+.bi-badge-8k-fill::before { content: "\f15e"; }
+.bi-badge-8k::before { content: "\f15f"; }
+.bi-badge-ad-fill::before { content: "\f160"; }
+.bi-badge-ad::before { content: "\f161"; }
+.bi-badge-ar-fill::before { content: "\f162"; }
+.bi-badge-ar::before { content: "\f163"; }
+.bi-badge-cc-fill::before { content: "\f164"; }
+.bi-badge-cc::before { content: "\f165"; }
+.bi-badge-hd-fill::before { content: "\f166"; }
+.bi-badge-hd::before { content: "\f167"; }
+.bi-badge-tm-fill::before { content: "\f168"; }
+.bi-badge-tm::before { content: "\f169"; }
+.bi-badge-vo-fill::before { content: "\f16a"; }
+.bi-badge-vo::before { content: "\f16b"; }
+.bi-badge-vr-fill::before { content: "\f16c"; }
+.bi-badge-vr::before { content: "\f16d"; }
+.bi-badge-wc-fill::before { content: "\f16e"; }
+.bi-badge-wc::before { content: "\f16f"; }
+.bi-bag-check-fill::before { content: "\f170"; }
+.bi-bag-check::before { content: "\f171"; }
+.bi-bag-dash-fill::before { content: "\f172"; }
+.bi-bag-dash::before { content: "\f173"; }
+.bi-bag-fill::before { content: "\f174"; }
+.bi-bag-plus-fill::before { content: "\f175"; }
+.bi-bag-plus::before { content: "\f176"; }
+.bi-bag-x-fill::before { content: "\f177"; }
+.bi-bag-x::before { content: "\f178"; }
+.bi-bag::before { content: "\f179"; }
+.bi-bar-chart-fill::before { content: "\f17a"; }
+.bi-bar-chart-line-fill::before { content: "\f17b"; }
+.bi-bar-chart-line::before { content: "\f17c"; }
+.bi-bar-chart-steps::before { content: "\f17d"; }
+.bi-bar-chart::before { content: "\f17e"; }
+.bi-basket-fill::before { content: "\f17f"; }
+.bi-basket::before { content: "\f180"; }
+.bi-basket2-fill::before { content: "\f181"; }
+.bi-basket2::before { content: "\f182"; }
+.bi-basket3-fill::before { content: "\f183"; }
+.bi-basket3::before { content: "\f184"; }
+.bi-battery-charging::before { content: "\f185"; }
+.bi-battery-full::before { content: "\f186"; }
+.bi-battery-half::before { content: "\f187"; }
+.bi-battery::before { content: "\f188"; }
+.bi-bell-fill::before { content: "\f189"; }
+.bi-bell::before { content: "\f18a"; }
+.bi-bezier::before { content: "\f18b"; }
+.bi-bezier2::before { content: "\f18c"; }
+.bi-bicycle::before { content: "\f18d"; }
+.bi-binoculars-fill::before { content: "\f18e"; }
+.bi-binoculars::before { content: "\f18f"; }
+.bi-blockquote-left::before { content: "\f190"; }
+.bi-blockquote-right::before { content: "\f191"; }
+.bi-book-fill::before { content: "\f192"; }
+.bi-book-half::before { content: "\f193"; }
+.bi-book::before { content: "\f194"; }
+.bi-bookmark-check-fill::before { content: "\f195"; }
+.bi-bookmark-check::before { content: "\f196"; }
+.bi-bookmark-dash-fill::before { content: "\f197"; }
+.bi-bookmark-dash::before { content: "\f198"; }
+.bi-bookmark-fill::before { content: "\f199"; }
+.bi-bookmark-heart-fill::before { content: "\f19a"; }
+.bi-bookmark-heart::before { content: "\f19b"; }
+.bi-bookmark-plus-fill::before { content: "\f19c"; }
+.bi-bookmark-plus::before { content: "\f19d"; }
+.bi-bookmark-star-fill::before { content: "\f19e"; }
+.bi-bookmark-star::before { content: "\f19f"; }
+.bi-bookmark-x-fill::before { content: "\f1a0"; }
+.bi-bookmark-x::before { content: "\f1a1"; }
+.bi-bookmark::before { content: "\f1a2"; }
+.bi-bookmarks-fill::before { content: "\f1a3"; }
+.bi-bookmarks::before { content: "\f1a4"; }
+.bi-bookshelf::before { content: "\f1a5"; }
+.bi-bootstrap-fill::before { content: "\f1a6"; }
+.bi-bootstrap-reboot::before { content: "\f1a7"; }
+.bi-bootstrap::before { content: "\f1a8"; }
+.bi-border-all::before { content: "\f1a9"; }
+.bi-border-bottom::before { content: "\f1aa"; }
+.bi-border-center::before { content: "\f1ab"; }
+.bi-border-inner::before { content: "\f1ac"; }
+.bi-border-left::before { content: "\f1ad"; }
+.bi-border-middle::before { content: "\f1ae"; }
+.bi-border-outer::before { content: "\f1af"; }
+.bi-border-right::before { content: "\f1b0"; }
+.bi-border-style::before { content: "\f1b1"; }
+.bi-border-top::before { content: "\f1b2"; }
+.bi-border-width::before { content: "\f1b3"; }
+.bi-border::before { content: "\f1b4"; }
+.bi-bounding-box-circles::before { content: "\f1b5"; }
+.bi-bounding-box::before { content: "\f1b6"; }
+.bi-box-arrow-down-left::before { content: "\f1b7"; }
+.bi-box-arrow-down-right::before { content: "\f1b8"; }
+.bi-box-arrow-down::before { content: "\f1b9"; }
+.bi-box-arrow-in-down-left::before { content: "\f1ba"; }
+.bi-box-arrow-in-down-right::before { content: "\f1bb"; }
+.bi-box-arrow-in-down::before { content: "\f1bc"; }
+.bi-box-arrow-in-left::before { content: "\f1bd"; }
+.bi-box-arrow-in-right::before { content: "\f1be"; }
+.bi-box-arrow-in-up-left::before { content: "\f1bf"; }
+.bi-box-arrow-in-up-right::before { content: "\f1c0"; }
+.bi-box-arrow-in-up::before { content: "\f1c1"; }
+.bi-box-arrow-left::before { content: "\f1c2"; }
+.bi-box-arrow-right::before { content: "\f1c3"; }
+.bi-box-arrow-up-left::before { content: "\f1c4"; }
+.bi-box-arrow-up-right::before { content: "\f1c5"; }
+.bi-box-arrow-up::before { content: "\f1c6"; }
+.bi-box-seam::before { content: "\f1c7"; }
+.bi-box::before { content: "\f1c8"; }
+.bi-braces::before { content: "\f1c9"; }
+.bi-bricks::before { content: "\f1ca"; }
+.bi-briefcase-fill::before { content: "\f1cb"; }
+.bi-briefcase::before { content: "\f1cc"; }
+.bi-brightness-alt-high-fill::before { content: "\f1cd"; }
+.bi-brightness-alt-high::before { content: "\f1ce"; }
+.bi-brightness-alt-low-fill::before { content: "\f1cf"; }
+.bi-brightness-alt-low::before { content: "\f1d0"; }
+.bi-brightness-high-fill::before { content: "\f1d1"; }
+.bi-brightness-high::before { content: "\f1d2"; }
+.bi-brightness-low-fill::before { content: "\f1d3"; }
+.bi-brightness-low::before { content: "\f1d4"; }
+.bi-broadcast-pin::before { content: "\f1d5"; }
+.bi-broadcast::before { content: "\f1d6"; }
+.bi-brush-fill::before { content: "\f1d7"; }
+.bi-brush::before { content: "\f1d8"; }
+.bi-bucket-fill::before { content: "\f1d9"; }
+.bi-bucket::before { content: "\f1da"; }
+.bi-bug-fill::before { content: "\f1db"; }
+.bi-bug::before { content: "\f1dc"; }
+.bi-building::before { content: "\f1dd"; }
+.bi-bullseye::before { content: "\f1de"; }
+.bi-calculator-fill::before { content: "\f1df"; }
+.bi-calculator::before { content: "\f1e0"; }
+.bi-calendar-check-fill::before { content: "\f1e1"; }
+.bi-calendar-check::before { content: "\f1e2"; }
+.bi-calendar-date-fill::before { content: "\f1e3"; }
+.bi-calendar-date::before { content: "\f1e4"; }
+.bi-calendar-day-fill::before { content: "\f1e5"; }
+.bi-calendar-day::before { content: "\f1e6"; }
+.bi-calendar-event-fill::before { content: "\f1e7"; }
+.bi-calendar-event::before { content: "\f1e8"; }
+.bi-calendar-fill::before { content: "\f1e9"; }
+.bi-calendar-minus-fill::before { content: "\f1ea"; }
+.bi-calendar-minus::before { content: "\f1eb"; }
+.bi-calendar-month-fill::before { content: "\f1ec"; }
+.bi-calendar-month::before { content: "\f1ed"; }
+.bi-calendar-plus-fill::before { content: "\f1ee"; }
+.bi-calendar-plus::before { content: "\f1ef"; }
+.bi-calendar-range-fill::before { content: "\f1f0"; }
+.bi-calendar-range::before { content: "\f1f1"; }
+.bi-calendar-week-fill::before { content: "\f1f2"; }
+.bi-calendar-week::before { content: "\f1f3"; }
+.bi-calendar-x-fill::before { content: "\f1f4"; }
+.bi-calendar-x::before { content: "\f1f5"; }
+.bi-calendar::before { content: "\f1f6"; }
+.bi-calendar2-check-fill::before { content: "\f1f7"; }
+.bi-calendar2-check::before { content: "\f1f8"; }
+.bi-calendar2-date-fill::before { content: "\f1f9"; }
+.bi-calendar2-date::before { content: "\f1fa"; }
+.bi-calendar2-day-fill::before { content: "\f1fb"; }
+.bi-calendar2-day::before { content: "\f1fc"; }
+.bi-calendar2-event-fill::before { content: "\f1fd"; }
+.bi-calendar2-event::before { content: "\f1fe"; }
+.bi-calendar2-fill::before { content: "\f1ff"; }
+.bi-calendar2-minus-fill::before { content: "\f200"; }
+.bi-calendar2-minus::before { content: "\f201"; }
+.bi-calendar2-month-fill::before { content: "\f202"; }
+.bi-calendar2-month::before { content: "\f203"; }
+.bi-calendar2-plus-fill::before { content: "\f204"; }
+.bi-calendar2-plus::before { content: "\f205"; }
+.bi-calendar2-range-fill::before { content: "\f206"; }
+.bi-calendar2-range::before { content: "\f207"; }
+.bi-calendar2-week-fill::before { content: "\f208"; }
+.bi-calendar2-week::before { content: "\f209"; }
+.bi-calendar2-x-fill::before { content: "\f20a"; }
+.bi-calendar2-x::before { content: "\f20b"; }
+.bi-calendar2::before { content: "\f20c"; }
+.bi-calendar3-event-fill::before { content: "\f20d"; }
+.bi-calendar3-event::before { content: "\f20e"; }
+.bi-calendar3-fill::before { content: "\f20f"; }
+.bi-calendar3-range-fill::before { content: "\f210"; }
+.bi-calendar3-range::before { content: "\f211"; }
+.bi-calendar3-week-fill::before { content: "\f212"; }
+.bi-calendar3-week::before { content: "\f213"; }
+.bi-calendar3::before { content: "\f214"; }
+.bi-calendar4-event::before { content: "\f215"; }
+.bi-calendar4-range::before { content: "\f216"; }
+.bi-calendar4-week::before { content: "\f217"; }
+.bi-calendar4::before { content: "\f218"; }
+.bi-camera-fill::before { content: "\f219"; }
+.bi-camera-reels-fill::before { content: "\f21a"; }
+.bi-camera-reels::before { content: "\f21b"; }
+.bi-camera-video-fill::before { content: "\f21c"; }
+.bi-camera-video-off-fill::before { content: "\f21d"; }
+.bi-camera-video-off::before { content: "\f21e"; }
+.bi-camera-video::before { content: "\f21f"; }
+.bi-camera::before { content: "\f220"; }
+.bi-camera2::before { content: "\f221"; }
+.bi-capslock-fill::before { content: "\f222"; }
+.bi-capslock::before { content: "\f223"; }
+.bi-card-checklist::before { content: "\f224"; }
+.bi-card-heading::before { content: "\f225"; }
+.bi-card-image::before { content: "\f226"; }
+.bi-card-list::before { content: "\f227"; }
+.bi-card-text::before { content: "\f228"; }
+.bi-caret-down-fill::before { content: "\f229"; }
+.bi-caret-down-square-fill::before { content: "\f22a"; }
+.bi-caret-down-square::before { content: "\f22b"; }
+.bi-caret-down::before { content: "\f22c"; }
+.bi-caret-left-fill::before { content: "\f22d"; }
+.bi-caret-left-square-fill::before { content: "\f22e"; }
+.bi-caret-left-square::before { content: "\f22f"; }
+.bi-caret-left::before { content: "\f230"; }
+.bi-caret-right-fill::before { content: "\f231"; }
+.bi-caret-right-square-fill::before { content: "\f232"; }
+.bi-caret-right-square::before { content: "\f233"; }
+.bi-caret-right::before { content: "\f234"; }
+.bi-caret-up-fill::before { content: "\f235"; }
+.bi-caret-up-square-fill::before { content: "\f236"; }
+.bi-caret-up-square::before { content: "\f237"; }
+.bi-caret-up::before { content: "\f238"; }
+.bi-cart-check-fill::before { content: "\f239"; }
+.bi-cart-check::before { content: "\f23a"; }
+.bi-cart-dash-fill::before { content: "\f23b"; }
+.bi-cart-dash::before { content: "\f23c"; }
+.bi-cart-fill::before { content: "\f23d"; }
+.bi-cart-plus-fill::before { content: "\f23e"; }
+.bi-cart-plus::before { content: "\f23f"; }
+.bi-cart-x-fill::before { content: "\f240"; }
+.bi-cart-x::before { content: "\f241"; }
+.bi-cart::before { content: "\f242"; }
+.bi-cart2::before { content: "\f243"; }
+.bi-cart3::before { content: "\f244"; }
+.bi-cart4::before { content: "\f245"; }
+.bi-cash-stack::before { content: "\f246"; }
+.bi-cash::before { content: "\f247"; }
+.bi-cast::before { content: "\f248"; }
+.bi-chat-dots-fill::before { content: "\f249"; }
+.bi-chat-dots::before { content: "\f24a"; }
+.bi-chat-fill::before { content: "\f24b"; }
+.bi-chat-left-dots-fill::before { content: "\f24c"; }
+.bi-chat-left-dots::before { content: "\f24d"; }
+.bi-chat-left-fill::before { content: "\f24e"; }
+.bi-chat-left-quote-fill::before { content: "\f24f"; }
+.bi-chat-left-quote::before { content: "\f250"; }
+.bi-chat-left-text-fill::before { content: "\f251"; }
+.bi-chat-left-text::before { content: "\f252"; }
+.bi-chat-left::before { content: "\f253"; }
+.bi-chat-quote-fill::before { content: "\f254"; }
+.bi-chat-quote::before { content: "\f255"; }
+.bi-chat-right-dots-fill::before { content: "\f256"; }
+.bi-chat-right-dots::before { content: "\f257"; }
+.bi-chat-right-fill::before { content: "\f258"; }
+.bi-chat-right-quote-fill::before { content: "\f259"; }
+.bi-chat-right-quote::before { content: "\f25a"; }
+.bi-chat-right-text-fill::before { content: "\f25b"; }
+.bi-chat-right-text::before { content: "\f25c"; }
+.bi-chat-right::before { content: "\f25d"; }
+.bi-chat-square-dots-fill::before { content: "\f25e"; }
+.bi-chat-square-dots::before { content: "\f25f"; }
+.bi-chat-square-fill::before { content: "\f260"; }
+.bi-chat-square-quote-fill::before { content: "\f261"; }
+.bi-chat-square-quote::before { content: "\f262"; }
+.bi-chat-square-text-fill::before { content: "\f263"; }
+.bi-chat-square-text::before { content: "\f264"; }
+.bi-chat-square::before { content: "\f265"; }
+.bi-chat-text-fill::before { content: "\f266"; }
+.bi-chat-text::before { content: "\f267"; }
+.bi-chat::before { content: "\f268"; }
+.bi-check-all::before { content: "\f269"; }
+.bi-check-circle-fill::before { content: "\f26a"; }
+.bi-check-circle::before { content: "\f26b"; }
+.bi-check-square-fill::before { content: "\f26c"; }
+.bi-check-square::before { content: "\f26d"; }
+.bi-check::before { content: "\f26e"; }
+.bi-check2-all::before { content: "\f26f"; }
+.bi-check2-circle::before { content: "\f270"; }
+.bi-check2-square::before { content: "\f271"; }
+.bi-check2::before { content: "\f272"; }
+.bi-chevron-bar-contract::before { content: "\f273"; }
+.bi-chevron-bar-down::before { content: "\f274"; }
+.bi-chevron-bar-expand::before { content: "\f275"; }
+.bi-chevron-bar-left::before { content: "\f276"; }
+.bi-chevron-bar-right::before { content: "\f277"; }
+.bi-chevron-bar-up::before { content: "\f278"; }
+.bi-chevron-compact-down::before { content: "\f279"; }
+.bi-chevron-compact-left::before { content: "\f27a"; }
+.bi-chevron-compact-right::before { content: "\f27b"; }
+.bi-chevron-compact-up::before { content: "\f27c"; }
+.bi-chevron-contract::before { content: "\f27d"; }
+.bi-chevron-double-down::before { content: "\f27e"; }
+.bi-chevron-double-left::before { content: "\f27f"; }
+.bi-chevron-double-right::before { content: "\f280"; }
+.bi-chevron-double-up::before { content: "\f281"; }
+.bi-chevron-down::before { content: "\f282"; }
+.bi-chevron-expand::before { content: "\f283"; }
+.bi-chevron-left::before { content: "\f284"; }
+.bi-chevron-right::before { content: "\f285"; }
+.bi-chevron-up::before { content: "\f286"; }
+.bi-circle-fill::before { content: "\f287"; }
+.bi-circle-half::before { content: "\f288"; }
+.bi-circle-square::before { content: "\f289"; }
+.bi-circle::before { content: "\f28a"; }
+.bi-clipboard-check::before { content: "\f28b"; }
+.bi-clipboard-data::before { content: "\f28c"; }
+.bi-clipboard-minus::before { content: "\f28d"; }
+.bi-clipboard-plus::before { content: "\f28e"; }
+.bi-clipboard-x::before { content: "\f28f"; }
+.bi-clipboard::before { content: "\f290"; }
+.bi-clock-fill::before { content: "\f291"; }
+.bi-clock-history::before { content: "\f292"; }
+.bi-clock::before { content: "\f293"; }
+.bi-cloud-arrow-down-fill::before { content: "\f294"; }
+.bi-cloud-arrow-down::before { content: "\f295"; }
+.bi-cloud-arrow-up-fill::before { content: "\f296"; }
+.bi-cloud-arrow-up::before { content: "\f297"; }
+.bi-cloud-check-fill::before { content: "\f298"; }
+.bi-cloud-check::before { content: "\f299"; }
+.bi-cloud-download-fill::before { content: "\f29a"; }
+.bi-cloud-download::before { content: "\f29b"; }
+.bi-cloud-drizzle-fill::before { content: "\f29c"; }
+.bi-cloud-drizzle::before { content: "\f29d"; }
+.bi-cloud-fill::before { content: "\f29e"; }
+.bi-cloud-fog-fill::before { content: "\f29f"; }
+.bi-cloud-fog::before { content: "\f2a0"; }
+.bi-cloud-fog2-fill::before { content: "\f2a1"; }
+.bi-cloud-fog2::before { content: "\f2a2"; }
+.bi-cloud-hail-fill::before { content: "\f2a3"; }
+.bi-cloud-hail::before { content: "\f2a4"; }
+.bi-cloud-haze-fill::before { content: "\f2a6"; }
+.bi-cloud-haze::before { content: "\f2a7"; }
+.bi-cloud-haze2-fill::before { content: "\f2a8"; }
+.bi-cloud-lightning-fill::before { content: "\f2a9"; }
+.bi-cloud-lightning-rain-fill::before { content: "\f2aa"; }
+.bi-cloud-lightning-rain::before { content: "\f2ab"; }
+.bi-cloud-lightning::before { content: "\f2ac"; }
+.bi-cloud-minus-fill::before { content: "\f2ad"; }
+.bi-cloud-minus::before { content: "\f2ae"; }
+.bi-cloud-moon-fill::before { content: "\f2af"; }
+.bi-cloud-moon::before { content: "\f2b0"; }
+.bi-cloud-plus-fill::before { content: "\f2b1"; }
+.bi-cloud-plus::before { content: "\f2b2"; }
+.bi-cloud-rain-fill::before { content: "\f2b3"; }
+.bi-cloud-rain-heavy-fill::before { content: "\f2b4"; }
+.bi-cloud-rain-heavy::before { content: "\f2b5"; }
+.bi-cloud-rain::before { content: "\f2b6"; }
+.bi-cloud-slash-fill::before { content: "\f2b7"; }
+.bi-cloud-slash::before { content: "\f2b8"; }
+.bi-cloud-sleet-fill::before { content: "\f2b9"; }
+.bi-cloud-sleet::before { content: "\f2ba"; }
+.bi-cloud-snow-fill::before { content: "\f2bb"; }
+.bi-cloud-snow::before { content: "\f2bc"; }
+.bi-cloud-sun-fill::before { content: "\f2bd"; }
+.bi-cloud-sun::before { content: "\f2be"; }
+.bi-cloud-upload-fill::before { content: "\f2bf"; }
+.bi-cloud-upload::before { content: "\f2c0"; }
+.bi-cloud::before { content: "\f2c1"; }
+.bi-clouds-fill::before { content: "\f2c2"; }
+.bi-clouds::before { content: "\f2c3"; }
+.bi-cloudy-fill::before { content: "\f2c4"; }
+.bi-cloudy::before { content: "\f2c5"; }
+.bi-code-slash::before { content: "\f2c6"; }
+.bi-code-square::before { content: "\f2c7"; }
+.bi-code::before { content: "\f2c8"; }
+.bi-collection-fill::before { content: "\f2c9"; }
+.bi-collection-play-fill::before { content: "\f2ca"; }
+.bi-collection-play::before { content: "\f2cb"; }
+.bi-collection::before { content: "\f2cc"; }
+.bi-columns-gap::before { content: "\f2cd"; }
+.bi-columns::before { content: "\f2ce"; }
+.bi-command::before { content: "\f2cf"; }
+.bi-compass-fill::before { content: "\f2d0"; }
+.bi-compass::before { content: "\f2d1"; }
+.bi-cone-striped::before { content: "\f2d2"; }
+.bi-cone::before { content: "\f2d3"; }
+.bi-controller::before { content: "\f2d4"; }
+.bi-cpu-fill::before { content: "\f2d5"; }
+.bi-cpu::before { content: "\f2d6"; }
+.bi-credit-card-2-back-fill::before { content: "\f2d7"; }
+.bi-credit-card-2-back::before { content: "\f2d8"; }
+.bi-credit-card-2-front-fill::before { content: "\f2d9"; }
+.bi-credit-card-2-front::before { content: "\f2da"; }
+.bi-credit-card-fill::before { content: "\f2db"; }
+.bi-credit-card::before { content: "\f2dc"; }
+.bi-crop::before { content: "\f2dd"; }
+.bi-cup-fill::before { content: "\f2de"; }
+.bi-cup-straw::before { content: "\f2df"; }
+.bi-cup::before { content: "\f2e0"; }
+.bi-cursor-fill::before { content: "\f2e1"; }
+.bi-cursor-text::before { content: "\f2e2"; }
+.bi-cursor::before { content: "\f2e3"; }
+.bi-dash-circle-dotted::before { content: "\f2e4"; }
+.bi-dash-circle-fill::before { content: "\f2e5"; }
+.bi-dash-circle::before { content: "\f2e6"; }
+.bi-dash-square-dotted::before { content: "\f2e7"; }
+.bi-dash-square-fill::before { content: "\f2e8"; }
+.bi-dash-square::before { content: "\f2e9"; }
+.bi-dash::before { content: "\f2ea"; }
+.bi-diagram-2-fill::before { content: "\f2eb"; }
+.bi-diagram-2::before { content: "\f2ec"; }
+.bi-diagram-3-fill::before { content: "\f2ed"; }
+.bi-diagram-3::before { content: "\f2ee"; }
+.bi-diamond-fill::before { content: "\f2ef"; }
+.bi-diamond-half::before { content: "\f2f0"; }
+.bi-diamond::before { content: "\f2f1"; }
+.bi-dice-1-fill::before { content: "\f2f2"; }
+.bi-dice-1::before { content: "\f2f3"; }
+.bi-dice-2-fill::before { content: "\f2f4"; }
+.bi-dice-2::before { content: "\f2f5"; }
+.bi-dice-3-fill::before { content: "\f2f6"; }
+.bi-dice-3::before { content: "\f2f7"; }
+.bi-dice-4-fill::before { content: "\f2f8"; }
+.bi-dice-4::before { content: "\f2f9"; }
+.bi-dice-5-fill::before { content: "\f2fa"; }
+.bi-dice-5::before { content: "\f2fb"; }
+.bi-dice-6-fill::before { content: "\f2fc"; }
+.bi-dice-6::before { content: "\f2fd"; }
+.bi-disc-fill::before { content: "\f2fe"; }
+.bi-disc::before { content: "\f2ff"; }
+.bi-discord::before { content: "\f300"; }
+.bi-display-fill::before { content: "\f301"; }
+.bi-display::before { content: "\f302"; }
+.bi-distribute-horizontal::before { content: "\f303"; }
+.bi-distribute-vertical::before { content: "\f304"; }
+.bi-door-closed-fill::before { content: "\f305"; }
+.bi-door-closed::before { content: "\f306"; }
+.bi-door-open-fill::before { content: "\f307"; }
+.bi-door-open::before { content: "\f308"; }
+.bi-dot::before { content: "\f309"; }
+.bi-download::before { content: "\f30a"; }
+.bi-droplet-fill::before { content: "\f30b"; }
+.bi-droplet-half::before { content: "\f30c"; }
+.bi-droplet::before { content: "\f30d"; }
+.bi-earbuds::before { content: "\f30e"; }
+.bi-easel-fill::before { content: "\f30f"; }
+.bi-easel::before { content: "\f310"; }
+.bi-egg-fill::before { content: "\f311"; }
+.bi-egg-fried::before { content: "\f312"; }
+.bi-egg::before { content: "\f313"; }
+.bi-eject-fill::before { content: "\f314"; }
+.bi-eject::before { content: "\f315"; }
+.bi-emoji-angry-fill::before { content: "\f316"; }
+.bi-emoji-angry::before { content: "\f317"; }
+.bi-emoji-dizzy-fill::before { content: "\f318"; }
+.bi-emoji-dizzy::before { content: "\f319"; }
+.bi-emoji-expressionless-fill::before { content: "\f31a"; }
+.bi-emoji-expressionless::before { content: "\f31b"; }
+.bi-emoji-frown-fill::before { content: "\f31c"; }
+.bi-emoji-frown::before { content: "\f31d"; }
+.bi-emoji-heart-eyes-fill::before { content: "\f31e"; }
+.bi-emoji-heart-eyes::before { content: "\f31f"; }
+.bi-emoji-laughing-fill::before { content: "\f320"; }
+.bi-emoji-laughing::before { content: "\f321"; }
+.bi-emoji-neutral-fill::before { content: "\f322"; }
+.bi-emoji-neutral::before { content: "\f323"; }
+.bi-emoji-smile-fill::before { content: "\f324"; }
+.bi-emoji-smile-upside-down-fill::before { content: "\f325"; }
+.bi-emoji-smile-upside-down::before { content: "\f326"; }
+.bi-emoji-smile::before { content: "\f327"; }
+.bi-emoji-sunglasses-fill::before { content: "\f328"; }
+.bi-emoji-sunglasses::before { content: "\f329"; }
+.bi-emoji-wink-fill::before { content: "\f32a"; }
+.bi-emoji-wink::before { content: "\f32b"; }
+.bi-envelope-fill::before { content: "\f32c"; }
+.bi-envelope-open-fill::before { content: "\f32d"; }
+.bi-envelope-open::before { content: "\f32e"; }
+.bi-envelope::before { content: "\f32f"; }
+.bi-eraser-fill::before { content: "\f330"; }
+.bi-eraser::before { content: "\f331"; }
+.bi-exclamation-circle-fill::before { content: "\f332"; }
+.bi-exclamation-circle::before { content: "\f333"; }
+.bi-exclamation-diamond-fill::before { content: "\f334"; }
+.bi-exclamation-diamond::before { content: "\f335"; }
+.bi-exclamation-octagon-fill::before { content: "\f336"; }
+.bi-exclamation-octagon::before { content: "\f337"; }
+.bi-exclamation-square-fill::before { content: "\f338"; }
+.bi-exclamation-square::before { content: "\f339"; }
+.bi-exclamation-triangle-fill::before { content: "\f33a"; }
+.bi-exclamation-triangle::before { content: "\f33b"; }
+.bi-exclamation::before { content: "\f33c"; }
+.bi-exclude::before { content: "\f33d"; }
+.bi-eye-fill::before { content: "\f33e"; }
+.bi-eye-slash-fill::before { content: "\f33f"; }
+.bi-eye-slash::before { content: "\f340"; }
+.bi-eye::before { content: "\f341"; }
+.bi-eyedropper::before { content: "\f342"; }
+.bi-eyeglasses::before { content: "\f343"; }
+.bi-facebook::before { content: "\f344"; }
+.bi-file-arrow-down-fill::before { content: "\f345"; }
+.bi-file-arrow-down::before { content: "\f346"; }
+.bi-file-arrow-up-fill::before { content: "\f347"; }
+.bi-file-arrow-up::before { content: "\f348"; }
+.bi-file-bar-graph-fill::before { content: "\f349"; }
+.bi-file-bar-graph::before { content: "\f34a"; }
+.bi-file-binary-fill::before { content: "\f34b"; }
+.bi-file-binary::before { content: "\f34c"; }
+.bi-file-break-fill::before { content: "\f34d"; }
+.bi-file-break::before { content: "\f34e"; }
+.bi-file-check-fill::before { content: "\f34f"; }
+.bi-file-check::before { content: "\f350"; }
+.bi-file-code-fill::before { content: "\f351"; }
+.bi-file-code::before { content: "\f352"; }
+.bi-file-diff-fill::before { content: "\f353"; }
+.bi-file-diff::before { content: "\f354"; }
+.bi-file-earmark-arrow-down-fill::before { content: "\f355"; }
+.bi-file-earmark-arrow-down::before { content: "\f356"; }
+.bi-file-earmark-arrow-up-fill::before { content: "\f357"; }
+.bi-file-earmark-arrow-up::before { content: "\f358"; }
+.bi-file-earmark-bar-graph-fill::before { content: "\f359"; }
+.bi-file-earmark-bar-graph::before { content: "\f35a"; }
+.bi-file-earmark-binary-fill::before { content: "\f35b"; }
+.bi-file-earmark-binary::before { content: "\f35c"; }
+.bi-file-earmark-break-fill::before { content: "\f35d"; }
+.bi-file-earmark-break::before { content: "\f35e"; }
+.bi-file-earmark-check-fill::before { content: "\f35f"; }
+.bi-file-earmark-check::before { content: "\f360"; }
+.bi-file-earmark-code-fill::before { content: "\f361"; }
+.bi-file-earmark-code::before { content: "\f362"; }
+.bi-file-earmark-diff-fill::before { content: "\f363"; }
+.bi-file-earmark-diff::before { content: "\f364"; }
+.bi-file-earmark-easel-fill::before { content: "\f365"; }
+.bi-file-earmark-easel::before { content: "\f366"; }
+.bi-file-earmark-excel-fill::before { content: "\f367"; }
+.bi-file-earmark-excel::before { content: "\f368"; }
+.bi-file-earmark-fill::before { content: "\f369"; }
+.bi-file-earmark-font-fill::before { content: "\f36a"; }
+.bi-file-earmark-font::before { content: "\f36b"; }
+.bi-file-earmark-image-fill::before { content: "\f36c"; }
+.bi-file-earmark-image::before { content: "\f36d"; }
+.bi-file-earmark-lock-fill::before { content: "\f36e"; }
+.bi-file-earmark-lock::before { content: "\f36f"; }
+.bi-file-earmark-lock2-fill::before { content: "\f370"; }
+.bi-file-earmark-lock2::before { content: "\f371"; }
+.bi-file-earmark-medical-fill::before { content: "\f372"; }
+.bi-file-earmark-medical::before { content: "\f373"; }
+.bi-file-earmark-minus-fill::before { content: "\f374"; }
+.bi-file-earmark-minus::before { content: "\f375"; }
+.bi-file-earmark-music-fill::before { content: "\f376"; }
+.bi-file-earmark-music::before { content: "\f377"; }
+.bi-file-earmark-person-fill::before { content: "\f378"; }
+.bi-file-earmark-person::before { content: "\f379"; }
+.bi-file-earmark-play-fill::before { content: "\f37a"; }
+.bi-file-earmark-play::before { content: "\f37b"; }
+.bi-file-earmark-plus-fill::before { content: "\f37c"; }
+.bi-file-earmark-plus::before { content: "\f37d"; }
+.bi-file-earmark-post-fill::before { content: "\f37e"; }
+.bi-file-earmark-post::before { content: "\f37f"; }
+.bi-file-earmark-ppt-fill::before { content: "\f380"; }
+.bi-file-earmark-ppt::before { content: "\f381"; }
+.bi-file-earmark-richtext-fill::before { content: "\f382"; }
+.bi-file-earmark-richtext::before { content: "\f383"; }
+.bi-file-earmark-ruled-fill::before { content: "\f384"; }
+.bi-file-earmark-ruled::before { content: "\f385"; }
+.bi-file-earmark-slides-fill::before { content: "\f386"; }
+.bi-file-earmark-slides::before { content: "\f387"; }
+.bi-file-earmark-spreadsheet-fill::before { content: "\f388"; }
+.bi-file-earmark-spreadsheet::before { content: "\f389"; }
+.bi-file-earmark-text-fill::before { content: "\f38a"; }
+.bi-file-earmark-text::before { content: "\f38b"; }
+.bi-file-earmark-word-fill::before { content: "\f38c"; }
+.bi-file-earmark-word::before { content: "\f38d"; }
+.bi-file-earmark-x-fill::before { content: "\f38e"; }
+.bi-file-earmark-x::before { content: "\f38f"; }
+.bi-file-earmark-zip-fill::before { content: "\f390"; }
+.bi-file-earmark-zip::before { content: "\f391"; }
+.bi-file-earmark::before { content: "\f392"; }
+.bi-file-easel-fill::before { content: "\f393"; }
+.bi-file-easel::before { content: "\f394"; }
+.bi-file-excel-fill::before { content: "\f395"; }
+.bi-file-excel::before { content: "\f396"; }
+.bi-file-fill::before { content: "\f397"; }
+.bi-file-font-fill::before { content: "\f398"; }
+.bi-file-font::before { content: "\f399"; }
+.bi-file-image-fill::before { content: "\f39a"; }
+.bi-file-image::before { content: "\f39b"; }
+.bi-file-lock-fill::before { content: "\f39c"; }
+.bi-file-lock::before { content: "\f39d"; }
+.bi-file-lock2-fill::before { content: "\f39e"; }
+.bi-file-lock2::before { content: "\f39f"; }
+.bi-file-medical-fill::before { content: "\f3a0"; }
+.bi-file-medical::before { content: "\f3a1"; }
+.bi-file-minus-fill::before { content: "\f3a2"; }
+.bi-file-minus::before { content: "\f3a3"; }
+.bi-file-music-fill::before { content: "\f3a4"; }
+.bi-file-music::before { content: "\f3a5"; }
+.bi-file-person-fill::before { content: "\f3a6"; }
+.bi-file-person::before { content: "\f3a7"; }
+.bi-file-play-fill::before { content: "\f3a8"; }
+.bi-file-play::before { content: "\f3a9"; }
+.bi-file-plus-fill::before { content: "\f3aa"; }
+.bi-file-plus::before { content: "\f3ab"; }
+.bi-file-post-fill::before { content: "\f3ac"; }
+.bi-file-post::before { content: "\f3ad"; }
+.bi-file-ppt-fill::before { content: "\f3ae"; }
+.bi-file-ppt::before { content: "\f3af"; }
+.bi-file-richtext-fill::before { content: "\f3b0"; }
+.bi-file-richtext::before { content: "\f3b1"; }
+.bi-file-ruled-fill::before { content: "\f3b2"; }
+.bi-file-ruled::before { content: "\f3b3"; }
+.bi-file-slides-fill::before { content: "\f3b4"; }
+.bi-file-slides::before { content: "\f3b5"; }
+.bi-file-spreadsheet-fill::before { content: "\f3b6"; }
+.bi-file-spreadsheet::before { content: "\f3b7"; }
+.bi-file-text-fill::before { content: "\f3b8"; }
+.bi-file-text::before { content: "\f3b9"; }
+.bi-file-word-fill::before { content: "\f3ba"; }
+.bi-file-word::before { content: "\f3bb"; }
+.bi-file-x-fill::before { content: "\f3bc"; }
+.bi-file-x::before { content: "\f3bd"; }
+.bi-file-zip-fill::before { content: "\f3be"; }
+.bi-file-zip::before { content: "\f3bf"; }
+.bi-file::before { content: "\f3c0"; }
+.bi-files-alt::before { content: "\f3c1"; }
+.bi-files::before { content: "\f3c2"; }
+.bi-film::before { content: "\f3c3"; }
+.bi-filter-circle-fill::before { content: "\f3c4"; }
+.bi-filter-circle::before { content: "\f3c5"; }
+.bi-filter-left::before { content: "\f3c6"; }
+.bi-filter-right::before { content: "\f3c7"; }
+.bi-filter-square-fill::before { content: "\f3c8"; }
+.bi-filter-square::before { content: "\f3c9"; }
+.bi-filter::before { content: "\f3ca"; }
+.bi-flag-fill::before { content: "\f3cb"; }
+.bi-flag::before { content: "\f3cc"; }
+.bi-flower1::before { content: "\f3cd"; }
+.bi-flower2::before { content: "\f3ce"; }
+.bi-flower3::before { content: "\f3cf"; }
+.bi-folder-check::before { content: "\f3d0"; }
+.bi-folder-fill::before { content: "\f3d1"; }
+.bi-folder-minus::before { content: "\f3d2"; }
+.bi-folder-plus::before { content: "\f3d3"; }
+.bi-folder-symlink-fill::before { content: "\f3d4"; }
+.bi-folder-symlink::before { content: "\f3d5"; }
+.bi-folder-x::before { content: "\f3d6"; }
+.bi-folder::before { content: "\f3d7"; }
+.bi-folder2-open::before { content: "\f3d8"; }
+.bi-folder2::before { content: "\f3d9"; }
+.bi-fonts::before { content: "\f3da"; }
+.bi-forward-fill::before { content: "\f3db"; }
+.bi-forward::before { content: "\f3dc"; }
+.bi-front::before { content: "\f3dd"; }
+.bi-fullscreen-exit::before { content: "\f3de"; }
+.bi-fullscreen::before { content: "\f3df"; }
+.bi-funnel-fill::before { content: "\f3e0"; }
+.bi-funnel::before { content: "\f3e1"; }
+.bi-gear-fill::before { content: "\f3e2"; }
+.bi-gear-wide-connected::before { content: "\f3e3"; }
+.bi-gear-wide::before { content: "\f3e4"; }
+.bi-gear::before { content: "\f3e5"; }
+.bi-gem::before { content: "\f3e6"; }
+.bi-geo-alt-fill::before { content: "\f3e7"; }
+.bi-geo-alt::before { content: "\f3e8"; }
+.bi-geo-fill::before { content: "\f3e9"; }
+.bi-geo::before { content: "\f3ea"; }
+.bi-gift-fill::before { content: "\f3eb"; }
+.bi-gift::before { content: "\f3ec"; }
+.bi-github::before { content: "\f3ed"; }
+.bi-globe::before { content: "\f3ee"; }
+.bi-globe2::before { content: "\f3ef"; }
+.bi-google::before { content: "\f3f0"; }
+.bi-graph-down::before { content: "\f3f1"; }
+.bi-graph-up::before { content: "\f3f2"; }
+.bi-grid-1x2-fill::before { content: "\f3f3"; }
+.bi-grid-1x2::before { content: "\f3f4"; }
+.bi-grid-3x2-gap-fill::before { content: "\f3f5"; }
+.bi-grid-3x2-gap::before { content: "\f3f6"; }
+.bi-grid-3x2::before { content: "\f3f7"; }
+.bi-grid-3x3-gap-fill::before { content: "\f3f8"; }
+.bi-grid-3x3-gap::before { content: "\f3f9"; }
+.bi-grid-3x3::before { content: "\f3fa"; }
+.bi-grid-fill::before { content: "\f3fb"; }
+.bi-grid::before { content: "\f3fc"; }
+.bi-grip-horizontal::before { content: "\f3fd"; }
+.bi-grip-vertical::before { content: "\f3fe"; }
+.bi-hammer::before { content: "\f3ff"; }
+.bi-hand-index-fill::before { content: "\f400"; }
+.bi-hand-index-thumb-fill::before { content: "\f401"; }
+.bi-hand-index-thumb::before { content: "\f402"; }
+.bi-hand-index::before { content: "\f403"; }
+.bi-hand-thumbs-down-fill::before { content: "\f404"; }
+.bi-hand-thumbs-down::before { content: "\f405"; }
+.bi-hand-thumbs-up-fill::before { content: "\f406"; }
+.bi-hand-thumbs-up::before { content: "\f407"; }
+.bi-handbag-fill::before { content: "\f408"; }
+.bi-handbag::before { content: "\f409"; }
+.bi-hash::before { content: "\f40a"; }
+.bi-hdd-fill::before { content: "\f40b"; }
+.bi-hdd-network-fill::before { content: "\f40c"; }
+.bi-hdd-network::before { content: "\f40d"; }
+.bi-hdd-rack-fill::before { content: "\f40e"; }
+.bi-hdd-rack::before { content: "\f40f"; }
+.bi-hdd-stack-fill::before { content: "\f410"; }
+.bi-hdd-stack::before { content: "\f411"; }
+.bi-hdd::before { content: "\f412"; }
+.bi-headphones::before { content: "\f413"; }
+.bi-headset::before { content: "\f414"; }
+.bi-heart-fill::before { content: "\f415"; }
+.bi-heart-half::before { content: "\f416"; }
+.bi-heart::before { content: "\f417"; }
+.bi-heptagon-fill::before { content: "\f418"; }
+.bi-heptagon-half::before { content: "\f419"; }
+.bi-heptagon::before { content: "\f41a"; }
+.bi-hexagon-fill::before { content: "\f41b"; }
+.bi-hexagon-half::before { content: "\f41c"; }
+.bi-hexagon::before { content: "\f41d"; }
+.bi-hourglass-bottom::before { content: "\f41e"; }
+.bi-hourglass-split::before { content: "\f41f"; }
+.bi-hourglass-top::before { content: "\f420"; }
+.bi-hourglass::before { content: "\f421"; }
+.bi-house-door-fill::before { content: "\f422"; }
+.bi-house-door::before { content: "\f423"; }
+.bi-house-fill::before { content: "\f424"; }
+.bi-house::before { content: "\f425"; }
+.bi-hr::before { content: "\f426"; }
+.bi-hurricane::before { content: "\f427"; }
+.bi-image-alt::before { content: "\f428"; }
+.bi-image-fill::before { content: "\f429"; }
+.bi-image::before { content: "\f42a"; }
+.bi-images::before { content: "\f42b"; }
+.bi-inbox-fill::before { content: "\f42c"; }
+.bi-inbox::before { content: "\f42d"; }
+.bi-inboxes-fill::before { content: "\f42e"; }
+.bi-inboxes::before { content: "\f42f"; }
+.bi-info-circle-fill::before { content: "\f430"; }
+.bi-info-circle::before { content: "\f431"; }
+.bi-info-square-fill::before { content: "\f432"; }
+.bi-info-square::before { content: "\f433"; }
+.bi-info::before { content: "\f434"; }
+.bi-input-cursor-text::before { content: "\f435"; }
+.bi-input-cursor::before { content: "\f436"; }
+.bi-instagram::before { content: "\f437"; }
+.bi-intersect::before { content: "\f438"; }
+.bi-journal-album::before { content: "\f439"; }
+.bi-journal-arrow-down::before { content: "\f43a"; }
+.bi-journal-arrow-up::before { content: "\f43b"; }
+.bi-journal-bookmark-fill::before { content: "\f43c"; }
+.bi-journal-bookmark::before { content: "\f43d"; }
+.bi-journal-check::before { content: "\f43e"; }
+.bi-journal-code::before { content: "\f43f"; }
+.bi-journal-medical::before { content: "\f440"; }
+.bi-journal-minus::before { content: "\f441"; }
+.bi-journal-plus::before { content: "\f442"; }
+.bi-journal-richtext::before { content: "\f443"; }
+.bi-journal-text::before { content: "\f444"; }
+.bi-journal-x::before { content: "\f445"; }
+.bi-journal::before { content: "\f446"; }
+.bi-journals::before { content: "\f447"; }
+.bi-joystick::before { content: "\f448"; }
+.bi-justify-left::before { content: "\f449"; }
+.bi-justify-right::before { content: "\f44a"; }
+.bi-justify::before { content: "\f44b"; }
+.bi-kanban-fill::before { content: "\f44c"; }
+.bi-kanban::before { content: "\f44d"; }
+.bi-key-fill::before { content: "\f44e"; }
+.bi-key::before { content: "\f44f"; }
+.bi-keyboard-fill::before { content: "\f450"; }
+.bi-keyboard::before { content: "\f451"; }
+.bi-ladder::before { content: "\f452"; }
+.bi-lamp-fill::before { content: "\f453"; }
+.bi-lamp::before { content: "\f454"; }
+.bi-laptop-fill::before { content: "\f455"; }
+.bi-laptop::before { content: "\f456"; }
+.bi-layer-backward::before { content: "\f457"; }
+.bi-layer-forward::before { content: "\f458"; }
+.bi-layers-fill::before { content: "\f459"; }
+.bi-layers-half::before { content: "\f45a"; }
+.bi-layers::before { content: "\f45b"; }
+.bi-layout-sidebar-inset-reverse::before { content: "\f45c"; }
+.bi-layout-sidebar-inset::before { content: "\f45d"; }
+.bi-layout-sidebar-reverse::before { content: "\f45e"; }
+.bi-layout-sidebar::before { content: "\f45f"; }
+.bi-layout-split::before { content: "\f460"; }
+.bi-layout-text-sidebar-reverse::before { content: "\f461"; }
+.bi-layout-text-sidebar::before { content: "\f462"; }
+.bi-layout-text-window-reverse::before { content: "\f463"; }
+.bi-layout-text-window::before { content: "\f464"; }
+.bi-layout-three-columns::before { content: "\f465"; }
+.bi-layout-wtf::before { content: "\f466"; }
+.bi-life-preserver::before { content: "\f467"; }
+.bi-lightbulb-fill::before { content: "\f468"; }
+.bi-lightbulb-off-fill::before { content: "\f469"; }
+.bi-lightbulb-off::before { content: "\f46a"; }
+.bi-lightbulb::before { content: "\f46b"; }
+.bi-lightning-charge-fill::before { content: "\f46c"; }
+.bi-lightning-charge::before { content: "\f46d"; }
+.bi-lightning-fill::before { content: "\f46e"; }
+.bi-lightning::before { content: "\f46f"; }
+.bi-link-45deg::before { content: "\f470"; }
+.bi-link::before { content: "\f471"; }
+.bi-linkedin::before { content: "\f472"; }
+.bi-list-check::before { content: "\f473"; }
+.bi-list-nested::before { content: "\f474"; }
+.bi-list-ol::before { content: "\f475"; }
+.bi-list-stars::before { content: "\f476"; }
+.bi-list-task::before { content: "\f477"; }
+.bi-list-ul::before { content: "\f478"; }
+.bi-list::before { content: "\f479"; }
+.bi-lock-fill::before { content: "\f47a"; }
+.bi-lock::before { content: "\f47b"; }
+.bi-mailbox::before { content: "\f47c"; }
+.bi-mailbox2::before { content: "\f47d"; }
+.bi-map-fill::before { content: "\f47e"; }
+.bi-map::before { content: "\f47f"; }
+.bi-markdown-fill::before { content: "\f480"; }
+.bi-markdown::before { content: "\f481"; }
+.bi-mask::before { content: "\f482"; }
+.bi-megaphone-fill::before { content: "\f483"; }
+.bi-megaphone::before { content: "\f484"; }
+.bi-menu-app-fill::before { content: "\f485"; }
+.bi-menu-app::before { content: "\f486"; }
+.bi-menu-button-fill::before { content: "\f487"; }
+.bi-menu-button-wide-fill::before { content: "\f488"; }
+.bi-menu-button-wide::before { content: "\f489"; }
+.bi-menu-button::before { content: "\f48a"; }
+.bi-menu-down::before { content: "\f48b"; }
+.bi-menu-up::before { content: "\f48c"; }
+.bi-mic-fill::before { content: "\f48d"; }
+.bi-mic-mute-fill::before { content: "\f48e"; }
+.bi-mic-mute::before { content: "\f48f"; }
+.bi-mic::before { content: "\f490"; }
+.bi-minecart-loaded::before { content: "\f491"; }
+.bi-minecart::before { content: "\f492"; }
+.bi-moisture::before { content: "\f493"; }
+.bi-moon-fill::before { content: "\f494"; }
+.bi-moon-stars-fill::before { content: "\f495"; }
+.bi-moon-stars::before { content: "\f496"; }
+.bi-moon::before { content: "\f497"; }
+.bi-mouse-fill::before { content: "\f498"; }
+.bi-mouse::before { content: "\f499"; }
+.bi-mouse2-fill::before { content: "\f49a"; }
+.bi-mouse2::before { content: "\f49b"; }
+.bi-mouse3-fill::before { content: "\f49c"; }
+.bi-mouse3::before { content: "\f49d"; }
+.bi-music-note-beamed::before { content: "\f49e"; }
+.bi-music-note-list::before { content: "\f49f"; }
+.bi-music-note::before { content: "\f4a0"; }
+.bi-music-player-fill::before { content: "\f4a1"; }
+.bi-music-player::before { content: "\f4a2"; }
+.bi-newspaper::before { content: "\f4a3"; }
+.bi-node-minus-fill::before { content: "\f4a4"; }
+.bi-node-minus::before { content: "\f4a5"; }
+.bi-node-plus-fill::before { content: "\f4a6"; }
+.bi-node-plus::before { content: "\f4a7"; }
+.bi-nut-fill::before { content: "\f4a8"; }
+.bi-nut::before { content: "\f4a9"; }
+.bi-octagon-fill::before { content: "\f4aa"; }
+.bi-octagon-half::before { content: "\f4ab"; }
+.bi-octagon::before { content: "\f4ac"; }
+.bi-option::before { content: "\f4ad"; }
+.bi-outlet::before { content: "\f4ae"; }
+.bi-paint-bucket::before { content: "\f4af"; }
+.bi-palette-fill::before { content: "\f4b0"; }
+.bi-palette::before { content: "\f4b1"; }
+.bi-palette2::before { content: "\f4b2"; }
+.bi-paperclip::before { content: "\f4b3"; }
+.bi-paragraph::before { content: "\f4b4"; }
+.bi-patch-check-fill::before { content: "\f4b5"; }
+.bi-patch-check::before { content: "\f4b6"; }
+.bi-patch-exclamation-fill::before { content: "\f4b7"; }
+.bi-patch-exclamation::before { content: "\f4b8"; }
+.bi-patch-minus-fill::before { content: "\f4b9"; }
+.bi-patch-minus::before { content: "\f4ba"; }
+.bi-patch-plus-fill::before { content: "\f4bb"; }
+.bi-patch-plus::before { content: "\f4bc"; }
+.bi-patch-question-fill::before { content: "\f4bd"; }
+.bi-patch-question::before { content: "\f4be"; }
+.bi-pause-btn-fill::before { content: "\f4bf"; }
+.bi-pause-btn::before { content: "\f4c0"; }
+.bi-pause-circle-fill::before { content: "\f4c1"; }
+.bi-pause-circle::before { content: "\f4c2"; }
+.bi-pause-fill::before { content: "\f4c3"; }
+.bi-pause::before { content: "\f4c4"; }
+.bi-peace-fill::before { content: "\f4c5"; }
+.bi-peace::before { content: "\f4c6"; }
+.bi-pen-fill::before { content: "\f4c7"; }
+.bi-pen::before { content: "\f4c8"; }
+.bi-pencil-fill::before { content: "\f4c9"; }
+.bi-pencil-square::before { content: "\f4ca"; }
+.bi-pencil::before { content: "\f4cb"; }
+.bi-pentagon-fill::before { content: "\f4cc"; }
+.bi-pentagon-half::before { content: "\f4cd"; }
+.bi-pentagon::before { content: "\f4ce"; }
+.bi-people-fill::before { content: "\f4cf"; }
+.bi-people::before { content: "\f4d0"; }
+.bi-percent::before { content: "\f4d1"; }
+.bi-person-badge-fill::before { content: "\f4d2"; }
+.bi-person-badge::before { content: "\f4d3"; }
+.bi-person-bounding-box::before { content: "\f4d4"; }
+.bi-person-check-fill::before { content: "\f4d5"; }
+.bi-person-check::before { content: "\f4d6"; }
+.bi-person-circle::before { content: "\f4d7"; }
+.bi-person-dash-fill::before { content: "\f4d8"; }
+.bi-person-dash::before { content: "\f4d9"; }
+.bi-person-fill::before { content: "\f4da"; }
+.bi-person-lines-fill::before { content: "\f4db"; }
+.bi-person-plus-fill::before { content: "\f4dc"; }
+.bi-person-plus::before { content: "\f4dd"; }
+.bi-person-square::before { content: "\f4de"; }
+.bi-person-x-fill::before { content: "\f4df"; }
+.bi-person-x::before { content: "\f4e0"; }
+.bi-person::before { content: "\f4e1"; }
+.bi-phone-fill::before { content: "\f4e2"; }
+.bi-phone-landscape-fill::before { content: "\f4e3"; }
+.bi-phone-landscape::before { content: "\f4e4"; }
+.bi-phone-vibrate-fill::before { content: "\f4e5"; }
+.bi-phone-vibrate::before { content: "\f4e6"; }
+.bi-phone::before { content: "\f4e7"; }
+.bi-pie-chart-fill::before { content: "\f4e8"; }
+.bi-pie-chart::before { content: "\f4e9"; }
+.bi-pin-angle-fill::before { content: "\f4ea"; }
+.bi-pin-angle::before { content: "\f4eb"; }
+.bi-pin-fill::before { content: "\f4ec"; }
+.bi-pin::before { content: "\f4ed"; }
+.bi-pip-fill::before { content: "\f4ee"; }
+.bi-pip::before { content: "\f4ef"; }
+.bi-play-btn-fill::before { content: "\f4f0"; }
+.bi-play-btn::before { content: "\f4f1"; }
+.bi-play-circle-fill::before { content: "\f4f2"; }
+.bi-play-circle::before { content: "\f4f3"; }
+.bi-play-fill::before { content: "\f4f4"; }
+.bi-play::before { content: "\f4f5"; }
+.bi-plug-fill::before { content: "\f4f6"; }
+.bi-plug::before { content: "\f4f7"; }
+.bi-plus-circle-dotted::before { content: "\f4f8"; }
+.bi-plus-circle-fill::before { content: "\f4f9"; }
+.bi-plus-circle::before { content: "\f4fa"; }
+.bi-plus-square-dotted::before { content: "\f4fb"; }
+.bi-plus-square-fill::before { content: "\f4fc"; }
+.bi-plus-square::before { content: "\f4fd"; }
+.bi-plus::before { content: "\f4fe"; }
+.bi-power::before { content: "\f4ff"; }
+.bi-printer-fill::before { content: "\f500"; }
+.bi-printer::before { content: "\f501"; }
+.bi-puzzle-fill::before { content: "\f502"; }
+.bi-puzzle::before { content: "\f503"; }
+.bi-question-circle-fill::before { content: "\f504"; }
+.bi-question-circle::before { content: "\f505"; }
+.bi-question-diamond-fill::before { content: "\f506"; }
+.bi-question-diamond::before { content: "\f507"; }
+.bi-question-octagon-fill::before { content: "\f508"; }
+.bi-question-octagon::before { content: "\f509"; }
+.bi-question-square-fill::before { content: "\f50a"; }
+.bi-question-square::before { content: "\f50b"; }
+.bi-question::before { content: "\f50c"; }
+.bi-rainbow::before { content: "\f50d"; }
+.bi-receipt-cutoff::before { content: "\f50e"; }
+.bi-receipt::before { content: "\f50f"; }
+.bi-reception-0::before { content: "\f510"; }
+.bi-reception-1::before { content: "\f511"; }
+.bi-reception-2::before { content: "\f512"; }
+.bi-reception-3::before { content: "\f513"; }
+.bi-reception-4::before { content: "\f514"; }
+.bi-record-btn-fill::before { content: "\f515"; }
+.bi-record-btn::before { content: "\f516"; }
+.bi-record-circle-fill::before { content: "\f517"; }
+.bi-record-circle::before { content: "\f518"; }
+.bi-record-fill::before { content: "\f519"; }
+.bi-record::before { content: "\f51a"; }
+.bi-record2-fill::before { content: "\f51b"; }
+.bi-record2::before { content: "\f51c"; }
+.bi-reply-all-fill::before { content: "\f51d"; }
+.bi-reply-all::before { content: "\f51e"; }
+.bi-reply-fill::before { content: "\f51f"; }
+.bi-reply::before { content: "\f520"; }
+.bi-rss-fill::before { content: "\f521"; }
+.bi-rss::before { content: "\f522"; }
+.bi-rulers::before { content: "\f523"; }
+.bi-save-fill::before { content: "\f524"; }
+.bi-save::before { content: "\f525"; }
+.bi-save2-fill::before { content: "\f526"; }
+.bi-save2::before { content: "\f527"; }
+.bi-scissors::before { content: "\f528"; }
+.bi-screwdriver::before { content: "\f529"; }
+.bi-search::before { content: "\f52a"; }
+.bi-segmented-nav::before { content: "\f52b"; }
+.bi-server::before { content: "\f52c"; }
+.bi-share-fill::before { content: "\f52d"; }
+.bi-share::before { content: "\f52e"; }
+.bi-shield-check::before { content: "\f52f"; }
+.bi-shield-exclamation::before { content: "\f530"; }
+.bi-shield-fill-check::before { content: "\f531"; }
+.bi-shield-fill-exclamation::before { content: "\f532"; }
+.bi-shield-fill-minus::before { content: "\f533"; }
+.bi-shield-fill-plus::before { content: "\f534"; }
+.bi-shield-fill-x::before { content: "\f535"; }
+.bi-shield-fill::before { content: "\f536"; }
+.bi-shield-lock-fill::before { content: "\f537"; }
+.bi-shield-lock::before { content: "\f538"; }
+.bi-shield-minus::before { content: "\f539"; }
+.bi-shield-plus::before { content: "\f53a"; }
+.bi-shield-shaded::before { content: "\f53b"; }
+.bi-shield-slash-fill::before { content: "\f53c"; }
+.bi-shield-slash::before { content: "\f53d"; }
+.bi-shield-x::before { content: "\f53e"; }
+.bi-shield::before { content: "\f53f"; }
+.bi-shift-fill::before { content: "\f540"; }
+.bi-shift::before { content: "\f541"; }
+.bi-shop-window::before { content: "\f542"; }
+.bi-shop::before { content: "\f543"; }
+.bi-shuffle::before { content: "\f544"; }
+.bi-signpost-2-fill::before { content: "\f545"; }
+.bi-signpost-2::before { content: "\f546"; }
+.bi-signpost-fill::before { content: "\f547"; }
+.bi-signpost-split-fill::before { content: "\f548"; }
+.bi-signpost-split::before { content: "\f549"; }
+.bi-signpost::before { content: "\f54a"; }
+.bi-sim-fill::before { content: "\f54b"; }
+.bi-sim::before { content: "\f54c"; }
+.bi-skip-backward-btn-fill::before { content: "\f54d"; }
+.bi-skip-backward-btn::before { content: "\f54e"; }
+.bi-skip-backward-circle-fill::before { content: "\f54f"; }
+.bi-skip-backward-circle::before { content: "\f550"; }
+.bi-skip-backward-fill::before { content: "\f551"; }
+.bi-skip-backward::before { content: "\f552"; }
+.bi-skip-end-btn-fill::before { content: "\f553"; }
+.bi-skip-end-btn::before { content: "\f554"; }
+.bi-skip-end-circle-fill::before { content: "\f555"; }
+.bi-skip-end-circle::before { content: "\f556"; }
+.bi-skip-end-fill::before { content: "\f557"; }
+.bi-skip-end::before { content: "\f558"; }
+.bi-skip-forward-btn-fill::before { content: "\f559"; }
+.bi-skip-forward-btn::before { content: "\f55a"; }
+.bi-skip-forward-circle-fill::before { content: "\f55b"; }
+.bi-skip-forward-circle::before { content: "\f55c"; }
+.bi-skip-forward-fill::before { content: "\f55d"; }
+.bi-skip-forward::before { content: "\f55e"; }
+.bi-skip-start-btn-fill::before { content: "\f55f"; }
+.bi-skip-start-btn::before { content: "\f560"; }
+.bi-skip-start-circle-fill::before { content: "\f561"; }
+.bi-skip-start-circle::before { content: "\f562"; }
+.bi-skip-start-fill::before { content: "\f563"; }
+.bi-skip-start::before { content: "\f564"; }
+.bi-slack::before { content: "\f565"; }
+.bi-slash-circle-fill::before { content: "\f566"; }
+.bi-slash-circle::before { content: "\f567"; }
+.bi-slash-square-fill::before { content: "\f568"; }
+.bi-slash-square::before { content: "\f569"; }
+.bi-slash::before { content: "\f56a"; }
+.bi-sliders::before { content: "\f56b"; }
+.bi-smartwatch::before { content: "\f56c"; }
+.bi-snow::before { content: "\f56d"; }
+.bi-snow2::before { content: "\f56e"; }
+.bi-snow3::before { content: "\f56f"; }
+.bi-sort-alpha-down-alt::before { content: "\f570"; }
+.bi-sort-alpha-down::before { content: "\f571"; }
+.bi-sort-alpha-up-alt::before { content: "\f572"; }
+.bi-sort-alpha-up::before { content: "\f573"; }
+.bi-sort-down-alt::before { content: "\f574"; }
+.bi-sort-down::before { content: "\f575"; }
+.bi-sort-numeric-down-alt::before { content: "\f576"; }
+.bi-sort-numeric-down::before { content: "\f577"; }
+.bi-sort-numeric-up-alt::before { content: "\f578"; }
+.bi-sort-numeric-up::before { content: "\f579"; }
+.bi-sort-up-alt::before { content: "\f57a"; }
+.bi-sort-up::before { content: "\f57b"; }
+.bi-soundwave::before { content: "\f57c"; }
+.bi-speaker-fill::before { content: "\f57d"; }
+.bi-speaker::before { content: "\f57e"; }
+.bi-speedometer::before { content: "\f57f"; }
+.bi-speedometer2::before { content: "\f580"; }
+.bi-spellcheck::before { content: "\f581"; }
+.bi-square-fill::before { content: "\f582"; }
+.bi-square-half::before { content: "\f583"; }
+.bi-square::before { content: "\f584"; }
+.bi-stack::before { content: "\f585"; }
+.bi-star-fill::before { content: "\f586"; }
+.bi-star-half::before { content: "\f587"; }
+.bi-star::before { content: "\f588"; }
+.bi-stars::before { content: "\f589"; }
+.bi-stickies-fill::before { content: "\f58a"; }
+.bi-stickies::before { content: "\f58b"; }
+.bi-sticky-fill::before { content: "\f58c"; }
+.bi-sticky::before { content: "\f58d"; }
+.bi-stop-btn-fill::before { content: "\f58e"; }
+.bi-stop-btn::before { content: "\f58f"; }
+.bi-stop-circle-fill::before { content: "\f590"; }
+.bi-stop-circle::before { content: "\f591"; }
+.bi-stop-fill::before { content: "\f592"; }
+.bi-stop::before { content: "\f593"; }
+.bi-stoplights-fill::before { content: "\f594"; }
+.bi-stoplights::before { content: "\f595"; }
+.bi-stopwatch-fill::before { content: "\f596"; }
+.bi-stopwatch::before { content: "\f597"; }
+.bi-subtract::before { content: "\f598"; }
+.bi-suit-club-fill::before { content: "\f599"; }
+.bi-suit-club::before { content: "\f59a"; }
+.bi-suit-diamond-fill::before { content: "\f59b"; }
+.bi-suit-diamond::before { content: "\f59c"; }
+.bi-suit-heart-fill::before { content: "\f59d"; }
+.bi-suit-heart::before { content: "\f59e"; }
+.bi-suit-spade-fill::before { content: "\f59f"; }
+.bi-suit-spade::before { content: "\f5a0"; }
+.bi-sun-fill::before { content: "\f5a1"; }
+.bi-sun::before { content: "\f5a2"; }
+.bi-sunglasses::before { content: "\f5a3"; }
+.bi-sunrise-fill::before { content: "\f5a4"; }
+.bi-sunrise::before { content: "\f5a5"; }
+.bi-sunset-fill::before { content: "\f5a6"; }
+.bi-sunset::before { content: "\f5a7"; }
+.bi-symmetry-horizontal::before { content: "\f5a8"; }
+.bi-symmetry-vertical::before { content: "\f5a9"; }
+.bi-table::before { content: "\f5aa"; }
+.bi-tablet-fill::before { content: "\f5ab"; }
+.bi-tablet-landscape-fill::before { content: "\f5ac"; }
+.bi-tablet-landscape::before { content: "\f5ad"; }
+.bi-tablet::before { content: "\f5ae"; }
+.bi-tag-fill::before { content: "\f5af"; }
+.bi-tag::before { content: "\f5b0"; }
+.bi-tags-fill::before { content: "\f5b1"; }
+.bi-tags::before { content: "\f5b2"; }
+.bi-telegram::before { content: "\f5b3"; }
+.bi-telephone-fill::before { content: "\f5b4"; }
+.bi-telephone-forward-fill::before { content: "\f5b5"; }
+.bi-telephone-forward::before { content: "\f5b6"; }
+.bi-telephone-inbound-fill::before { content: "\f5b7"; }
+.bi-telephone-inbound::before { content: "\f5b8"; }
+.bi-telephone-minus-fill::before { content: "\f5b9"; }
+.bi-telephone-minus::before { content: "\f5ba"; }
+.bi-telephone-outbound-fill::before { content: "\f5bb"; }
+.bi-telephone-outbound::before { content: "\f5bc"; }
+.bi-telephone-plus-fill::before { content: "\f5bd"; }
+.bi-telephone-plus::before { content: "\f5be"; }
+.bi-telephone-x-fill::before { content: "\f5bf"; }
+.bi-telephone-x::before { content: "\f5c0"; }
+.bi-telephone::before { content: "\f5c1"; }
+.bi-terminal-fill::before { content: "\f5c2"; }
+.bi-terminal::before { content: "\f5c3"; }
+.bi-text-center::before { content: "\f5c4"; }
+.bi-text-indent-left::before { content: "\f5c5"; }
+.bi-text-indent-right::before { content: "\f5c6"; }
+.bi-text-left::before { content: "\f5c7"; }
+.bi-text-paragraph::before { content: "\f5c8"; }
+.bi-text-right::before { content: "\f5c9"; }
+.bi-textarea-resize::before { content: "\f5ca"; }
+.bi-textarea-t::before { content: "\f5cb"; }
+.bi-textarea::before { content: "\f5cc"; }
+.bi-thermometer-half::before { content: "\f5cd"; }
+.bi-thermometer-high::before { content: "\f5ce"; }
+.bi-thermometer-low::before { content: "\f5cf"; }
+.bi-thermometer-snow::before { content: "\f5d0"; }
+.bi-thermometer-sun::before { content: "\f5d1"; }
+.bi-thermometer::before { content: "\f5d2"; }
+.bi-three-dots-vertical::before { content: "\f5d3"; }
+.bi-three-dots::before { content: "\f5d4"; }
+.bi-toggle-off::before { content: "\f5d5"; }
+.bi-toggle-on::before { content: "\f5d6"; }
+.bi-toggle2-off::before { content: "\f5d7"; }
+.bi-toggle2-on::before { content: "\f5d8"; }
+.bi-toggles::before { content: "\f5d9"; }
+.bi-toggles2::before { content: "\f5da"; }
+.bi-tools::before { content: "\f5db"; }
+.bi-tornado::before { content: "\f5dc"; }
+.bi-trash-fill::before { content: "\f5dd"; }
+.bi-trash::before { content: "\f5de"; }
+.bi-trash2-fill::before { content: "\f5df"; }
+.bi-trash2::before { content: "\f5e0"; }
+.bi-tree-fill::before { content: "\f5e1"; }
+.bi-tree::before { content: "\f5e2"; }
+.bi-triangle-fill::before { content: "\f5e3"; }
+.bi-triangle-half::before { content: "\f5e4"; }
+.bi-triangle::before { content: "\f5e5"; }
+.bi-trophy-fill::before { content: "\f5e6"; }
+.bi-trophy::before { content: "\f5e7"; }
+.bi-tropical-storm::before { content: "\f5e8"; }
+.bi-truck-flatbed::before { content: "\f5e9"; }
+.bi-truck::before { content: "\f5ea"; }
+.bi-tsunami::before { content: "\f5eb"; }
+.bi-tv-fill::before { content: "\f5ec"; }
+.bi-tv::before { content: "\f5ed"; }
+.bi-twitch::before { content: "\f5ee"; }
+.bi-twitter::before { content: "\f5ef"; }
+.bi-type-bold::before { content: "\f5f0"; }
+.bi-type-h1::before { content: "\f5f1"; }
+.bi-type-h2::before { content: "\f5f2"; }
+.bi-type-h3::before { content: "\f5f3"; }
+.bi-type-italic::before { content: "\f5f4"; }
+.bi-type-strikethrough::before { content: "\f5f5"; }
+.bi-type-underline::before { content: "\f5f6"; }
+.bi-type::before { content: "\f5f7"; }
+.bi-ui-checks-grid::before { content: "\f5f8"; }
+.bi-ui-checks::before { content: "\f5f9"; }
+.bi-ui-radios-grid::before { content: "\f5fa"; }
+.bi-ui-radios::before { content: "\f5fb"; }
+.bi-umbrella-fill::before { content: "\f5fc"; }
+.bi-umbrella::before { content: "\f5fd"; }
+.bi-union::before { content: "\f5fe"; }
+.bi-unlock-fill::before { content: "\f5ff"; }
+.bi-unlock::before { content: "\f600"; }
+.bi-upc-scan::before { content: "\f601"; }
+.bi-upc::before { content: "\f602"; }
+.bi-upload::before { content: "\f603"; }
+.bi-vector-pen::before { content: "\f604"; }
+.bi-view-list::before { content: "\f605"; }
+.bi-view-stacked::before { content: "\f606"; }
+.bi-vinyl-fill::before { content: "\f607"; }
+.bi-vinyl::before { content: "\f608"; }
+.bi-voicemail::before { content: "\f609"; }
+.bi-volume-down-fill::before { content: "\f60a"; }
+.bi-volume-down::before { content: "\f60b"; }
+.bi-volume-mute-fill::before { content: "\f60c"; }
+.bi-volume-mute::before { content: "\f60d"; }
+.bi-volume-off-fill::before { content: "\f60e"; }
+.bi-volume-off::before { content: "\f60f"; }
+.bi-volume-up-fill::before { content: "\f610"; }
+.bi-volume-up::before { content: "\f611"; }
+.bi-vr::before { content: "\f612"; }
+.bi-wallet-fill::before { content: "\f613"; }
+.bi-wallet::before { content: "\f614"; }
+.bi-wallet2::before { content: "\f615"; }
+.bi-watch::before { content: "\f616"; }
+.bi-water::before { content: "\f617"; }
+.bi-whatsapp::before { content: "\f618"; }
+.bi-wifi-1::before { content: "\f619"; }
+.bi-wifi-2::before { content: "\f61a"; }
+.bi-wifi-off::before { content: "\f61b"; }
+.bi-wifi::before { content: "\f61c"; }
+.bi-wind::before { content: "\f61d"; }
+.bi-window-dock::before { content: "\f61e"; }
+.bi-window-sidebar::before { content: "\f61f"; }
+.bi-window::before { content: "\f620"; }
+.bi-wrench::before { content: "\f621"; }
+.bi-x-circle-fill::before { content: "\f622"; }
+.bi-x-circle::before { content: "\f623"; }
+.bi-x-diamond-fill::before { content: "\f624"; }
+.bi-x-diamond::before { content: "\f625"; }
+.bi-x-octagon-fill::before { content: "\f626"; }
+.bi-x-octagon::before { content: "\f627"; }
+.bi-x-square-fill::before { content: "\f628"; }
+.bi-x-square::before { content: "\f629"; }
+.bi-x::before { content: "\f62a"; }
+.bi-youtube::before { content: "\f62b"; }
+.bi-zoom-in::before { content: "\f62c"; }
+.bi-zoom-out::before { content: "\f62d"; }
+.bi-bank::before { content: "\f62e"; }
+.bi-bank2::before { content: "\f62f"; }
+.bi-bell-slash-fill::before { content: "\f630"; }
+.bi-bell-slash::before { content: "\f631"; }
+.bi-cash-coin::before { content: "\f632"; }
+.bi-check-lg::before { content: "\f633"; }
+.bi-coin::before { content: "\f634"; }
+.bi-currency-bitcoin::before { content: "\f635"; }
+.bi-currency-dollar::before { content: "\f636"; }
+.bi-currency-euro::before { content: "\f637"; }
+.bi-currency-exchange::before { content: "\f638"; }
+.bi-currency-pound::before { content: "\f639"; }
+.bi-currency-yen::before { content: "\f63a"; }
+.bi-dash-lg::before { content: "\f63b"; }
+.bi-exclamation-lg::before { content: "\f63c"; }
+.bi-file-earmark-pdf-fill::before { content: "\f63d"; }
+.bi-file-earmark-pdf::before { content: "\f63e"; }
+.bi-file-pdf-fill::before { content: "\f63f"; }
+.bi-file-pdf::before { content: "\f640"; }
+.bi-gender-ambiguous::before { content: "\f641"; }
+.bi-gender-female::before { content: "\f642"; }
+.bi-gender-male::before { content: "\f643"; }
+.bi-gender-trans::before { content: "\f644"; }
+.bi-headset-vr::before { content: "\f645"; }
+.bi-info-lg::before { content: "\f646"; }
+.bi-mastodon::before { content: "\f647"; }
+.bi-messenger::before { content: "\f648"; }
+.bi-piggy-bank-fill::before { content: "\f649"; }
+.bi-piggy-bank::before { content: "\f64a"; }
+.bi-pin-map-fill::before { content: "\f64b"; }
+.bi-pin-map::before { content: "\f64c"; }
+.bi-plus-lg::before { content: "\f64d"; }
+.bi-question-lg::before { content: "\f64e"; }
+.bi-recycle::before { content: "\f64f"; }
+.bi-reddit::before { content: "\f650"; }
+.bi-safe-fill::before { content: "\f651"; }
+.bi-safe2-fill::before { content: "\f652"; }
+.bi-safe2::before { content: "\f653"; }
+.bi-sd-card-fill::before { content: "\f654"; }
+.bi-sd-card::before { content: "\f655"; }
+.bi-skype::before { content: "\f656"; }
+.bi-slash-lg::before { content: "\f657"; }
+.bi-translate::before { content: "\f658"; }
+.bi-x-lg::before { content: "\f659"; }
+.bi-safe::before { content: "\f65a"; }
+.bi-apple::before { content: "\f65b"; }
+.bi-microsoft::before { content: "\f65d"; }
+.bi-windows::before { content: "\f65e"; }
+.bi-behance::before { content: "\f65c"; }
+.bi-dribbble::before { content: "\f65f"; }
+.bi-line::before { content: "\f660"; }
+.bi-medium::before { content: "\f661"; }
+.bi-paypal::before { content: "\f662"; }
+.bi-pinterest::before { content: "\f663"; }
+.bi-signal::before { content: "\f664"; }
+.bi-snapchat::before { content: "\f665"; }
+.bi-spotify::before { content: "\f666"; }
+.bi-stack-overflow::before { content: "\f667"; }
+.bi-strava::before { content: "\f668"; }
+.bi-wordpress::before { content: "\f669"; }
+.bi-vimeo::before { content: "\f66a"; }
+.bi-activity::before { content: "\f66b"; }
+.bi-easel2-fill::before { content: "\f66c"; }
+.bi-easel2::before { content: "\f66d"; }
+.bi-easel3-fill::before { content: "\f66e"; }
+.bi-easel3::before { content: "\f66f"; }
+.bi-fan::before { content: "\f670"; }
+.bi-fingerprint::before { content: "\f671"; }
+.bi-graph-down-arrow::before { content: "\f672"; }
+.bi-graph-up-arrow::before { content: "\f673"; }
+.bi-hypnotize::before { content: "\f674"; }
+.bi-magic::before { content: "\f675"; }
+.bi-person-rolodex::before { content: "\f676"; }
+.bi-person-video::before { content: "\f677"; }
+.bi-person-video2::before { content: "\f678"; }
+.bi-person-video3::before { content: "\f679"; }
+.bi-person-workspace::before { content: "\f67a"; }
+.bi-radioactive::before { content: "\f67b"; }
+.bi-webcam-fill::before { content: "\f67c"; }
+.bi-webcam::before { content: "\f67d"; }
+.bi-yin-yang::before { content: "\f67e"; }
+.bi-bandaid-fill::before { content: "\f680"; }
+.bi-bandaid::before { content: "\f681"; }
+.bi-bluetooth::before { content: "\f682"; }
+.bi-body-text::before { content: "\f683"; }
+.bi-boombox::before { content: "\f684"; }
+.bi-boxes::before { content: "\f685"; }
+.bi-dpad-fill::before { content: "\f686"; }
+.bi-dpad::before { content: "\f687"; }
+.bi-ear-fill::before { content: "\f688"; }
+.bi-ear::before { content: "\f689"; }
+.bi-envelope-check-fill::before { content: "\f68b"; }
+.bi-envelope-check::before { content: "\f68c"; }
+.bi-envelope-dash-fill::before { content: "\f68e"; }
+.bi-envelope-dash::before { content: "\f68f"; }
+.bi-envelope-exclamation-fill::before { content: "\f691"; }
+.bi-envelope-exclamation::before { content: "\f692"; }
+.bi-envelope-plus-fill::before { content: "\f693"; }
+.bi-envelope-plus::before { content: "\f694"; }
+.bi-envelope-slash-fill::before { content: "\f696"; }
+.bi-envelope-slash::before { content: "\f697"; }
+.bi-envelope-x-fill::before { content: "\f699"; }
+.bi-envelope-x::before { content: "\f69a"; }
+.bi-explicit-fill::before { content: "\f69b"; }
+.bi-explicit::before { content: "\f69c"; }
+.bi-git::before { content: "\f69d"; }
+.bi-infinity::before { content: "\f69e"; }
+.bi-list-columns-reverse::before { content: "\f69f"; }
+.bi-list-columns::before { content: "\f6a0"; }
+.bi-meta::before { content: "\f6a1"; }
+.bi-nintendo-switch::before { content: "\f6a4"; }
+.bi-pc-display-horizontal::before { content: "\f6a5"; }
+.bi-pc-display::before { content: "\f6a6"; }
+.bi-pc-horizontal::before { content: "\f6a7"; }
+.bi-pc::before { content: "\f6a8"; }
+.bi-playstation::before { content: "\f6a9"; }
+.bi-plus-slash-minus::before { content: "\f6aa"; }
+.bi-projector-fill::before { content: "\f6ab"; }
+.bi-projector::before { content: "\f6ac"; }
+.bi-qr-code-scan::before { content: "\f6ad"; }
+.bi-qr-code::before { content: "\f6ae"; }
+.bi-quora::before { content: "\f6af"; }
+.bi-quote::before { content: "\f6b0"; }
+.bi-robot::before { content: "\f6b1"; }
+.bi-send-check-fill::before { content: "\f6b2"; }
+.bi-send-check::before { content: "\f6b3"; }
+.bi-send-dash-fill::before { content: "\f6b4"; }
+.bi-send-dash::before { content: "\f6b5"; }
+.bi-send-exclamation-fill::before { content: "\f6b7"; }
+.bi-send-exclamation::before { content: "\f6b8"; }
+.bi-send-fill::before { content: "\f6b9"; }
+.bi-send-plus-fill::before { content: "\f6ba"; }
+.bi-send-plus::before { content: "\f6bb"; }
+.bi-send-slash-fill::before { content: "\f6bc"; }
+.bi-send-slash::before { content: "\f6bd"; }
+.bi-send-x-fill::before { content: "\f6be"; }
+.bi-send-x::before { content: "\f6bf"; }
+.bi-send::before { content: "\f6c0"; }
+.bi-steam::before { content: "\f6c1"; }
+.bi-terminal-dash::before { content: "\f6c3"; }
+.bi-terminal-plus::before { content: "\f6c4"; }
+.bi-terminal-split::before { content: "\f6c5"; }
+.bi-ticket-detailed-fill::before { content: "\f6c6"; }
+.bi-ticket-detailed::before { content: "\f6c7"; }
+.bi-ticket-fill::before { content: "\f6c8"; }
+.bi-ticket-perforated-fill::before { content: "\f6c9"; }
+.bi-ticket-perforated::before { content: "\f6ca"; }
+.bi-ticket::before { content: "\f6cb"; }
+.bi-tiktok::before { content: "\f6cc"; }
+.bi-window-dash::before { content: "\f6cd"; }
+.bi-window-desktop::before { content: "\f6ce"; }
+.bi-window-fullscreen::before { content: "\f6cf"; }
+.bi-window-plus::before { content: "\f6d0"; }
+.bi-window-split::before { content: "\f6d1"; }
+.bi-window-stack::before { content: "\f6d2"; }
+.bi-window-x::before { content: "\f6d3"; }
+.bi-xbox::before { content: "\f6d4"; }
+.bi-ethernet::before { content: "\f6d5"; }
+.bi-hdmi-fill::before { content: "\f6d6"; }
+.bi-hdmi::before { content: "\f6d7"; }
+.bi-usb-c-fill::before { content: "\f6d8"; }
+.bi-usb-c::before { content: "\f6d9"; }
+.bi-usb-fill::before { content: "\f6da"; }
+.bi-usb-plug-fill::before { content: "\f6db"; }
+.bi-usb-plug::before { content: "\f6dc"; }
+.bi-usb-symbol::before { content: "\f6dd"; }
+.bi-usb::before { content: "\f6de"; }
+.bi-boombox-fill::before { content: "\f6df"; }
+.bi-displayport::before { content: "\f6e1"; }
+.bi-gpu-card::before { content: "\f6e2"; }
+.bi-memory::before { content: "\f6e3"; }
+.bi-modem-fill::before { content: "\f6e4"; }
+.bi-modem::before { content: "\f6e5"; }
+.bi-motherboard-fill::before { content: "\f6e6"; }
+.bi-motherboard::before { content: "\f6e7"; }
+.bi-optical-audio-fill::before { content: "\f6e8"; }
+.bi-optical-audio::before { content: "\f6e9"; }
+.bi-pci-card::before { content: "\f6ea"; }
+.bi-router-fill::before { content: "\f6eb"; }
+.bi-router::before { content: "\f6ec"; }
+.bi-thunderbolt-fill::before { content: "\f6ef"; }
+.bi-thunderbolt::before { content: "\f6f0"; }
+.bi-usb-drive-fill::before { content: "\f6f1"; }
+.bi-usb-drive::before { content: "\f6f2"; }
+.bi-usb-micro-fill::before { content: "\f6f3"; }
+.bi-usb-micro::before { content: "\f6f4"; }
+.bi-usb-mini-fill::before { content: "\f6f5"; }
+.bi-usb-mini::before { content: "\f6f6"; }
+.bi-cloud-haze2::before { content: "\f6f7"; }
+.bi-device-hdd-fill::before { content: "\f6f8"; }
+.bi-device-hdd::before { content: "\f6f9"; }
+.bi-device-ssd-fill::before { content: "\f6fa"; }
+.bi-device-ssd::before { content: "\f6fb"; }
+.bi-displayport-fill::before { content: "\f6fc"; }
+.bi-mortarboard-fill::before { content: "\f6fd"; }
+.bi-mortarboard::before { content: "\f6fe"; }
+.bi-terminal-x::before { content: "\f6ff"; }
+.bi-arrow-through-heart-fill::before { content: "\f700"; }
+.bi-arrow-through-heart::before { content: "\f701"; }
+.bi-badge-sd-fill::before { content: "\f702"; }
+.bi-badge-sd::before { content: "\f703"; }
+.bi-bag-heart-fill::before { content: "\f704"; }
+.bi-bag-heart::before { content: "\f705"; }
+.bi-balloon-fill::before { content: "\f706"; }
+.bi-balloon-heart-fill::before { content: "\f707"; }
+.bi-balloon-heart::before { content: "\f708"; }
+.bi-balloon::before { content: "\f709"; }
+.bi-box2-fill::before { content: "\f70a"; }
+.bi-box2-heart-fill::before { content: "\f70b"; }
+.bi-box2-heart::before { content: "\f70c"; }
+.bi-box2::before { content: "\f70d"; }
+.bi-braces-asterisk::before { content: "\f70e"; }
+.bi-calendar-heart-fill::before { content: "\f70f"; }
+.bi-calendar-heart::before { content: "\f710"; }
+.bi-calendar2-heart-fill::before { content: "\f711"; }
+.bi-calendar2-heart::before { content: "\f712"; }
+.bi-chat-heart-fill::before { content: "\f713"; }
+.bi-chat-heart::before { content: "\f714"; }
+.bi-chat-left-heart-fill::before { content: "\f715"; }
+.bi-chat-left-heart::before { content: "\f716"; }
+.bi-chat-right-heart-fill::before { content: "\f717"; }
+.bi-chat-right-heart::before { content: "\f718"; }
+.bi-chat-square-heart-fill::before { content: "\f719"; }
+.bi-chat-square-heart::before { content: "\f71a"; }
+.bi-clipboard-check-fill::before { content: "\f71b"; }
+.bi-clipboard-data-fill::before { content: "\f71c"; }
+.bi-clipboard-fill::before { content: "\f71d"; }
+.bi-clipboard-heart-fill::before { content: "\f71e"; }
+.bi-clipboard-heart::before { content: "\f71f"; }
+.bi-clipboard-minus-fill::before { content: "\f720"; }
+.bi-clipboard-plus-fill::before { content: "\f721"; }
+.bi-clipboard-pulse::before { content: "\f722"; }
+.bi-clipboard-x-fill::before { content: "\f723"; }
+.bi-clipboard2-check-fill::before { content: "\f724"; }
+.bi-clipboard2-check::before { content: "\f725"; }
+.bi-clipboard2-data-fill::before { content: "\f726"; }
+.bi-clipboard2-data::before { content: "\f727"; }
+.bi-clipboard2-fill::before { content: "\f728"; }
+.bi-clipboard2-heart-fill::before { content: "\f729"; }
+.bi-clipboard2-heart::before { content: "\f72a"; }
+.bi-clipboard2-minus-fill::before { content: "\f72b"; }
+.bi-clipboard2-minus::before { content: "\f72c"; }
+.bi-clipboard2-plus-fill::before { content: "\f72d"; }
+.bi-clipboard2-plus::before { content: "\f72e"; }
+.bi-clipboard2-pulse-fill::before { content: "\f72f"; }
+.bi-clipboard2-pulse::before { content: "\f730"; }
+.bi-clipboard2-x-fill::before { content: "\f731"; }
+.bi-clipboard2-x::before { content: "\f732"; }
+.bi-clipboard2::before { content: "\f733"; }
+.bi-emoji-kiss-fill::before { content: "\f734"; }
+.bi-emoji-kiss::before { content: "\f735"; }
+.bi-envelope-heart-fill::before { content: "\f736"; }
+.bi-envelope-heart::before { content: "\f737"; }
+.bi-envelope-open-heart-fill::before { content: "\f738"; }
+.bi-envelope-open-heart::before { content: "\f739"; }
+.bi-envelope-paper-fill::before { content: "\f73a"; }
+.bi-envelope-paper-heart-fill::before { content: "\f73b"; }
+.bi-envelope-paper-heart::before { content: "\f73c"; }
+.bi-envelope-paper::before { content: "\f73d"; }
+.bi-filetype-aac::before { content: "\f73e"; }
+.bi-filetype-ai::before { content: "\f73f"; }
+.bi-filetype-bmp::before { content: "\f740"; }
+.bi-filetype-cs::before { content: "\f741"; }
+.bi-filetype-css::before { content: "\f742"; }
+.bi-filetype-csv::before { content: "\f743"; }
+.bi-filetype-doc::before { content: "\f744"; }
+.bi-filetype-docx::before { content: "\f745"; }
+.bi-filetype-exe::before { content: "\f746"; }
+.bi-filetype-gif::before { content: "\f747"; }
+.bi-filetype-heic::before { content: "\f748"; }
+.bi-filetype-html::before { content: "\f749"; }
+.bi-filetype-java::before { content: "\f74a"; }
+.bi-filetype-jpg::before { content: "\f74b"; }
+.bi-filetype-js::before { content: "\f74c"; }
+.bi-filetype-jsx::before { content: "\f74d"; }
+.bi-filetype-key::before { content: "\f74e"; }
+.bi-filetype-m4p::before { content: "\f74f"; }
+.bi-filetype-md::before { content: "\f750"; }
+.bi-filetype-mdx::before { content: "\f751"; }
+.bi-filetype-mov::before { content: "\f752"; }
+.bi-filetype-mp3::before { content: "\f753"; }
+.bi-filetype-mp4::before { content: "\f754"; }
+.bi-filetype-otf::before { content: "\f755"; }
+.bi-filetype-pdf::before { content: "\f756"; }
+.bi-filetype-php::before { content: "\f757"; }
+.bi-filetype-png::before { content: "\f758"; }
+.bi-filetype-ppt::before { content: "\f75a"; }
+.bi-filetype-psd::before { content: "\f75b"; }
+.bi-filetype-py::before { content: "\f75c"; }
+.bi-filetype-raw::before { content: "\f75d"; }
+.bi-filetype-rb::before { content: "\f75e"; }
+.bi-filetype-sass::before { content: "\f75f"; }
+.bi-filetype-scss::before { content: "\f760"; }
+.bi-filetype-sh::before { content: "\f761"; }
+.bi-filetype-svg::before { content: "\f762"; }
+.bi-filetype-tiff::before { content: "\f763"; }
+.bi-filetype-tsx::before { content: "\f764"; }
+.bi-filetype-ttf::before { content: "\f765"; }
+.bi-filetype-txt::before { content: "\f766"; }
+.bi-filetype-wav::before { content: "\f767"; }
+.bi-filetype-woff::before { content: "\f768"; }
+.bi-filetype-xls::before { content: "\f76a"; }
+.bi-filetype-xml::before { content: "\f76b"; }
+.bi-filetype-yml::before { content: "\f76c"; }
+.bi-heart-arrow::before { content: "\f76d"; }
+.bi-heart-pulse-fill::before { content: "\f76e"; }
+.bi-heart-pulse::before { content: "\f76f"; }
+.bi-heartbreak-fill::before { content: "\f770"; }
+.bi-heartbreak::before { content: "\f771"; }
+.bi-hearts::before { content: "\f772"; }
+.bi-hospital-fill::before { content: "\f773"; }
+.bi-hospital::before { content: "\f774"; }
+.bi-house-heart-fill::before { content: "\f775"; }
+.bi-house-heart::before { content: "\f776"; }
+.bi-incognito::before { content: "\f777"; }
+.bi-magnet-fill::before { content: "\f778"; }
+.bi-magnet::before { content: "\f779"; }
+.bi-person-heart::before { content: "\f77a"; }
+.bi-person-hearts::before { content: "\f77b"; }
+.bi-phone-flip::before { content: "\f77c"; }
+.bi-plugin::before { content: "\f77d"; }
+.bi-postage-fill::before { content: "\f77e"; }
+.bi-postage-heart-fill::before { content: "\f77f"; }
+.bi-postage-heart::before { content: "\f780"; }
+.bi-postage::before { content: "\f781"; }
+.bi-postcard-fill::before { content: "\f782"; }
+.bi-postcard-heart-fill::before { content: "\f783"; }
+.bi-postcard-heart::before { content: "\f784"; }
+.bi-postcard::before { content: "\f785"; }
+.bi-search-heart-fill::before { content: "\f786"; }
+.bi-search-heart::before { content: "\f787"; }
+.bi-sliders2-vertical::before { content: "\f788"; }
+.bi-sliders2::before { content: "\f789"; }
+.bi-trash3-fill::before { content: "\f78a"; }
+.bi-trash3::before { content: "\f78b"; }
+.bi-valentine::before { content: "\f78c"; }
+.bi-valentine2::before { content: "\f78d"; }
+.bi-wrench-adjustable-circle-fill::before { content: "\f78e"; }
+.bi-wrench-adjustable-circle::before { content: "\f78f"; }
+.bi-wrench-adjustable::before { content: "\f790"; }
+.bi-filetype-json::before { content: "\f791"; }
+.bi-filetype-pptx::before { content: "\f792"; }
+.bi-filetype-xlsx::before { content: "\f793"; }
+.bi-1-circle-fill::before { content: "\f796"; }
+.bi-1-circle::before { content: "\f797"; }
+.bi-1-square-fill::before { content: "\f798"; }
+.bi-1-square::before { content: "\f799"; }
+.bi-2-circle-fill::before { content: "\f79c"; }
+.bi-2-circle::before { content: "\f79d"; }
+.bi-2-square-fill::before { content: "\f79e"; }
+.bi-2-square::before { content: "\f79f"; }
+.bi-3-circle-fill::before { content: "\f7a2"; }
+.bi-3-circle::before { content: "\f7a3"; }
+.bi-3-square-fill::before { content: "\f7a4"; }
+.bi-3-square::before { content: "\f7a5"; }
+.bi-4-circle-fill::before { content: "\f7a8"; }
+.bi-4-circle::before { content: "\f7a9"; }
+.bi-4-square-fill::before { content: "\f7aa"; }
+.bi-4-square::before { content: "\f7ab"; }
+.bi-5-circle-fill::before { content: "\f7ae"; }
+.bi-5-circle::before { content: "\f7af"; }
+.bi-5-square-fill::before { content: "\f7b0"; }
+.bi-5-square::before { content: "\f7b1"; }
+.bi-6-circle-fill::before { content: "\f7b4"; }
+.bi-6-circle::before { content: "\f7b5"; }
+.bi-6-square-fill::before { content: "\f7b6"; }
+.bi-6-square::before { content: "\f7b7"; }
+.bi-7-circle-fill::before { content: "\f7ba"; }
+.bi-7-circle::before { content: "\f7bb"; }
+.bi-7-square-fill::before { content: "\f7bc"; }
+.bi-7-square::before { content: "\f7bd"; }
+.bi-8-circle-fill::before { content: "\f7c0"; }
+.bi-8-circle::before { content: "\f7c1"; }
+.bi-8-square-fill::before { content: "\f7c2"; }
+.bi-8-square::before { content: "\f7c3"; }
+.bi-9-circle-fill::before { content: "\f7c6"; }
+.bi-9-circle::before { content: "\f7c7"; }
+.bi-9-square-fill::before { content: "\f7c8"; }
+.bi-9-square::before { content: "\f7c9"; }
+.bi-airplane-engines-fill::before { content: "\f7ca"; }
+.bi-airplane-engines::before { content: "\f7cb"; }
+.bi-airplane-fill::before { content: "\f7cc"; }
+.bi-airplane::before { content: "\f7cd"; }
+.bi-alexa::before { content: "\f7ce"; }
+.bi-alipay::before { content: "\f7cf"; }
+.bi-android::before { content: "\f7d0"; }
+.bi-android2::before { content: "\f7d1"; }
+.bi-box-fill::before { content: "\f7d2"; }
+.bi-box-seam-fill::before { content: "\f7d3"; }
+.bi-browser-chrome::before { content: "\f7d4"; }
+.bi-browser-edge::before { content: "\f7d5"; }
+.bi-browser-firefox::before { content: "\f7d6"; }
+.bi-browser-safari::before { content: "\f7d7"; }
+.bi-c-circle-fill::before { content: "\f7da"; }
+.bi-c-circle::before { content: "\f7db"; }
+.bi-c-square-fill::before { content: "\f7dc"; }
+.bi-c-square::before { content: "\f7dd"; }
+.bi-capsule-pill::before { content: "\f7de"; }
+.bi-capsule::before { content: "\f7df"; }
+.bi-car-front-fill::before { content: "\f7e0"; }
+.bi-car-front::before { content: "\f7e1"; }
+.bi-cassette-fill::before { content: "\f7e2"; }
+.bi-cassette::before { content: "\f7e3"; }
+.bi-cc-circle-fill::before { content: "\f7e6"; }
+.bi-cc-circle::before { content: "\f7e7"; }
+.bi-cc-square-fill::before { content: "\f7e8"; }
+.bi-cc-square::before { content: "\f7e9"; }
+.bi-cup-hot-fill::before { content: "\f7ea"; }
+.bi-cup-hot::before { content: "\f7eb"; }
+.bi-currency-rupee::before { content: "\f7ec"; }
+.bi-dropbox::before { content: "\f7ed"; }
+.bi-escape::before { content: "\f7ee"; }
+.bi-fast-forward-btn-fill::before { content: "\f7ef"; }
+.bi-fast-forward-btn::before { content: "\f7f0"; }
+.bi-fast-forward-circle-fill::before { content: "\f7f1"; }
+.bi-fast-forward-circle::before { content: "\f7f2"; }
+.bi-fast-forward-fill::before { content: "\f7f3"; }
+.bi-fast-forward::before { content: "\f7f4"; }
+.bi-filetype-sql::before { content: "\f7f5"; }
+.bi-fire::before { content: "\f7f6"; }
+.bi-google-play::before { content: "\f7f7"; }
+.bi-h-circle-fill::before { content: "\f7fa"; }
+.bi-h-circle::before { content: "\f7fb"; }
+.bi-h-square-fill::before { content: "\f7fc"; }
+.bi-h-square::before { content: "\f7fd"; }
+.bi-indent::before { content: "\f7fe"; }
+.bi-lungs-fill::before { content: "\f7ff"; }
+.bi-lungs::before { content: "\f800"; }
+.bi-microsoft-teams::before { content: "\f801"; }
+.bi-p-circle-fill::before { content: "\f804"; }
+.bi-p-circle::before { content: "\f805"; }
+.bi-p-square-fill::before { content: "\f806"; }
+.bi-p-square::before { content: "\f807"; }
+.bi-pass-fill::before { content: "\f808"; }
+.bi-pass::before { content: "\f809"; }
+.bi-prescription::before { content: "\f80a"; }
+.bi-prescription2::before { content: "\f80b"; }
+.bi-r-circle-fill::before { content: "\f80e"; }
+.bi-r-circle::before { content: "\f80f"; }
+.bi-r-square-fill::before { content: "\f810"; }
+.bi-r-square::before { content: "\f811"; }
+.bi-repeat-1::before { content: "\f812"; }
+.bi-repeat::before { content: "\f813"; }
+.bi-rewind-btn-fill::before { content: "\f814"; }
+.bi-rewind-btn::before { content: "\f815"; }
+.bi-rewind-circle-fill::before { content: "\f816"; }
+.bi-rewind-circle::before { content: "\f817"; }
+.bi-rewind-fill::before { content: "\f818"; }
+.bi-rewind::before { content: "\f819"; }
+.bi-train-freight-front-fill::before { content: "\f81a"; }
+.bi-train-freight-front::before { content: "\f81b"; }
+.bi-train-front-fill::before { content: "\f81c"; }
+.bi-train-front::before { content: "\f81d"; }
+.bi-train-lightrail-front-fill::before { content: "\f81e"; }
+.bi-train-lightrail-front::before { content: "\f81f"; }
+.bi-truck-front-fill::before { content: "\f820"; }
+.bi-truck-front::before { content: "\f821"; }
+.bi-ubuntu::before { content: "\f822"; }
+.bi-unindent::before { content: "\f823"; }
+.bi-unity::before { content: "\f824"; }
+.bi-universal-access-circle::before { content: "\f825"; }
+.bi-universal-access::before { content: "\f826"; }
+.bi-virus::before { content: "\f827"; }
+.bi-virus2::before { content: "\f828"; }
+.bi-wechat::before { content: "\f829"; }
+.bi-yelp::before { content: "\f82a"; }
+.bi-sign-stop-fill::before { content: "\f82b"; }
+.bi-sign-stop-lights-fill::before { content: "\f82c"; }
+.bi-sign-stop-lights::before { content: "\f82d"; }
+.bi-sign-stop::before { content: "\f82e"; }
+.bi-sign-turn-left-fill::before { content: "\f82f"; }
+.bi-sign-turn-left::before { content: "\f830"; }
+.bi-sign-turn-right-fill::before { content: "\f831"; }
+.bi-sign-turn-right::before { content: "\f832"; }
+.bi-sign-turn-slight-left-fill::before { content: "\f833"; }
+.bi-sign-turn-slight-left::before { content: "\f834"; }
+.bi-sign-turn-slight-right-fill::before { content: "\f835"; }
+.bi-sign-turn-slight-right::before { content: "\f836"; }
+.bi-sign-yield-fill::before { content: "\f837"; }
+.bi-sign-yield::before { content: "\f838"; }
+.bi-ev-station-fill::before { content: "\f839"; }
+.bi-ev-station::before { content: "\f83a"; }
+.bi-fuel-pump-diesel-fill::before { content: "\f83b"; }
+.bi-fuel-pump-diesel::before { content: "\f83c"; }
+.bi-fuel-pump-fill::before { content: "\f83d"; }
+.bi-fuel-pump::before { content: "\f83e"; }
+.bi-0-circle-fill::before { content: "\f83f"; }
+.bi-0-circle::before { content: "\f840"; }
+.bi-0-square-fill::before { content: "\f841"; }
+.bi-0-square::before { content: "\f842"; }
+.bi-rocket-fill::before { content: "\f843"; }
+.bi-rocket-takeoff-fill::before { content: "\f844"; }
+.bi-rocket-takeoff::before { content: "\f845"; }
+.bi-rocket::before { content: "\f846"; }
+.bi-stripe::before { content: "\f847"; }
+.bi-subscript::before { content: "\f848"; }
+.bi-superscript::before { content: "\f849"; }
+.bi-trello::before { content: "\f84a"; }
+.bi-envelope-at-fill::before { content: "\f84b"; }
+.bi-envelope-at::before { content: "\f84c"; }
+.bi-regex::before { content: "\f84d"; }
+.bi-text-wrap::before { content: "\f84e"; }
+.bi-sign-dead-end-fill::before { content: "\f84f"; }
+.bi-sign-dead-end::before { content: "\f850"; }
+.bi-sign-do-not-enter-fill::before { content: "\f851"; }
+.bi-sign-do-not-enter::before { content: "\f852"; }
+.bi-sign-intersection-fill::before { content: "\f853"; }
+.bi-sign-intersection-side-fill::before { content: "\f854"; }
+.bi-sign-intersection-side::before { content: "\f855"; }
+.bi-sign-intersection-t-fill::before { content: "\f856"; }
+.bi-sign-intersection-t::before { content: "\f857"; }
+.bi-sign-intersection-y-fill::before { content: "\f858"; }
+.bi-sign-intersection-y::before { content: "\f859"; }
+.bi-sign-intersection::before { content: "\f85a"; }
+.bi-sign-merge-left-fill::before { content: "\f85b"; }
+.bi-sign-merge-left::before { content: "\f85c"; }
+.bi-sign-merge-right-fill::before { content: "\f85d"; }
+.bi-sign-merge-right::before { content: "\f85e"; }
+.bi-sign-no-left-turn-fill::before { content: "\f85f"; }
+.bi-sign-no-left-turn::before { content: "\f860"; }
+.bi-sign-no-parking-fill::before { content: "\f861"; }
+.bi-sign-no-parking::before { content: "\f862"; }
+.bi-sign-no-right-turn-fill::before { content: "\f863"; }
+.bi-sign-no-right-turn::before { content: "\f864"; }
+.bi-sign-railroad-fill::before { content: "\f865"; }
+.bi-sign-railroad::before { content: "\f866"; }
+.bi-building-add::before { content: "\f867"; }
+.bi-building-check::before { content: "\f868"; }
+.bi-building-dash::before { content: "\f869"; }
+.bi-building-down::before { content: "\f86a"; }
+.bi-building-exclamation::before { content: "\f86b"; }
+.bi-building-fill-add::before { content: "\f86c"; }
+.bi-building-fill-check::before { content: "\f86d"; }
+.bi-building-fill-dash::before { content: "\f86e"; }
+.bi-building-fill-down::before { content: "\f86f"; }
+.bi-building-fill-exclamation::before { content: "\f870"; }
+.bi-building-fill-gear::before { content: "\f871"; }
+.bi-building-fill-lock::before { content: "\f872"; }
+.bi-building-fill-slash::before { content: "\f873"; }
+.bi-building-fill-up::before { content: "\f874"; }
+.bi-building-fill-x::before { content: "\f875"; }
+.bi-building-fill::before { content: "\f876"; }
+.bi-building-gear::before { content: "\f877"; }
+.bi-building-lock::before { content: "\f878"; }
+.bi-building-slash::before { content: "\f879"; }
+.bi-building-up::before { content: "\f87a"; }
+.bi-building-x::before { content: "\f87b"; }
+.bi-buildings-fill::before { content: "\f87c"; }
+.bi-buildings::before { content: "\f87d"; }
+.bi-bus-front-fill::before { content: "\f87e"; }
+.bi-bus-front::before { content: "\f87f"; }
+.bi-ev-front-fill::before { content: "\f880"; }
+.bi-ev-front::before { content: "\f881"; }
+.bi-globe-americas::before { content: "\f882"; }
+.bi-globe-asia-australia::before { content: "\f883"; }
+.bi-globe-central-south-asia::before { content: "\f884"; }
+.bi-globe-europe-africa::before { content: "\f885"; }
+.bi-house-add-fill::before { content: "\f886"; }
+.bi-house-add::before { content: "\f887"; }
+.bi-house-check-fill::before { content: "\f888"; }
+.bi-house-check::before { content: "\f889"; }
+.bi-house-dash-fill::before { content: "\f88a"; }
+.bi-house-dash::before { content: "\f88b"; }
+.bi-house-down-fill::before { content: "\f88c"; }
+.bi-house-down::before { content: "\f88d"; }
+.bi-house-exclamation-fill::before { content: "\f88e"; }
+.bi-house-exclamation::before { content: "\f88f"; }
+.bi-house-gear-fill::before { content: "\f890"; }
+.bi-house-gear::before { content: "\f891"; }
+.bi-house-lock-fill::before { content: "\f892"; }
+.bi-house-lock::before { content: "\f893"; }
+.bi-house-slash-fill::before { content: "\f894"; }
+.bi-house-slash::before { content: "\f895"; }
+.bi-house-up-fill::before { content: "\f896"; }
+.bi-house-up::before { content: "\f897"; }
+.bi-house-x-fill::before { content: "\f898"; }
+.bi-house-x::before { content: "\f899"; }
+.bi-person-add::before { content: "\f89a"; }
+.bi-person-down::before { content: "\f89b"; }
+.bi-person-exclamation::before { content: "\f89c"; }
+.bi-person-fill-add::before { content: "\f89d"; }
+.bi-person-fill-check::before { content: "\f89e"; }
+.bi-person-fill-dash::before { content: "\f89f"; }
+.bi-person-fill-down::before { content: "\f8a0"; }
+.bi-person-fill-exclamation::before { content: "\f8a1"; }
+.bi-person-fill-gear::before { content: "\f8a2"; }
+.bi-person-fill-lock::before { content: "\f8a3"; }
+.bi-person-fill-slash::before { content: "\f8a4"; }
+.bi-person-fill-up::before { content: "\f8a5"; }
+.bi-person-fill-x::before { content: "\f8a6"; }
+.bi-person-gear::before { content: "\f8a7"; }
+.bi-person-lock::before { content: "\f8a8"; }
+.bi-person-slash::before { content: "\f8a9"; }
+.bi-person-up::before { content: "\f8aa"; }
+.bi-scooter::before { content: "\f8ab"; }
+.bi-taxi-front-fill::before { content: "\f8ac"; }
+.bi-taxi-front::before { content: "\f8ad"; }
+.bi-amd::before { content: "\f8ae"; }
+.bi-database-add::before { content: "\f8af"; }
+.bi-database-check::before { content: "\f8b0"; }
+.bi-database-dash::before { content: "\f8b1"; }
+.bi-database-down::before { content: "\f8b2"; }
+.bi-database-exclamation::before { content: "\f8b3"; }
+.bi-database-fill-add::before { content: "\f8b4"; }
+.bi-database-fill-check::before { content: "\f8b5"; }
+.bi-database-fill-dash::before { content: "\f8b6"; }
+.bi-database-fill-down::before { content: "\f8b7"; }
+.bi-database-fill-exclamation::before { content: "\f8b8"; }
+.bi-database-fill-gear::before { content: "\f8b9"; }
+.bi-database-fill-lock::before { content: "\f8ba"; }
+.bi-database-fill-slash::before { content: "\f8bb"; }
+.bi-database-fill-up::before { content: "\f8bc"; }
+.bi-database-fill-x::before { content: "\f8bd"; }
+.bi-database-fill::before { content: "\f8be"; }
+.bi-database-gear::before { content: "\f8bf"; }
+.bi-database-lock::before { content: "\f8c0"; }
+.bi-database-slash::before { content: "\f8c1"; }
+.bi-database-up::before { content: "\f8c2"; }
+.bi-database-x::before { content: "\f8c3"; }
+.bi-database::before { content: "\f8c4"; }
+.bi-houses-fill::before { content: "\f8c5"; }
+.bi-houses::before { content: "\f8c6"; }
+.bi-nvidia::before { content: "\f8c7"; }
+.bi-person-vcard-fill::before { content: "\f8c8"; }
+.bi-person-vcard::before { content: "\f8c9"; }
+.bi-sina-weibo::before { content: "\f8ca"; }
+.bi-tencent-qq::before { content: "\f8cb"; }
+.bi-wikipedia::before { content: "\f8cc"; }
+.bi-alphabet-uppercase::before { content: "\f2a5"; }
+.bi-alphabet::before { content: "\f68a"; }
+.bi-amazon::before { content: "\f68d"; }
+.bi-arrows-collapse-vertical::before { content: "\f690"; }
+.bi-arrows-expand-vertical::before { content: "\f695"; }
+.bi-arrows-vertical::before { content: "\f698"; }
+.bi-arrows::before { content: "\f6a2"; }
+.bi-ban-fill::before { content: "\f6a3"; }
+.bi-ban::before { content: "\f6b6"; }
+.bi-bing::before { content: "\f6c2"; }
+.bi-cake::before { content: "\f6e0"; }
+.bi-cake2::before { content: "\f6ed"; }
+.bi-cookie::before { content: "\f6ee"; }
+.bi-copy::before { content: "\f759"; }
+.bi-crosshair::before { content: "\f769"; }
+.bi-crosshair2::before { content: "\f794"; }
+.bi-emoji-astonished-fill::before { content: "\f795"; }
+.bi-emoji-astonished::before { content: "\f79a"; }
+.bi-emoji-grimace-fill::before { content: "\f79b"; }
+.bi-emoji-grimace::before { content: "\f7a0"; }
+.bi-emoji-grin-fill::before { content: "\f7a1"; }
+.bi-emoji-grin::before { content: "\f7a6"; }
+.bi-emoji-surprise-fill::before { content: "\f7a7"; }
+.bi-emoji-surprise::before { content: "\f7ac"; }
+.bi-emoji-tear-fill::before { content: "\f7ad"; }
+.bi-emoji-tear::before { content: "\f7b2"; }
+.bi-envelope-arrow-down-fill::before { content: "\f7b3"; }
+.bi-envelope-arrow-down::before { content: "\f7b8"; }
+.bi-envelope-arrow-up-fill::before { content: "\f7b9"; }
+.bi-envelope-arrow-up::before { content: "\f7be"; }
+.bi-feather::before { content: "\f7bf"; }
+.bi-feather2::before { content: "\f7c4"; }
+.bi-floppy-fill::before { content: "\f7c5"; }
+.bi-floppy::before { content: "\f7d8"; }
+.bi-floppy2-fill::before { content: "\f7d9"; }
+.bi-floppy2::before { content: "\f7e4"; }
+.bi-gitlab::before { content: "\f7e5"; }
+.bi-highlighter::before { content: "\f7f8"; }
+.bi-marker-tip::before { content: "\f802"; }
+.bi-nvme-fill::before { content: "\f803"; }
+.bi-nvme::before { content: "\f80c"; }
+.bi-opencollective::before { content: "\f80d"; }
+.bi-pci-card-network::before { content: "\f8cd"; }
+.bi-pci-card-sound::before { content: "\f8ce"; }
+.bi-radar::before { content: "\f8cf"; }
+.bi-send-arrow-down-fill::before { content: "\f8d0"; }
+.bi-send-arrow-down::before { content: "\f8d1"; }
+.bi-send-arrow-up-fill::before { content: "\f8d2"; }
+.bi-send-arrow-up::before { content: "\f8d3"; }
+.bi-sim-slash-fill::before { content: "\f8d4"; }
+.bi-sim-slash::before { content: "\f8d5"; }
+.bi-sourceforge::before { content: "\f8d6"; }
+.bi-substack::before { content: "\f8d7"; }
+.bi-threads-fill::before { content: "\f8d8"; }
+.bi-threads::before { content: "\f8d9"; }
+.bi-transparency::before { content: "\f8da"; }
+.bi-twitter-x::before { content: "\f8db"; }
+.bi-type-h4::before { content: "\f8dc"; }
+.bi-type-h5::before { content: "\f8dd"; }
+.bi-type-h6::before { content: "\f8de"; }
+.bi-backpack-fill::before { content: "\f8df"; }
+.bi-backpack::before { content: "\f8e0"; }
+.bi-backpack2-fill::before { content: "\f8e1"; }
+.bi-backpack2::before { content: "\f8e2"; }
+.bi-backpack3-fill::before { content: "\f8e3"; }
+.bi-backpack3::before { content: "\f8e4"; }
+.bi-backpack4-fill::before { content: "\f8e5"; }
+.bi-backpack4::before { content: "\f8e6"; }
+.bi-brilliance::before { content: "\f8e7"; }
+.bi-cake-fill::before { content: "\f8e8"; }
+.bi-cake2-fill::before { content: "\f8e9"; }
+.bi-duffle-fill::before { content: "\f8ea"; }
+.bi-duffle::before { content: "\f8eb"; }
+.bi-exposure::before { content: "\f8ec"; }
+.bi-gender-neuter::before { content: "\f8ed"; }
+.bi-highlights::before { content: "\f8ee"; }
+.bi-luggage-fill::before { content: "\f8ef"; }
+.bi-luggage::before { content: "\f8f0"; }
+.bi-mailbox-flag::before { content: "\f8f1"; }
+.bi-mailbox2-flag::before { content: "\f8f2"; }
+.bi-noise-reduction::before { content: "\f8f3"; }
+.bi-passport-fill::before { content: "\f8f4"; }
+.bi-passport::before { content: "\f8f5"; }
+.bi-person-arms-up::before { content: "\f8f6"; }
+.bi-person-raised-hand::before { content: "\f8f7"; }
+.bi-person-standing-dress::before { content: "\f8f8"; }
+.bi-person-standing::before { content: "\f8f9"; }
+.bi-person-walking::before { content: "\f8fa"; }
+.bi-person-wheelchair::before { content: "\f8fb"; }
+.bi-shadows::before { content: "\f8fc"; }
+.bi-suitcase-fill::before { content: "\f8fd"; }
+.bi-suitcase-lg-fill::before { content: "\f8fe"; }
+.bi-suitcase-lg::before { content: "\f8ff"; }
+.bi-suitcase::before { content: "\f900"; }
+.bi-suitcase2-fill::before { content: "\f901"; }
+.bi-suitcase2::before { content: "\f902"; }
+.bi-vignette::before { content: "\f903"; }

BIN
data/www/assets/bootstrap-icons/fonts/bootstrap-icons.woff


BIN
data/www/assets/bootstrap-icons/fonts/bootstrap-icons.woff2


ファイルの差分が大きいため隠しています
+ 4 - 0
data/www/assets/bootstrap/css/bootstrap.min.css


ファイルの差分が大きいため隠しています
+ 5 - 0
data/www/assets/bootstrap/js/bootstrap.bundle.min.js


ファイルの差分が大きいため隠しています
+ 6 - 0
data/www/assets/chartjs/chart.umd.min.js


BIN
data/www/assets/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf


ファイルの差分が大きいため隠しています
+ 5 - 0
data/www/assets/monaco/vs/base/worker/workerMain.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/abap/abap.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/apex/apex.js


+ 10 - 0
data/www/assets/monaco/vs/basic-languages/azcli/azcli.js

@@ -0,0 +1,10 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/azcli/azcli", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var s=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var r=Object.getOwnPropertyNames;var l=Object.prototype.hasOwnProperty;var c=(t,e)=>{for(var o in e)s(t,o,{get:e[o],enumerable:!0})},k=(t,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of r(e))!l.call(t,n)&&n!==o&&s(t,n,{get:()=>e[n],enumerable:!(a=i(e,n))||a.enumerable});return t};var p=t=>k(s({},"__esModule",{value:!0}),t);var d={};c(d,{conf:()=>f,language:()=>g});var f={comments:{lineComment:"#"}},g={defaultToken:"keyword",ignoreCase:!0,tokenPostfix:".azcli",str:/[^#\s]/,tokenizer:{root:[{include:"@comment"},[/\s-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}],[/^-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}]],type:[{include:"@comment"},[/-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":"key.identifier"}}],[/@str+\s*/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}]],comment:[[/#.*$/,{cases:{"@eos":{token:"comment",next:"@popall"}}}]]}};return p(d);})();
+return moduleExports;
+});

ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/bat/bat.js


+ 11 - 0
data/www/assets/monaco/vs/basic-languages/bicep/bicep.js

@@ -0,0 +1,11 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/bicep/bicep", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var r=Object.defineProperty;var s=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var a=Object.prototype.hasOwnProperty;var g=(e,n)=>{for(var o in n)r(e,o,{get:n[o],enumerable:!0})},l=(e,n,o,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of c(n))!a.call(e,t)&&t!==o&&r(e,t,{get:()=>n[t],enumerable:!(i=s(n,t))||i.enumerable});return e};var m=e=>l(r({},"__esModule",{value:!0}),e);var y={};g(y,{conf:()=>$,language:()=>w});var p=e=>`\\b${e}\\b`,k="[_a-zA-Z]",x="[_a-zA-Z0-9]",u=p(`${k}${x}*`),d=["targetScope","resource","module","param","var","output","for","in","if","existing"],b=["true","false","null"],f="[ \\t\\r\\n]",C="[0-9]+",$={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'"},{open:"'''",close:"'''"}],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:"'''",close:"'''",notIn:["string","comment"]}],autoCloseBefore:`:.,=}])' 
+	`,indentationRules:{increaseIndentPattern:new RegExp("^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$"),decreaseIndentPattern:new RegExp("^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$")}},w={defaultToken:"",tokenPostfix:".bicep",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],symbols:/[=><!~?:&|+\-*/^%]+/,keywords:d,namedLiterals:b,escapes:"\\\\(u{[0-9A-Fa-f]+}|n|r|t|\\\\|'|\\${)",tokenizer:{root:[{include:"@expression"},{include:"@whitespace"}],stringVerbatim:[{regex:"(|'|'')[^']",action:{token:"string"}},{regex:"'''",action:{token:"string.quote",next:"@pop"}}],stringLiteral:[{regex:"\\${",action:{token:"delimiter.bracket",next:"@bracketCounting"}},{regex:"[^\\\\'$]+",action:{token:"string"}},{regex:"@escapes",action:{token:"string.escape"}},{regex:"\\\\.",action:{token:"string.escape.invalid"}},{regex:"'",action:{token:"string",next:"@pop"}}],bracketCounting:[{regex:"{",action:{token:"delimiter.bracket",next:"@bracketCounting"}},{regex:"}",action:{token:"delimiter.bracket",next:"@pop"}},{include:"expression"}],comment:[{regex:"[^\\*]+",action:{token:"comment"}},{regex:"\\*\\/",action:{token:"comment",next:"@pop"}},{regex:"[\\/*]",action:{token:"comment"}}],whitespace:[{regex:f},{regex:"\\/\\*",action:{token:"comment",next:"@comment"}},{regex:"\\/\\/.*$",action:{token:"comment"}}],expression:[{regex:"'''",action:{token:"string.quote",next:"@stringVerbatim"}},{regex:"'",action:{token:"string.quote",next:"@stringLiteral"}},{regex:C,action:{token:"number"}},{regex:u,action:{cases:{"@keywords":{token:"keyword"},"@namedLiterals":{token:"keyword"},"@default":{token:"identifier"}}}}]}};return m(y);})();
+return moduleExports;
+});

ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/cameligo/cameligo.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/clojure/clojure.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/coffee/coffee.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/cpp/cpp.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/csharp/csharp.js


+ 10 - 0
data/www/assets/monaco/vs/basic-languages/csp/csp.js

@@ -0,0 +1,10 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/csp/csp", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var o=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var u=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var a=(r,t)=>{for(var s in t)o(r,s,{get:t[s],enumerable:!0})},c=(r,t,s,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let e of u(t))!g.call(r,e)&&e!==s&&o(r,e,{get:()=>t[e],enumerable:!(n=i(t,e))||n.enumerable});return r};var q=r=>c(o({},"__esModule",{value:!0}),r);var p={};a(p,{conf:()=>f,language:()=>l});var f={brackets:[],autoClosingPairs:[],surroundingPairs:[]},l={keywords:[],typeKeywords:[],tokenPostfix:".csp",operators:[],symbols:/[=><!~?:&|+\-*\/\^%]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/child-src/,"string.quote"],[/connect-src/,"string.quote"],[/default-src/,"string.quote"],[/font-src/,"string.quote"],[/frame-src/,"string.quote"],[/img-src/,"string.quote"],[/manifest-src/,"string.quote"],[/media-src/,"string.quote"],[/object-src/,"string.quote"],[/script-src/,"string.quote"],[/style-src/,"string.quote"],[/worker-src/,"string.quote"],[/base-uri/,"string.quote"],[/plugin-types/,"string.quote"],[/sandbox/,"string.quote"],[/disown-opener/,"string.quote"],[/form-action/,"string.quote"],[/frame-ancestors/,"string.quote"],[/report-uri/,"string.quote"],[/report-to/,"string.quote"],[/upgrade-insecure-requests/,"string.quote"],[/block-all-mixed-content/,"string.quote"],[/require-sri-for/,"string.quote"],[/reflected-xss/,"string.quote"],[/referrer/,"string.quote"],[/policy-uri/,"string.quote"],[/'self'/,"string.quote"],[/'unsafe-inline'/,"string.quote"],[/'unsafe-eval'/,"string.quote"],[/'strict-dynamic'/,"string.quote"],[/'unsafe-hashed-attributes'/,"string.quote"]]}};return q(p);})();
+return moduleExports;
+});

ファイルの差分が大きいため隠しています
+ 8 - 0
data/www/assets/monaco/vs/basic-languages/css/css.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/cypher/cypher.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/dart/dart.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/dockerfile/dockerfile.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/ecl/ecl.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/elixir/elixir.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/flow9/flow9.js


ファイルの差分が大きいため隠しています
+ 9 - 0
data/www/assets/monaco/vs/basic-languages/freemarker2/freemarker2.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/fsharp/fsharp.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/go/go.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/graphql/graphql.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/handlebars/handlebars.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/hcl/hcl.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/html/html.js


+ 10 - 0
data/www/assets/monaco/vs/basic-languages/ini/ini.js

@@ -0,0 +1,10 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/ini/ini", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var t=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var r=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var c=(n,e)=>{for(var s in e)t(n,s,{get:e[s],enumerable:!0})},l=(n,e,s,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of r(e))!g.call(n,o)&&o!==s&&t(n,o,{get:()=>e[o],enumerable:!(a=i(e,o))||a.enumerable});return n};var p=n=>l(t({},"__esModule",{value:!0}),n);var f={};c(f,{conf:()=>u,language:()=>m});var u={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},m={defaultToken:"",tokenPostfix:".ini",escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/^\[[^\]]*\]/,"metatag"],[/(^\w+)(\s*)(\=)/,["key","","delimiter"]],{include:"@whitespace"},[/\d+/,"number"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string",'@string."'],[/'/,"string","@string.'"]],whitespace:[[/[ \t\r\n]+/,""],[/^\s*[#;].*$/,"comment"]],string:[[/[^\\"']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]]}};return p(f);})();
+return moduleExports;
+});

ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/java/java.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/javascript/javascript.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/julia/julia.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/kotlin/kotlin.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/less/less.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/lexon/lexon.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/liquid/liquid.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/lua/lua.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/m3/m3.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/markdown/markdown.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/mdx/mdx.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/mips/mips.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/msdax/msdax.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/mysql/mysql.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/objective-c/objective-c.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/pascal/pascal.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/pascaligo/pascaligo.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/perl/perl.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/pgsql/pgsql.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/php/php.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/pla/pla.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/postiats/postiats.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/powerquery/powerquery.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/powershell/powershell.js


ファイルの差分が大きいため隠しています
+ 8 - 0
data/www/assets/monaco/vs/basic-languages/protobuf/protobuf.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/pug/pug.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/python/python.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/qsharp/qsharp.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/r/r.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/razor/razor.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/redis/redis.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/redshift/redshift.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/restructuredtext/restructuredtext.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/ruby/ruby.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/rust/rust.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/sb/sb.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/scala/scala.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/scheme/scheme.js


ファイルの差分が大きいため隠しています
+ 8 - 0
data/www/assets/monaco/vs/basic-languages/scss/scss.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/shell/shell.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/solidity/solidity.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/sophia/sophia.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/sparql/sparql.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/sql/sql.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/st/st.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/swift/swift.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/systemverilog/systemverilog.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/tcl/tcl.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/twig/twig.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/typescript/typescript.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/typespec/typespec.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/vb/vb.js


+ 307 - 0
data/www/assets/monaco/vs/basic-languages/wgsl/wgsl.js

@@ -0,0 +1,307 @@
+/*!-----------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
+ * Released under the MIT license
+ * https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
+ *-----------------------------------------------------------------------------*/
+define("vs/basic-languages/wgsl/wgsl", ["require","require"],(require)=>{
+"use strict";var moduleExports=(()=>{var s=Object.defineProperty;var m=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var p=(t,e)=>{for(var a in e)s(t,a,{get:e[a],enumerable:!0})},d=(t,e,a,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of l(e))!u.call(t,i)&&i!==a&&s(t,i,{get:()=>e[i],enumerable:!(o=m(e,i))||o.enumerable});return t};var x=t=>d(s({},"__esModule",{value:!0}),t);var F={};p(F,{conf:()=>f,language:()=>L});var f={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"}]};function r(t){let e=[],a=t.split(/\t+|\r+|\n+| +/);for(let o=0;o<a.length;++o)a[o].length>0&&e.push(a[o]);return e}var g=r("true false"),_=r(`
+			  alias
+			  break
+			  case
+			  const
+			  const_assert
+			  continue
+			  continuing
+			  default
+			  diagnostic
+			  discard
+			  else
+			  enable
+			  fn
+			  for
+			  if
+			  let
+			  loop
+			  override
+			  requires
+			  return
+			  struct
+			  switch
+			  var
+			  while
+			  `),h=r(`
+			  NULL
+			  Self
+			  abstract
+			  active
+			  alignas
+			  alignof
+			  as
+			  asm
+			  asm_fragment
+			  async
+			  attribute
+			  auto
+			  await
+			  become
+			  binding_array
+			  cast
+			  catch
+			  class
+			  co_await
+			  co_return
+			  co_yield
+			  coherent
+			  column_major
+			  common
+			  compile
+			  compile_fragment
+			  concept
+			  const_cast
+			  consteval
+			  constexpr
+			  constinit
+			  crate
+			  debugger
+			  decltype
+			  delete
+			  demote
+			  demote_to_helper
+			  do
+			  dynamic_cast
+			  enum
+			  explicit
+			  export
+			  extends
+			  extern
+			  external
+			  fallthrough
+			  filter
+			  final
+			  finally
+			  friend
+			  from
+			  fxgroup
+			  get
+			  goto
+			  groupshared
+			  highp
+			  impl
+			  implements
+			  import
+			  inline
+			  instanceof
+			  interface
+			  layout
+			  lowp
+			  macro
+			  macro_rules
+			  match
+			  mediump
+			  meta
+			  mod
+			  module
+			  move
+			  mut
+			  mutable
+			  namespace
+			  new
+			  nil
+			  noexcept
+			  noinline
+			  nointerpolation
+			  noperspective
+			  null
+			  nullptr
+			  of
+			  operator
+			  package
+			  packoffset
+			  partition
+			  pass
+			  patch
+			  pixelfragment
+			  precise
+			  precision
+			  premerge
+			  priv
+			  protected
+			  pub
+			  public
+			  readonly
+			  ref
+			  regardless
+			  register
+			  reinterpret_cast
+			  require
+			  resource
+			  restrict
+			  self
+			  set
+			  shared
+			  sizeof
+			  smooth
+			  snorm
+			  static
+			  static_assert
+			  static_cast
+			  std
+			  subroutine
+			  super
+			  target
+			  template
+			  this
+			  thread_local
+			  throw
+			  trait
+			  try
+			  type
+			  typedef
+			  typeid
+			  typename
+			  typeof
+			  union
+			  unless
+			  unorm
+			  unsafe
+			  unsized
+			  use
+			  using
+			  varying
+			  virtual
+			  volatile
+			  wgsl
+			  where
+			  with
+			  writeonly
+			  yield
+			  `),b=r(`
+		read write read_write
+		function private workgroup uniform storage
+		perspective linear flat
+		center centroid sample
+		vertex_index instance_index position front_facing frag_depth
+			local_invocation_id local_invocation_index
+			global_invocation_id workgroup_id num_workgroups
+			sample_index sample_mask
+		rgba8unorm
+		rgba8snorm
+		rgba8uint
+		rgba8sint
+		rgba16uint
+		rgba16sint
+		rgba16float
+		r32uint
+		r32sint
+		r32float
+		rg32uint
+		rg32sint
+		rg32float
+		rgba32uint
+		rgba32sint
+		rgba32float
+		bgra8unorm
+`),v=r(`
+		bool
+		f16
+		f32
+		i32
+		sampler sampler_comparison
+		texture_depth_2d
+		texture_depth_2d_array
+		texture_depth_cube
+		texture_depth_cube_array
+		texture_depth_multisampled_2d
+		texture_external
+		texture_external
+		u32
+		`),y=r(`
+		array
+		atomic
+		mat2x2
+		mat2x3
+		mat2x4
+		mat3x2
+		mat3x3
+		mat3x4
+		mat4x2
+		mat4x3
+		mat4x4
+		ptr
+		texture_1d
+		texture_2d
+		texture_2d_array
+		texture_3d
+		texture_cube
+		texture_cube_array
+		texture_multisampled_2d
+		texture_storage_1d
+		texture_storage_2d
+		texture_storage_2d_array
+		texture_storage_3d
+		vec2
+		vec3
+		vec4
+		`),k=r(`
+		vec2i vec3i vec4i
+		vec2u vec3u vec4u
+		vec2f vec3f vec4f
+		vec2h vec3h vec4h
+		mat2x2f mat2x3f mat2x4f
+		mat3x2f mat3x3f mat3x4f
+		mat4x2f mat4x3f mat4x4f
+		mat2x2h mat2x3h mat2x4h
+		mat3x2h mat3x3h mat3x4h
+		mat4x2h mat4x3h mat4x4h
+		`),w=r(`
+  bitcast all any select arrayLength abs acos acosh asin asinh atan atanh atan2
+  ceil clamp cos cosh countLeadingZeros countOneBits countTrailingZeros cross
+  degrees determinant distance dot exp exp2 extractBits faceForward firstLeadingBit
+  firstTrailingBit floor fma fract frexp inverseBits inverseSqrt ldexp length
+  log log2 max min mix modf normalize pow quantizeToF16 radians reflect refract
+  reverseBits round saturate sign sin sinh smoothstep sqrt step tan tanh transpose
+  trunc dpdx dpdxCoarse dpdxFine dpdy dpdyCoarse dpdyFine fwidth fwidthCoarse fwidthFine
+  textureDimensions textureGather textureGatherCompare textureLoad textureNumLayers
+  textureNumLevels textureNumSamples textureSample textureSampleBias textureSampleCompare
+  textureSampleCompareLevel textureSampleGrad textureSampleLevel textureSampleBaseClampToEdge
+  textureStore atomicLoad atomicStore atomicAdd atomicSub atomicMax atomicMin
+  atomicAnd atomicOr atomicXor atomicExchange atomicCompareExchangeWeak pack4x8snorm
+  pack4x8unorm pack2x16snorm pack2x16unorm pack2x16float unpack4x8snorm unpack4x8unorm
+  unpack2x16snorm unpack2x16unorm unpack2x16float storageBarrier workgroupBarrier
+  workgroupUniformLoad
+`),S=r(`
+					 &
+					 &&
+					 ->
+					 /
+					 =
+					 ==
+					 !=
+					 >
+					 >=
+					 <
+					 <=
+					 %
+					 -
+					 --
+					 +
+					 ++
+					 |
+					 ||
+					 *
+					 <<
+					 >>
+					 +=
+					 -=
+					 *=
+					 /=
+					 %=
+					 &=
+					 |=
+					 ^=
+					 >>=
+					 <<=
+					 `),C=/enable|requires|diagnostic/,c=/[_\p{XID_Start}]\p{XID_Continue}*/u,n="variable.predefined",L={tokenPostfix:".wgsl",defaultToken:"invalid",unicode:!0,atoms:g,keywords:_,reserved:h,predeclared_enums:b,predeclared_types:v,predeclared_type_generators:y,predeclared_type_aliases:k,predeclared_intrinsics:w,operators:S,symbols:/[!%&*+\-\.\/:;<=>^|_~,]+/,tokenizer:{root:[[C,"keyword","@directive"],[c,{cases:{"@atoms":n,"@keywords":"keyword","@reserved":"invalid","@predeclared_enums":n,"@predeclared_types":n,"@predeclared_type_generators":n,"@predeclared_type_aliases":n,"@predeclared_intrinsics":n,"@default":"identifier"}}],{include:"@commentOrSpace"},{include:"@numbers"},[/[{}()\[\]]/,"@brackets"],["@","annotation","@attribute"],[/@symbols/,{cases:{"@operators":"operator","@default":"delimiter"}}],[/./,"invalid"]],commentOrSpace:[[/\s+/,"white"],[/\/\*/,"comment","@blockComment"],[/\/\/.*$/,"comment"]],blockComment:[[/[^\/*]+/,"comment"],[/\/\*/,"comment","@push"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],attribute:[{include:"@commentOrSpace"},[/\w+/,"annotation","@pop"]],directive:[{include:"@commentOrSpace"},[/[()]/,"@brackets"],[/,/,"delimiter"],[c,"meta.content"],[/;/,"delimiter","@pop"]],numbers:[[/0[fh]/,"number.float"],[/[1-9][0-9]*[fh]/,"number.float"],[/[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?[fh]?/,"number.float"],[/[0-9]+\.[0-9]*([eE][+-]?[0-9]+)?[fh]?/,"number.float"],[/[0-9]+[eE][+-]?[0-9]+[fh]?/,"number.float"],[/0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+(?:[pP][+-]?[0-9]+[fh]?)?/,"number.hex"],[/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*(?:[pP][+-]?[0-9]+[fh]?)?/,"number.hex"],[/0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+[fh]?/,"number.hex"],[/0[xX][0-9a-fA-F]+[iu]?/,"number.hex"],[/[1-9][0-9]*[iu]?/,"number"],[/0[iu]?/,"number"]]}};return x(F);})();
+return moduleExports;
+});

ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/xml/xml.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/basic-languages/yaml/yaml.js


ファイルの差分が大きいため隠しています
+ 5 - 0
data/www/assets/monaco/vs/editor/editor.main.css


ファイルの差分が大きいため隠しています
+ 5 - 0
data/www/assets/monaco/vs/editor/editor.main.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/language/css/cssMode.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/language/css/cssWorker.js


ファイルの差分が大きいため隠しています
+ 7 - 0
data/www/assets/monaco/vs/language/html/htmlMode.js


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません