Kaynağa Gözat

Merge branch 'development' of github.com:visuddhinanda/mint into development

visuddhinanda 2 ay önce
ebeveyn
işleme
da7acfef9c
100 değiştirilmiş dosya ile 1922 ekleme ve 333 silme
  1. 1 0
      .gitignore
  2. 23 0
      DEVELOPMENT.md
  3. 1 0
      ai-translate/.gitignore
  4. 5 3
      ai-translate/ai_translate/__main__.py
  5. 0 31
      dashboard-v4/rpc/tulip/Dockerfile
  6. 0 15
      dashboard-v4/rpc/tulip/build.sh
  7. 0 15
      dashboard-v4/rpc/tulip/start.sh
  8. 0 17
      dashboard-v4/rpc/tulip/tulip/README.md
  9. 27 0
      dashboard-v6/.gitignore
  10. 1 0
      dashboard-v6/.tool-versions
  11. 10 0
      dashboard-v6/README.md
  12. 23 0
      dashboard-v6/eslint.config.js
  13. 13 0
      dashboard-v6/index.html
  14. 41 0
      dashboard-v6/package.json
  15. 1 0
      dashboard-v6/public/vite.svg
  16. 13 0
      dashboard-v6/scripts/dashboard.sh
  17. 25 0
      dashboard-v6/src/App.tsx
  18. 47 0
      dashboard-v6/src/Router.tsx
  19. 0 0
      dashboard-v6/src/api/index.ts
  20. 0 0
      dashboard-v6/src/assets/react.svg
  21. 6 0
      dashboard-v6/src/components/Loading.tsx
  22. 10 0
      dashboard-v6/src/index.css
  23. 6 0
      dashboard-v6/src/layouts/Footer.tsx
  24. 16 0
      dashboard-v6/src/layouts/Root.tsx
  25. 21 0
      dashboard-v6/src/layouts/anonymous/index.tsx
  26. 21 0
      dashboard-v6/src/layouts/dashboard/index.tsx
  27. 3 0
      dashboard-v6/src/locales/en-US.ts
  28. 51 0
      dashboard-v6/src/locales/index.ts
  29. 1 0
      dashboard-v6/src/locales/zh-Hans.ts
  30. 1 0
      dashboard-v6/src/locales/zh-Hant.ts
  31. 12 0
      dashboard-v6/src/main.tsx
  32. 6 0
      dashboard-v6/src/pages/dashboard/index.tsx
  33. 6 0
      dashboard-v6/src/pages/home.tsx
  34. 6 0
      dashboard-v6/src/pages/users/personal/index.tsx
  35. 6 0
      dashboard-v6/src/pages/users/sign-in.tsx
  36. 41 0
      dashboard-v6/src/reducers/layout.ts
  37. 63 0
      dashboard-v6/src/reducers/session.ts
  38. 98 0
      dashboard-v6/src/request.ts
  39. 17 0
      dashboard-v6/src/store.ts
  40. 28 0
      dashboard-v6/tsconfig.app.json
  41. 7 0
      dashboard-v6/tsconfig.json
  42. 26 0
      dashboard-v6/tsconfig.node.json
  43. 15 0
      dashboard-v6/vite.config.ts
  44. 4 0
      deploy/group_vars/all.yml
  45. 115 0
      deploy/mint-deprecated.yml
  46. 20 98
      deploy/mint.yml
  47. 16 0
      deploy/roles/clove-build/tasks/main.yml
  48. 7 11
      deploy/roles/mint-dashboard-v4-build/tasks/main.yml
  49. 16 2
      deploy/roles/mint-nginx/tasks/main.yml
  50. 6 4
      deploy/roles/mint-nginx/templates/fpm.conf.j2
  51. 6 7
      deploy/roles/mint-nginx/templates/nginx.conf.j2
  52. 11 0
      deploy/roles/mint-openai-proxy-build/tasks/main.yml
  53. 11 0
      deploy/roles/mint-release-build/tasks/ai-translate.yml
  54. 9 0
      deploy/roles/mint-release-build/tasks/api-v12.yml
  55. 12 0
      deploy/roles/mint-release-build/tasks/api-v8.yml
  56. 45 0
      deploy/roles/mint-release-build/tasks/main.yml
  57. 73 0
      deploy/roles/mint-release-build/templates/build.sh.j2
  58. 3 0
      deploy/roles/mint-release-build/templates/shell.sh.j2
  59. 3 3
      deploy/roles/mint-v2.1/tasks/ai-translate.yml
  60. 3 3
      deploy/roles/mint-v2.1/tasks/clove.yml
  61. 5 0
      deploy/roles/mint-v2.1/tasks/laravel-workers.yml
  62. 0 3
      deploy/roles/mint-v2.1/tasks/laravel.yml
  63. 11 2
      deploy/roles/mint-v2.1/tasks/main.yml
  64. 10 3
      deploy/roles/mint-v2.1/tasks/openai-proxy.yml
  65. 3 3
      deploy/roles/mint-v2.1/tasks/schedule-run.yml
  66. 1 1
      deploy/roles/mint-v2.1/templates/containers/api-v8.sh.j2
  67. 1 1
      deploy/roles/mint-v2.1/templates/containers/openai-proxy.sh.j2
  68. 1 1
      deploy/roles/mint-v2.1/templates/v2/ai-translate.toml.j2
  69. 2 2
      deploy/roles/mint-v2.1/templates/v2/openai-proxy.json.j2
  70. 27 0
      deploy/roles/mint-v2.2/tasks/clove.yml
  71. 42 0
      deploy/roles/mint-v2.2/tasks/laravel.yml
  72. 89 0
      deploy/roles/mint-v2.2/tasks/main.yml
  73. 4 0
      deploy/roles/mint-v2.2/templates/containers/php-fpm.sh.j2
  74. 52 0
      deploy/roles/mint-v2.2/templates/containers/run.sh.j2
  75. 10 0
      deploy/roles/mint-v2.2/templates/containers/schedule-run.timer.j2
  76. 6 0
      deploy/roles/mint-v2.2/templates/containers/shell.sh.j2
  77. 18 0
      deploy/roles/mint-v2.2/templates/v1/config.js.j2
  78. 67 0
      deploy/roles/mint-v2.2/templates/v1/config.php.j2
  79. 16 0
      deploy/roles/mint-v2.2/templates/v2/ai-translate.toml.j2
  80. 97 0
      deploy/roles/mint-v2.2/templates/v2/env.j2
  81. 5 0
      deploy/roles/mint-v2.2/templates/v2/openai-proxy.json.j2
  82. 2 0
      deploy/roles/mint-v2.2/templates/version.txt.j2
  83. 1 0
      docker/.gitignore
  84. 8 6
      docker/mint/Dockerfile
  85. 12 4
      docker/mint/README.md
  86. 7 7
      docker/mint/build.sh
  87. 58 0
      docker/python/Dockerfile
  88. 16 0
      docker/python/README.md
  89. 22 0
      docker/python/build.sh
  90. 128 91
      docker/spring/Dockerfile
  91. 1 0
      magnolia/.gitignore
  92. 2 0
      magnolia/docker/.gitignore
  93. 71 0
      magnolia/docker/Dockerfile
  94. 25 0
      magnolia/docker/README.md
  95. 16 0
      magnolia/docker/build.sh
  96. 5 0
      magnolia/docker/start.sh
  97. 7 0
      magnolia/ec2.yml
  98. 3 0
      magnolia/group_vars/all.yml
  99. 4 0
      magnolia/open-search.yml
  100. 9 0
      magnolia/ping.yml

+ 1 - 0
.gitignore

@@ -5,5 +5,6 @@
 *.swp
 *.log
 .stop
+*.xz
 
 /k8s/

+ 23 - 0
DEVELOPMENT.md

@@ -0,0 +1,23 @@
+# Development
+
+## For local dev
+
+- Download [asdf](https://github.com/asdf-vm/asdf/releases) and put in into your `/usr/local/bin` folder.
+
+- Install Nodejs & Php
+
+```bash
+
+sudo apt -y install dirmngr gpg curl gawk
+
+asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
+asdf install nodejs 24.11.1
+node -v
+npm -v
+
+# https://github.com/asdf-community/asdf-php
+sudo apt -y install plocate
+asdf plugin add php https://github.com/asdf-community/asdf-php.git
+asdf install php 8.5.0
+php -v
+```

+ 1 - 0
ai-translate/.gitignore

@@ -4,3 +4,4 @@
 __pycache__/
 ai_translate.egg-info/
 .stop
+*.log

+ 5 - 3
ai-translate/ai_translate/__main__.py

@@ -2,6 +2,7 @@ import logging
 import argparse
 import sys
 import os
+import datetime
 
 from . import launch
 from .utils import is_stopped
@@ -28,10 +29,11 @@ def main():
 
     if args.debug:
         logging.basicConfig(
-            level=logging.DEBUG if args.debug else logging.INFO, format='%(levelname)-5s %(asctime)s(%(pathname)s %(lineno)d): %(message)s')
+            level=args.debug, format='%(levelname)-5s %(asctime)s(%(pathname)s %(lineno)d): %(message)s')
     else:
-        logging.basicConfig(
-            level=logging.DEBUG if args.debug else logging.INFO, format='%(levelname)-5s %(asctime)s(%(module)s): %(message)s')
+        now = datetime.datetime.now()
+        logging.basicConfig(filename=now.strftime("%Y%m%d%H%M%S.log"), level=logging.INFO,
+                            format='%(levelname)-5s %(asctime)s(%(module)s): %(message)s')
     is_stopped()
     try:
         launch(args.name, args.queue, args.config)

+ 0 - 31
dashboard-v4/rpc/tulip/Dockerfile

@@ -1,31 +0,0 @@
-FROM ubuntu:latest
-LABEL maintainer="Jeremy Zheng"
-
-ENV DEBIAN_FRONTEND noninteractive
-
-RUN apt update
-RUN apt install -y apt-utils debian-keyring debian-archive-keyring apt-transport-https software-properties-common gnupg
-RUN LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php
-RUN apt -y upgrade
-ENV PHP_VERSION="8.2"
-RUN apt install -y sudo wget unzip \
-    php${PHP_VERSION}-zip php${PHP_VERSION}-grpc php${PHP_VERSION}-protobuf
-RUN apt clean
-
-# https://getcomposer.org/download/
-RUN wget https://raw.githubusercontent.com/composer/getcomposer.org/76a7060ccb93902cd7576b67264ad91c8a2700e2/web/installer -O - -q | php -- --quiet --install-dir=/usr/local/bin --filename=composer
-
-RUN useradd -s /bin/bash -m deploy
-RUN passwd -l deploy
-RUN echo 'deploy ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/101-deploy
-RUN mkdir /opt/tulip
-RUN chown deploy:deploy /opt/tulip
-USER deploy
-
-COPY tulip /opt/tulip/
-RUN cd /opt/tulip && composer install
-RUN echo "$(date -u +%4Y%m%d%H%M%S)" | sudo tee /VERSION
-
-WORKDIR /opt/tulip
-
-CMD ["/bin/bash", "-l"]

+ 0 - 15
dashboard-v4/rpc/tulip/build.sh

@@ -1,15 +0,0 @@
-#!/bin/bash
-
-set -e
-
-export VERSION=$(date "+%4Y%m%d%H%M%S")
-export CODE="mint-tulip"
-
-buildah pull ubuntu:latest
-buildah bud --layers -t $CODE .
-podman save --format=oci-archive -o $CODE-$VERSION.tar $CODE
-md5sum $CODE-$VERSION.tar > md5.txt
-
-echo "done($CODE-$VERSION.tar)."
-
-exit 0

+ 0 - 15
dashboard-v4/rpc/tulip/start.sh

@@ -1,15 +0,0 @@
-#!/bin/bash
-
-set -e
-
-export CODE="mint-tulip"
-
-if [ "$#" -ne 1 ]
-then
-    echo "USAGE: $0 PORT"
-    exit 1
-fi
-
-podman run -d --rm --events-backend=file --hostname=palm --network host $CODE /usr/bin/php server.php --port $1
-
-exit 0

+ 0 - 17
dashboard-v4/rpc/tulip/tulip/README.md

@@ -1,17 +0,0 @@
-# 全文搜索字典更新
-
-1. 下载字典文件
-
-```bash
-php dict_maker.php
-```
-
-文件将下载到 storage/dict 目录。下载过程中,该目录下有.stop 文件。下载结束后.stop 文件会被删除。
-
-2. 复制字典文件到 postgresql 目录
-3. 运行 `php dict_update.php` 令字典文件生效
-4. 运行 `php content_update.php` 重新生成索引
-
-# 全文搜索内容数据更新
-
-运行 `php content_download.php` 从 api 获取巴利语全文搜索数据。运行时间约 1 小时。

+ 27 - 0
dashboard-v6/.gitignore

@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# my add
+package-lock.json

+ 1 - 0
dashboard-v6/.tool-versions

@@ -0,0 +1 @@
+nodejs 24.11.1

+ 10 - 0
dashboard-v6/README.md

@@ -0,0 +1,10 @@
+# Dashboard
+
+## Usage
+
+```bash
+npm install
+
+# http://localhost:4000/pcd-v2026/
+npm run dev
+```

+ 23 - 0
dashboard-v6/eslint.config.js

@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+  globalIgnores(['dist']),
+  {
+    files: ['**/*.{ts,tsx}'],
+    extends: [
+      js.configs.recommended,
+      tseslint.configs.recommended,
+      reactHooks.configs.flat.recommended,
+      reactRefresh.configs.vite,
+    ],
+    languageOptions: {
+      ecmaVersion: 2020,
+      globals: globals.browser,
+    },
+  },
+])

+ 13 - 0
dashboard-v6/index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>dashboard</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>

+ 41 - 0
dashboard-v6/package.json

@@ -0,0 +1,41 @@
+{
+  "name": "dashboard",
+  "private": true,
+  "version": "2026.1.1",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "lint": "eslint .",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@ant-design/x": "^2.1.2",
+    "@graphiql/react": "^0.37.3",
+    "@reduxjs/toolkit": "^2.11.2",
+    "@types/js-cookie": "^3.0.6",
+    "antd": "^6.1.3",
+    "jose": "^6.1.3",
+    "js-cookie": "^3.0.5",
+    "react": "^19.2.0",
+    "react-dom": "^19.2.0",
+    "react-intl": "^7.1.14",
+    "react-redux": "^9.2.0",
+    "react-router": "^7.11.0",
+    "usehooks-ts": "^3.1.1"
+  },
+  "devDependencies": {
+    "@eslint/js": "^9.39.1",
+    "@types/node": "^24.10.1",
+    "@types/react": "^19.2.5",
+    "@types/react-dom": "^19.2.3",
+    "@vitejs/plugin-react": "^5.1.1",
+    "eslint": "^9.39.1",
+    "eslint-plugin-react-hooks": "^7.0.1",
+    "eslint-plugin-react-refresh": "^0.4.24",
+    "globals": "^16.5.0",
+    "typescript": "~5.9.3",
+    "typescript-eslint": "^8.46.4",
+    "vite": "^7.2.4"
+  }
+}

+ 1 - 0
dashboard-v6/public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 13 - 0
dashboard-v6/scripts/dashboard.sh

@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -e
+
+# @ant-design/pro-components
+npm install --save \
+    react-router react-intl @reduxjs/toolkit react-redux \
+    @graphiql/react \
+    usehooks-ts jose \
+    js-cookie @types/js-cookie \
+    antd @ant-design/x
+
+exit 0

+ 25 - 0
dashboard-v6/src/App.tsx

@@ -0,0 +1,25 @@
+import { Suspense } from "react";
+import { IntlProvider } from "react-intl";
+import { Provider } from "react-redux";
+
+import Loading from "./components/Loading";
+import Router from "./Router";
+import store from "./store";
+import { detect as detect_locale, messages as get_messages } from "./locales";
+
+const locale = detect_locale();
+const messages = get_messages(locale);
+
+const Widget = () => {
+  return (
+    <IntlProvider locale={locale} messages={messages}>
+      <Suspense fallback={<Loading />}>
+        <Provider store={store}>
+          <Router />
+        </Provider>
+      </Suspense>
+    </IntlProvider>
+  );
+};
+
+export default Widget;

+ 47 - 0
dashboard-v6/src/Router.tsx

@@ -0,0 +1,47 @@
+import { lazy } from "react";
+import { createBrowserRouter } from "react-router";
+import { RouterProvider } from "react-router/dom";
+
+const UsersSignIn = lazy(() => import("./pages/users/sign-in"));
+const UsersPersonal = lazy(() => import("./pages/users/personal"));
+const DashboardIndex = lazy(() => import("./pages/dashboard/index"));
+const Home = lazy(() => import("./pages/home"));
+
+const RootLayout = lazy(() => import("./layouts/Root"));
+const AnonymousLayout = lazy(() => import("./layouts/anonymous"));
+const DashboardLayout = lazy(() => import("./layouts/dashboard"));
+
+const router = createBrowserRouter(
+  [
+    {
+      path: "/",
+      Component: RootLayout,
+      children: [
+        { index: true, Component: Home },
+        {
+          path: "anonymous",
+          Component: AnonymousLayout,
+          children: [{ path: "sign-in", Component: UsersSignIn }],
+        },
+        {
+          path: "dashboard",
+          Component: DashboardLayout,
+          children: [
+            { index: true, Component: DashboardIndex },
+            {
+              path: "users",
+              children: [{ path: "personal", Component: UsersPersonal }],
+            },
+          ],
+        },
+      ],
+    },
+  ],
+  { basename: import.meta.env.BASE_URL }
+);
+
+const Widget = () => {
+  return <RouterProvider router={router} />;
+};
+
+export default Widget;

+ 0 - 0
dashboard-v6/src/api/index.ts


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
dashboard-v6/src/assets/react.svg


+ 6 - 0
dashboard-v6/src/components/Loading.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+  // TODO
+  return <>Loading</>;
+};
+
+export default Widget;

+ 10 - 0
dashboard-v6/src/index.css

@@ -0,0 +1,10 @@
+html,
+body {
+  padding: 0;
+  margin: 0;
+  background-color: #fff;
+}
+
+#root {
+  padding: 24px;
+}

+ 6 - 0
dashboard-v6/src/layouts/Footer.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+  // TODO
+  return <div>base layout footer</div>;
+};
+
+export default Widget;

+ 16 - 0
dashboard-v6/src/layouts/Root.tsx

@@ -0,0 +1,16 @@
+import { Outlet } from "react-router";
+
+const Widget = () => {
+  // TODO
+  return (
+    <div>
+      <div>root header</div>
+      <div>
+        <Outlet />
+      </div>
+      <div>root layout footer</div>
+    </div>
+  );
+};
+
+export default Widget;

+ 21 - 0
dashboard-v6/src/layouts/anonymous/index.tsx

@@ -0,0 +1,21 @@
+import { Outlet } from "react-router";
+
+import Footer from "../Footer";
+
+const Widget = () => {
+  // TODO
+  return (
+    <div>
+      <div>anonymous header</div>
+      <div>
+        <Outlet />
+      </div>
+      <div>
+        anonymous layout footer
+        <Footer />
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 21 - 0
dashboard-v6/src/layouts/dashboard/index.tsx

@@ -0,0 +1,21 @@
+import { Outlet } from "react-router";
+
+import Footer from "../Footer";
+
+const Widget = () => {
+  // TODO
+  return (
+    <div>
+      <div>dashboard header</div>
+      <div>
+        <Outlet />
+      </div>
+      <div>
+        dashboard layout footer
+        <Footer />
+      </div>
+    </div>
+  );
+};
+
+export default Widget;

+ 3 - 0
dashboard-v6/src/locales/en-US.ts

@@ -0,0 +1,3 @@
+export default {
+  "buttons.ok": "Ok",
+};

+ 51 - 0
dashboard-v6/src/locales/index.ts

@@ -0,0 +1,51 @@
+import Cookies from "js-cookie";
+import { type MessageFormatElement } from "react-intl";
+import dayjs from "dayjs";
+import isLeapYear from "dayjs/plugin/isLeapYear";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+import localizedFormat from "dayjs/plugin/localizedFormat";
+import "dayjs/locale/zh-cn";
+import "dayjs/locale/zh-tw";
+import "dayjs/locale/en";
+
+dayjs.extend(isLeapYear);
+dayjs.extend(utc);
+dayjs.extend(timezone);
+dayjs.extend(localizedFormat);
+
+import enUS from "./en-US";
+import zhHans from "./zh-Hans";
+import zhHant from "./zh-Hant";
+
+const KEY = "locale";
+
+export const detect = (): string => Cookies.get(KEY) || "en-US";
+
+export const set = (locale: string) => {
+  switch (locale) {
+    case "zh-Hans":
+      dayjs.locale("zh-cn");
+      break;
+    case "zh-Hants":
+      dayjs.locale("zh-tw");
+      break;
+    default:
+      dayjs.locale("en-us");
+      break;
+  }
+  Cookies.set(KEY, locale);
+};
+
+export const messages = (
+  locale: string
+): Record<string, string> | Record<string, MessageFormatElement[]> => {
+  switch (locale) {
+    case "zh-Hans":
+      return zhHans;
+    case "zh-Hants":
+      return zhHant;
+    default:
+      return enUS;
+  }
+};

+ 1 - 0
dashboard-v6/src/locales/zh-Hans.ts

@@ -0,0 +1 @@
+export default {};

+ 1 - 0
dashboard-v6/src/locales/zh-Hant.ts

@@ -0,0 +1 @@
+export default {};

+ 12 - 0
dashboard-v6/src/main.tsx

@@ -0,0 +1,12 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import App from "./App.tsx";
+
+import "./index.css";
+
+createRoot(document.getElementById("root")!).render(
+  <StrictMode>
+    <App />
+  </StrictMode>
+);

+ 6 - 0
dashboard-v6/src/pages/dashboard/index.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+  // TODO
+  return <>dashboard index</>;
+};
+
+export default Widget;

+ 6 - 0
dashboard-v6/src/pages/home.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+  // TODO
+  return <>home</>;
+};
+
+export default Widget;

+ 6 - 0
dashboard-v6/src/pages/users/personal/index.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+  // TODO
+  return <>User personal</>;
+};
+
+export default Widget;

+ 6 - 0
dashboard-v6/src/pages/users/sign-in.tsx

@@ -0,0 +1,6 @@
+const Widget = () => {
+  // TODO
+  return <>user sign-in</>;
+};
+
+export default Widget;

+ 41 - 0
dashboard-v6/src/reducers/layout.ts

@@ -0,0 +1,41 @@
+import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export interface IRefresh {
+  title: string;
+  subhead: string;
+  description: string;
+  copyright: string;
+  version: string;
+}
+
+interface LayoutState {
+  title?: string;
+  subhead?: string;
+  description?: string;
+  copyright?: string;
+  version?: string;
+}
+
+const initialState: LayoutState = {};
+
+export const layoutSlice = createSlice({
+  name: "layout",
+  initialState,
+  reducers: {
+    refresh: (state, action: PayloadAction<IRefresh>) => {
+      state.version = action.payload.version;
+      state.subhead = action.payload.subhead;
+      state.description = action.payload.description;
+      state.copyright = action.payload.copyright;
+      state.version = action.payload.version;
+    },
+  },
+});
+
+export const { refresh } = layoutSlice.actions;
+
+export const selectVersion = (state: RootState) => state.layout.version;
+
+export default layoutSlice.reducer;

+ 63 - 0
dashboard-v6/src/reducers/session.ts

@@ -0,0 +1,63 @@
+import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
+import * as jose from "jose";
+
+import type { RootState } from "../store";
+
+export const SIGN_IN = "/anonymous/sign-in";
+export const PERSONAL = "/dashboard/personal";
+
+export interface ISignIn {
+  token: string;
+  roles: string[];
+}
+
+const KEY = "token";
+export const get = (): string | null => {
+  return sessionStorage.getItem(KEY);
+};
+
+const set = (token: string) => {
+  sessionStorage.setItem(KEY, token);
+};
+
+const remove = () => {
+  sessionStorage.removeItem(KEY);
+};
+
+interface SessionState {
+  name?: string;
+  roles: string[];
+}
+
+const initialState: SessionState = { roles: [] };
+
+export const sessionSlice = createSlice({
+  name: "session",
+  initialState,
+  reducers: {
+    signOut: (state) => {
+      state.name = undefined;
+      state.roles = [];
+      remove();
+    },
+    signIn: (state, action: PayloadAction<ISignIn>) => {
+      try {
+        const claims = jose.decodeJwt(action.payload.token);
+        if (claims.sub) {
+          state.name = claims.sub;
+          state.roles = action.payload.roles;
+        }
+        set(action.payload.token);
+      } catch (e) {
+        console.error(e);
+        state.name = undefined;
+      }
+    },
+  },
+});
+
+export const { signIn, signOut } = sessionSlice.actions;
+
+export const currentUser = (state: RootState) => state.session.name;
+
+export default sessionSlice.reducer;

+ 98 - 0
dashboard-v6/src/request.ts

@@ -0,0 +1,98 @@
+import { GraphQLError } from "graphql";
+
+import { get as get_token } from "./reducers/session";
+
+export const upload = () => {
+  return {
+    Authorization: `Bearer ${get_token()}`,
+  };
+};
+
+export const options = (method: string): RequestInit => {
+  return {
+    credentials: "include",
+    headers: {
+      Authorization: `Bearer ${get_token()}`,
+      "Content-Type": "application/json; charset=utf-8",
+    },
+    mode: "cors",
+    method,
+  };
+};
+
+export const get = async <R>(path: string): Promise<R> => {
+  const response = await fetch(path, options("GET"));
+  const res: R = await response.json();
+  return res;
+};
+
+export const delete_ = async <R>(path: string): Promise<R> => {
+  const response = await fetch(path, options("DELETE"));
+  const res: R = await response.json();
+  return res;
+};
+
+export interface IGraphqlResponse<R> {
+  data?: R;
+  errors?: GraphQLError[];
+}
+export const graphql = async <Q, R>(
+  query: string,
+  variables: Q
+): Promise<IGraphqlResponse<R>> => {
+  const res: IGraphqlResponse<R> = await post("/graphql", { query, variables });
+  return res;
+};
+
+// https://github.github.io/fetch/#options
+export const post = async <Q, R>(path: string, body: Q): Promise<R> => {
+  const data = options("POST");
+  data.body = JSON.stringify(body);
+  const response = await fetch(path, data);
+  const res: R = await response.json();
+  return res;
+};
+
+export const patch = <Request, Response>(
+  path: string,
+  body: Request
+): Promise<Response> => {
+  const data = options("PATCH");
+  data.body = JSON.stringify(body);
+  return fetch(path, data).then((res) => {
+    if (res.status === 200) {
+      return res.json();
+    }
+    throw res.text();
+  });
+};
+
+export const put = <Request, Response>(
+  path: string,
+  body: Request
+): Promise<Response> => {
+  const data = options("PUT");
+  data.body = JSON.stringify(body);
+  return fetch(path, data).then((res) =>
+    res.status === 200
+      ? res.json()
+      : res.json().then((err) => {
+          throw err;
+        })
+  );
+};
+
+export const download = (path: string, name: string) => {
+  const data = options("GET");
+  fetch(path, data)
+    .then((response) => response.blob())
+    .then((blob) => {
+      const url = window.URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      a.href = url;
+      a.download = name;
+      document.body.appendChild(a); // for firefox
+      a.click();
+      a.remove();
+    });
+};

+ 17 - 0
dashboard-v6/src/store.ts

@@ -0,0 +1,17 @@
+import { configureStore } from "@reduxjs/toolkit";
+
+import layoutReducer from "./reducers/layout";
+import sessionReducer from "./reducers/session";
+
+const store = configureStore({
+  reducer: {
+    layout: layoutReducer,
+    session: sessionReducer,
+  },
+});
+
+export type RootState = ReturnType<typeof store.getState>;
+export type AppDispatch = typeof store.dispatch;
+export type AppStore = typeof store;
+
+export default store;

+ 28 - 0
dashboard-v6/tsconfig.app.json

@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "target": "ES2022",
+    "useDefineForClassFields": true,
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "types": ["vite/client"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "jsx": "react-jsx",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["src"]
+}

+ 7 - 0
dashboard-v6/tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 26 - 0
dashboard-v6/tsconfig.node.json

@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2023",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "types": ["node"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 15 - 0
dashboard-v6/vite.config.ts

@@ -0,0 +1,15 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vite.dev/config/
+export default defineConfig({
+  base: "/pcd-v2026/",
+  server: {
+    host: "127.0.0.1",
+    port: 4000,
+    proxy: {
+      "/api": "http://127.0.0.1:8080",
+    },
+  },
+  plugins: [react()],
+});

+ 4 - 0
deploy/group_vars/all.yml

@@ -3,6 +3,7 @@ ansible_python_interpreter: /usr/bin/python3
 ansible_ssh_private_key_file: "{{ inventory_dir }}/.ssh/id_ed25519"
 
 app_deploy_target: "/srv/{{ app_domain }}/mint-{{ mint_version }}"
+app_build_target: "{{ ansible_env.HOME }}/build/{{ app_domain }}/mint-{{ mint_version }}"
 app_downloads: "{{ ansible_env.HOME }}/downloads"
 app_debug: false
 
@@ -11,6 +12,9 @@ app_postgresql_version: "16"
 app_open_search_version: "2.19.1"
 app_python_version: "3.13"
 app_php_version: "8.1"
+app_php_fpm_port: 9081
 app_php_memory_limit: "128M"
 app_container_prefix: "mint"
 app_consumer_loop_limit_ai_translate: 16
+
+app_docker_image_name: "magnolia"

+ 115 - 0
deploy/mint-deprecated.yml

@@ -0,0 +1,115 @@
+- name: Build frontend v4
+  hosts: 127.0.0.1
+  roles:
+    - mint-dashboard-v4-build
+    - openai-proxy-build
+
+- name: Setup mint repo
+  hosts:
+    - web
+    - fpm
+    - task
+    - fort
+    - ai_translate
+    - openai_proxy
+  roles:
+    - mint-v2.1
+
+- name: Setup postgresql pali t-search
+  hosts:
+    - db
+  tasks:
+    - name: Upload pali.stop
+      become: true
+      ansible.builtin.copy:
+        dest: "/usr/share/postgresql/{{ app_postgresql_version }}/tsearch_data/"
+        src: "{{ playbook_dir }}/tsearch_data/pali.stop"
+        owner: root
+        group: root
+        mode: "0444"
+    - name: Upload pali.syn
+      become: true
+      ansible.builtin.copy:
+        dest: "/usr/share/postgresql/{{ app_postgresql_version }}/tsearch_data/"
+        src: "{{ playbook_dir }}/tsearch_data/pali.syn"
+        owner: root
+        group: root
+        mode: "0444"
+    - name: Restart postgresql server
+      become: true
+      ansible.builtin.systemd:
+        state: restarted
+        name: postgresql
+
+- name: Start mint background workers
+  hosts:
+    - task
+  tasks:
+    - name: Start worker service
+      ansible.builtin.systemd_service:
+        name: "{{ app_container_prefix }}-{{ app_domain }}-worker-mq-{{ item }}"
+        enabled: true
+        state: restarted
+        daemon_reload: true
+        scope: user
+      loop:
+        - discussion
+        - pr
+        - progress
+        - wbw.analyses
+        - export.pali.chapter
+        - export.article
+
+- name: Start mint php-fpm
+  hosts:
+    - fpm
+  tasks:
+    - name: Start fpm service
+      ansible.builtin.systemd_service:
+        name: "{{ app_container_prefix }}-{{ app_domain }}-fpm-{{ app_php_fpm_port }}"
+        enabled: true
+        state: restarted
+        daemon_reload: true
+        scope: user
+
+- name: Start mint ai-translate worker
+  hosts:
+    - fort
+    - ai_translate
+  tasks:
+    - name: Enable ai-translate service
+      ansible.builtin.systemd_service:
+        name: "{{ app_container_prefix }}-{{ app_domain }}-worker-mq-ai.translate"
+        enabled: true
+        state: restarted
+        daemon_reload: true
+        scope: user
+
+- name: Start mint openai-proxy worker
+  hosts:
+    - openai_proxy
+  tasks:
+    - name: Enable openai-proxy service
+      ansible.builtin.systemd_service:
+        name: "{{ app_container_prefix }}-{{ app_domain }}-openai.proxy"
+        enabled: true
+        state: restarted
+        daemon_reload: true
+        scope: user
+
+- name: Setup nginx
+  hosts:
+    - web
+  roles:
+    - mint-nginx
+
+- name: Start single-node worker
+  hosts:
+    - fort
+  tasks:
+    - name: Enable schedule run timer
+      ansible.builtin.systemd_service:
+        name: "{{ app_domain }}-scheduler.timer"
+        state: restarted
+        enabled: true
+        scope: user

+ 20 - 98
deploy/mint.yml

@@ -1,109 +1,31 @@
-- name: Build frontend v4
-  hosts: 127.0.0.1
+- name: Setup local build folder
+  hosts: localhost
+  tasks:
+    - name: Git checkout repo
+      ansible.builtin.git:
+        repo: "https://github.com/iapt-platform/mint.git"
+        dest: "{{ playbook_dir }}/tmp/mint"
+        version: "{{ mint_version }}"
+
+- name: Build on local
+  hosts: localhost
   roles:
     - mint-dashboard-v4-build
+    - mint-openai-proxy-build
+    - clove-build
 
-- name: Setup mint repo
-  hosts:
-    - web
-    - fpm
-    - task
-    - fort
-    - ai_translate
-    - openai_proxy
+- name: Build mint package on remote
+  hosts: building
   roles:
-    - mint-v2.1
-
-- name: Setup postgresql pali t-search
-  hosts:
-    - db
-  tasks:
-    - name: Upload pali.stop
-      become: true
-      ansible.builtin.copy:
-        dest: "/usr/share/postgresql/{{ app_postgresql_version }}/tsearch_data/"
-        src: "{{ playbook_dir }}/tsearch_data/pali.stop"
-        owner: root
-        group: root
-        mode: "0444"
-    - name: Upload pali.syn
-      become: true
-      ansible.builtin.copy:
-        dest: "/usr/share/postgresql/{{ app_postgresql_version }}/tsearch_data/"
-        src: "{{ playbook_dir }}/tsearch_data/pali.syn"
-        owner: root
-        group: root
-        mode: "0444"
-    - name: Restart postgresql server
-      become: true
-      ansible.builtin.systemd:
-        state: restarted
-        name: postgresql
+    - mint-release-build
 
-- name: Start mint background workers
-  hosts:
-    - task
-  tasks:
-    - name: Start worker service
-      ansible.builtin.systemd_service:
-        name: "{{ app_container_prefix }}-{{ app_domain }}-worker-mq-{{ item }}"
-        enabled: true
-        state: restarted
-        scope: user
-      loop:
-        - discussion
-        - pr
-        - progress
-        - wbw.analyses
-        - export.pali.chapter
-        - export.article
-
-- name: Start mint php-fpm
-  hosts:
-    - fpm
-  tasks:
-    - name: Start fpm service
-      ansible.builtin.systemd_service:
-        name: "{{ app_container_prefix }}-{{ app_domain }}-fpm-{{ app_php_fpm_port }}"
-        enabled: true
-        state: restarted
-        scope: user
-
-- name: Start mint ai-translate worker
-  hosts:
-    - ai_translate
-  tasks:
-    - name: Enable php ai-translate service
-      ansible.builtin.systemd_service:
-        name: "{{ app_container_prefix }}-{{ app_domain }}-worker-mq-ai.translate"
-        enabled: true
-        state: started
-        scope: user
-
-- name: Start mint openai-proxy worker
-  hosts:
-    - openai_proxy
-  tasks:
-    - name: Enable openai-proxy service
-      ansible.builtin.systemd_service:
-        name: "{{ app_container_prefix }}-{{ app_domain }}-worker-mq-openai.proxy"
-        enabled: true
-        state: started
-        scope: user
+- name: Setup deploy folder
+  hosts: all:!localhost
+  roles:
+    - mint-v2.2
 
 - name: Setup nginx
   hosts:
     - web
   roles:
     - mint-nginx
-
-- name: Start single-node worker
-  hosts:
-    - fort
-  tasks:
-    - name: Enable schedule run timer
-      ansible.builtin.systemd_service:
-        name: "{{ app_domain }}-scheduler.timer"
-        state: restarted
-        enabled: true
-        scope: user

+ 16 - 0
deploy/roles/clove-build/tasks/main.yml

@@ -0,0 +1,16 @@
+- name: Clone clove project
+  ansible.builtin.git:
+    repo: "https://github.com/iapt-platform/clove.git"
+    dest: "{{ playbook_dir }}/tmp/clove"
+
+- name: Create clove.tar.xz
+  ansible.builtin.shell: |
+    set -e
+    tar cf clove.tar -C {{ playbook_dir }}/tmp/clove \
+      dict_text pali_html pali_sentence pali_similarity pali_title pali_word word_statistics
+    xz -z -F xz -C sha256 --best -T +1 clove.tar
+    exit 0
+  args:
+    executable: /usr/bin/bash
+    chdir: "{{ playbook_dir }}/tmp"
+    creates: clove.tar.xz

+ 7 - 11
deploy/roles/mint-dashboard-v4-build/tasks/main.yml

@@ -1,23 +1,17 @@
-- name: Git checkout repo
-  ansible.builtin.git:
-    repo: "https://github.com/iapt-platform/mint.git"
-    dest: "{{ playbook_dir }}/tmp/mint"
-    version: "{{ mint_version }}"
-
-- name: Extract dashboard node_modules
+- name: Extract dashboard-v4 node_modules
   ansible.builtin.unarchive:
     src: dashboard-20241201115354.tar.xz
     dest: "{{ playbook_dir }}/tmp/mint/dashboard-v4/dashboard"
     creates: "{{ playbook_dir }}/tmp/mint/dashboard-v4/dashboard/node_modules"
 
-- name: Build dashboard dist
+- name: Build dashboard-v4 dist
   ansible.builtin.shell: yarn build
   args:
     chdir: "{{ playbook_dir }}/tmp/mint/dashboard-v4/dashboard"
-    creates: "{{ playbook_dir }}/tmp/mint/dashboard-v4/dashboard/dist-{{ mint_version }}"
+    creates: "dist-{{ app_domain }}-{{ mint_version }}"
   environment:
-    BUILD_PATH: "dist-{{ mint_version }}"
-    NODE_OPTIONS: "--max_old_space_size=4096"
+    BUILD_PATH: "dist-{{ app_domain }}-{{ mint_version }}"
+    NODE_OPTIONS: "--max_old_space_size=5120"
     PUBLIC_URL: "{{ app_dashboard_base_path }}"
     REACT_APP_DEFAULT_LOCALE: "zh-Hans"
     REACT_APP_LANGUAGES: "en-US,zh-Hans,zh-Hant"
@@ -30,3 +24,5 @@
     REACT_APP_ICP_CODE: "{{ app_icp_code }}"
     REACT_APP_MPS_CODE: "{{ app_mps_code }}"
     REACT_APP_QUESTIONNAIRE_LINK: "{{ app_questionnaire_link }}"
+    # https://github.com/iapt-platform/mint/blob/5e81587433455cbc4994da49db5b880cf79e3539/dashboard-v4/dashboard/.env.orig#L17
+    REACT_APP_OPENAI_PROXY: "{{ app_openai_proxy_server }}/api/openai"

+ 16 - 2
deploy/roles/mint-nginx/tasks/main.yml

@@ -1,7 +1,7 @@
 - name: Create nginx logs folder
   become: true
   ansible.builtin.file:
-    path: "{{ app_deploy_target | dirname }}/logs"
+    path: "{{ app_deploy_target | dirname }}/logs/nginx"
     state: directory
     owner: www-data
     mode: "0755"
@@ -10,8 +10,9 @@
   become: true
   ansible.builtin.template:
     src: fpm.conf.j2
-    dest: /etc/nginx/sites-enabled/{{ app_domain }}-fpm.conf
+    dest: /etc/nginx/sites-enabled/php-fpm-{{ app_php_fpm_port }}.conf
     mode: "0644"
+    force: false
 
 - name: Upload nginx.conf
   become: true
@@ -19,3 +20,16 @@
     src: nginx.conf.j2
     dest: /etc/nginx/sites-enabled/{{ app_domain }}.conf
     mode: "0644"
+    force: false
+
+- name: Set document root
+  become: true
+  ansible.builtin.shell: sed -i '5s#.*#root {{ app_deploy_target }}/api-v8/public;#' /etc/nginx/sites-enabled/{{ app_domain }}.conf
+
+- name: Set dashboard alias
+  become: true
+  ansible.builtin.shell: sed -i '25s#.*#alias {{ app_deploy_target }}/dashboard-v4/dashboard/dist/;#' /etc/nginx/sites-enabled/{{ app_domain }}.conf
+
+- name: Reload nginx.conf
+  become: true
+  ansible.builtin.shell: nginx -s reload

+ 6 - 4
deploy/roles/mint-nginx/templates/fpm.conf.j2

@@ -1,7 +1,9 @@
 upstream php_fpm_{{ app_php_fpm_port }} {
-   least_conn;
+   # least_conn;
+   
+   {% for host in groups['php_fpm'] %}
+   server {{ host }}:{{ app_php_fpm_port }} weight=10;
+   {% endfor %}
 
-   # server 192.168.0.11:{{ app_php_fpm_port }} weight=10;   
-
-   server 127.0.0.1:{{ app_php_fpm_port }} backup;
+   # server 127.0.0.1:{{ app_php_fpm_port }} backup;
 }

+ 6 - 7
deploy/roles/mint-nginx/templates/nginx.conf.j2

@@ -1,9 +1,8 @@
 # https://laravel.com/docs/12.x/deployment#nginx
 
 server {
-    listen 80;
     server_name {{ app_domain }};
-    root {{ app_deploy_target | dirname }}/current/api-v8/public;
+    root /bla-bla-bla;
  
     add_header X-Frame-Options "SAMEORIGIN";
     add_header X-Content-Type-Options "nosniff";
@@ -16,14 +15,14 @@ server {
     gzip_types text/plain text/css application/xml application/javascript;
     gzip_vary on;
     client_max_body_size 512M;
-
-    access_log {{ app_deploy_target | dirname }}/logs/access.log;
-    error_log {{ app_deploy_target | dirname }}/logs/error.log warn;
+    
+    access_log {{ app_deploy_target | dirname }}/logs/nginx/access.log;
+    error_log {{ app_deploy_target | dirname }}/logs/nginx/error.log warn;
     # access_log syslog:server=unix:/dev/log,tag={{ app_domain }},nohostname,severity=info combined;
     # error_log  syslog:server=unix:/dev/log,tag={{ app_domain }},nohostname,severity=error;
 
     location {{ app_dashboard_base_path }}/ {
-            alias {{ app_deploy_target | dirname }}/current/dashboard-v4/dashboard/dist/;
+            alias /bla-bla-bla;
             try_files $uri $uri/ {{ app_dashboard_base_path }}/index.html;
 
             location ~* \.(css|js|png|jpg|jpeg|gif|gz|svg|mp4|ogg|ogv|webm|htc|xml|woff)$ {
@@ -47,7 +46,7 @@ server {
         # for nginx v1.18
         # fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
         include fastcgi_params;
-        fastcgi_hide_header X-Powered-By;
+        fastcgi_hide_header X-Powered-By;        
     }
  
     location ~ /\.(?!well-known).* {

+ 11 - 0
deploy/roles/mint-openai-proxy-build/tasks/main.yml

@@ -0,0 +1,11 @@
+- name: Install openai-proxy nodejs dependencies
+  ansible.builtin.shell: npm install --quiet
+  args:
+    chdir: "{{ playbook_dir }}/tmp/mint/open-ai-server"
+    creates: "node_modules"
+
+- name: Build openai-proxy target dist
+  ansible.builtin.shell: npm run build
+  args:
+    chdir: "{{ playbook_dir }}/tmp/mint/open-ai-server"
+    creates: "dist"

+ 11 - 0
deploy/roles/mint-release-build/tasks/ai-translate.yml

@@ -0,0 +1,11 @@
+- name: Install ai-translate dependencies
+  ansible.builtin.shell: |
+    python3 -m venv $PWD/python3
+    source $PWD/python3/bin/activate
+    python3 -m pip install -e .
+    deactivate
+    
+    exit 0
+  args:
+    executable: /bin/bash
+    chdir: "{{ ansible_env.HOME }}/build/mint-{{ mint_version }}/ai-translate"

+ 9 - 0
deploy/roles/mint-release-build/tasks/api-v12.yml

@@ -0,0 +1,9 @@
+- name: Install api v12 dependencies
+  ansible.builtin.shell: |
+    npm install
+    php8.4 /usr/local/bin/composer install
+    
+    exit 0
+  args:
+    executable: /bin/bash
+    chdir: "{{ ansible_env.HOME }}/build/mint-{{ mint_version }}/api-v12"

+ 12 - 0
deploy/roles/mint-release-build/tasks/api-v8.yml

@@ -0,0 +1,12 @@
+- name: Install api v8 dependencies
+  ansible.builtin.shell: |
+    npm install
+    php8.1 /usr/local/bin/composer install
+    cd public
+    npm install
+    php8.1 /usr/local/bin/composer install
+
+    exit 0
+  args:
+    executable: /bin/bash
+    chdir: "{{ ansible_env.HOME }}/build/mint-{{ mint_version }}/api-v8"

+ 45 - 0
deploy/roles/mint-release-build/tasks/main.yml

@@ -0,0 +1,45 @@
+- name: Create build folder
+  ansible.builtin.file:
+    path: "{{ app_build_target | dirname }}"
+    state: directory
+    mode: "0755"
+
+- name: Download source code
+  ansible.builtin.unarchive:
+    src: https://github.com/iapt-platform/mint/archive/{{ mint_version }}.zip
+    dest: "{{ app_build_target | dirname }}"
+    remote_src: true
+    creates: "{{ app_build_target }}"
+
+- name: Upload dashboard-v4 dist
+  ansible.posix.synchronize:
+    src: "{{ playbook_dir }}/tmp/mint/dashboard-v4/dashboard/dist-{{ app_domain }}-{{ mint_version }}/"
+    dest: "{{ app_build_target }}/dashboard-v4/dashboard/dist/"
+
+- name: Upload openai-proxy dist
+  ansible.posix.synchronize:
+    src: "{{ playbook_dir }}/tmp/mint/open-ai-server/dist/"
+    dest: "{{ app_build_target }}/open-ai-server/dist/"
+
+- name: Upload shell.sh
+  ansible.builtin.template:
+    src: shell.sh.j2
+    dest: "{{ app_build_target }}/scripts/shell.sh"
+    mode: "0555"
+
+- name: Upload build.sh
+  ansible.builtin.template:
+    src: build.sh.j2
+    dest: "{{ app_build_target }}/scripts/build.sh"
+    mode: "0555"
+
+- name: Setup mint dependencies and build mint.tar.xz
+  ansible.builtin.shell:
+    cmd: "docker run --rm -it --hostname=mint --network host -v {{ app_build_target }}:{{ app_deploy_target }}:z {{ app_docker_image_name }} /bin/bash -c {{ app_deploy_target }}/scripts/build.sh"
+    creates: "{{ app_build_target }}/tmp/{{ app_domain }}/mint-{{ mint_version }}.tar.xz"
+
+- name: Download mint.tar.xz
+  ansible.posix.synchronize:
+    src: "{{ app_build_target }}/tmp/{{ app_domain }}/mint-{{ mint_version }}.tar.xz"
+    dest: "{{ playbook_dir }}/tmp/{{ app_domain }}-mint-{{ mint_version }}.tar.xz"
+    mode: pull

+ 73 - 0
deploy/roles/mint-release-build/templates/build.sh.j2

@@ -0,0 +1,73 @@
+#!/bin/bash
+
+set -e
+
+source $HOME/.sdkman/bin/sdkman-init.sh
+source $HOME/.nvm/nvm.sh
+
+# cd {{ app_deploy_target }}/dashboard-v4/dashboard/
+# if [ -d dist ]
+# then
+#     rm -f dist
+# fi
+# https://github.com/iapt-platform/mint/blob/5e81587433455cbc4994da49db5b880cf79e3539/dashboard-v4/dashboard/.env.orig#L17
+# NODE_OPTIONS="--max_old_space_size=2048" \
+#     PUBLIC_URL="{{ app_dashboard_base_path }}" \
+#     BUILD_PATH: "dist-{{ app_domain }}-{{ mint_version }}"
+#     REACT_APP_DEFAULT_LOCALE="zh-Hans" \
+#     REACT_APP_LANGUAGES="en-US,zh-Hans,zh-Hant" \
+#     REACT_APP_ENABLE_LOCAL_TOKEN="true" \
+#     REACT_APP_TOKEN_KEY="token.20250320" \
+#     REACT_APP_DOCUMENTS_SERVER="{{ app_documents_server }}" \
+#     REACT_APP_RPC_SERVER="{{ app_grpc_web_server }}" \
+#     REACT_APP_ASSETS_SERVER="{{ app_assets_server }}" \
+#     REACT_APP_API_SERVER="https://{{ app_domain }}" \
+#     REACT_APP_ICP_CODE="{{ app_icp_code }}" \
+#     REACT_APP_MPS_CODE="{{ app_mps_code }}" \
+#     REACT_APP_QUESTIONNAIRE_LINK="{{ app_questionnaire_link }}" \
+#     REACT_APP_OPENAI_PROXY="{{ app_openai_proxy_server }}/api/openai" \
+#     npm run build
+
+cd {{ app_deploy_target }}/api-v8/
+NO_COLOR=true npm install --quiet --omit=dev
+php8.1 /usr/local/bin/composer install --no-ansi --quiet --optimize-autoloader --no-dev
+cd {{ app_deploy_target }}/api-v8/public
+NO_COLOR=true npm install --quiet --omit=dev
+php8.1 /usr/local/bin/composer install --no-ansi --quiet --optimize-autoloader --no-dev
+
+cd {{ app_deploy_target }}/api-v12/
+NO_COLOR=true npm install --quiet --omit=dev
+php8.4 /usr/local/bin/composer install --no-ansi --quiet --optimize-autoloader --no-dev
+
+# cd {{ app_deploy_target }}/open-ai-server/
+# npm install
+# if [ -d dist ]
+# then
+#     rm -f dist
+# fi
+# npm run build
+
+cd {{ app_deploy_target }}/ai-translate/
+if [ -d python3 ]
+then
+    rm -r python3
+fi
+python3.13 -m venv $PWD/python3
+source $PWD/python3/bin/activate
+if [ ! -f get-pip.py ]
+then
+    wget -O get-pip.py https://bootstrap.pypa.io/get-pip.py
+fi
+python get-pip.py
+python -m pip install --quiet -e .
+deactivate
+
+mkdir -p {{ app_deploy_target }}/tmp/{{ app_domain }}
+cd {{ app_deploy_target }}/tmp/{{ app_domain }}/
+tar cf mint-{{ mint_version }}.tar -C {{ app_deploy_target }} \
+    api-v12 api-v8 \
+    dashboard-v4/dashboard/dist open-ai-server/dist \
+    ai-translate/ai_translate ai-translate/pyproject.toml ai-translate/python3
+xz -z -F xz -C sha256 --best -T +1 mint-{{ mint_version }}.tar
+
+exit 0

+ 3 - 0
deploy/roles/mint-release-build/templates/shell.sh.j2

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+docker run --rm -it --hostname=mint --network host -v $HOME/build/mint-{{ mint_version }}:{{ app_deploy_target }}:z {{ app_docker_image_name }}

+ 3 - 3
deploy/roles/mint-v2.1/tasks/ai-translate.yml

@@ -1,14 +1,14 @@
 - name: Upload script for ai.translate
   ansible.builtin.template:
     src: containers/ai-translate.sh.j2
-    dest: "{{ app_deploy_target }}/scripts/worker-mq-ai.translate.sh"
+    dest: "{{ app_deploy_target }}/ai-translate/start.sh"
     mode: "0555"
 
 - name: Upload config for ai.translate
   ansible.builtin.template:
     src: v2/ai-translate.toml.j2
     dest: "{{ app_deploy_target }}/ai-translate/config.toml"
-    mode: "0555"
+    mode: "0444"
 
 - name: Stop ai.translate
   containers.podman.podman_container:
@@ -19,7 +19,7 @@
   containers.podman.podman_container:
     name: "{{ app_domain }}-worker-mq-ai.translate"
     image: "mint-python-3.13"
-    command: "{{ app_deploy_target }}/scripts/worker-mq-ai.translate.sh"
+    command: "{{ app_deploy_target }}/start.sh"
     volumes:
       - "{{ app_deploy_target }}/ai-translate:{{ app_deploy_target }}:z"
     workdir: "{{ app_deploy_target }}"

+ 3 - 3
deploy/roles/mint-v2.1/tasks/clove.yml

@@ -1,4 +1,4 @@
-- name: Clone source codes
+- name: Clone clove source codes
   ansible.builtin.git:
     repo: "https://github.com/iapt-platform/clove.git"
     dest: "{{ app_deploy_target | dirname }}/clove"
@@ -8,7 +8,7 @@
 #     path: "{{ app_deploy_target }}/api-v8/storage/resources"
 #     state: absent
 
-- name: Setup resources for v8
+- name: Setup clove resources for v8
   become: true
   ansible.builtin.file:
     src: "{{ app_deploy_target | dirname }}/clove"
@@ -21,7 +21,7 @@
 #     path: "{{ app_deploy_target }}/api-v12/storage/resources"
 #     state: absent
 
-- name: Setup resources for v12
+- name: Setup clove resources for v12
   become: true
   ansible.builtin.file:
     src: "{{ app_deploy_target | dirname }}/clove"

+ 5 - 0
deploy/roles/mint-v2.1/tasks/laravel-workers.yml

@@ -9,6 +9,11 @@
     name: "{{ app_domain }}-worker-{{ zone_name }}-{{ worker_name }}"
     state: absent
 
+- name: Waiting for write-back
+  ansible.builtin.pause:
+    # minutes: 1
+    seconds: 5
+
 - name: Create {{ zone_name }}-{{ worker_name }}
   containers.podman.podman_container:
     name: "{{ app_domain }}-worker-{{ zone_name }}-{{ worker_name }}"

+ 0 - 3
deploy/roles/mint-v2.1/tasks/laravel.yml

@@ -46,9 +46,6 @@
   loop_control:
     loop_var: worker_name
 
-- name: Setup clove resources
-  ansible.builtin.import_tasks: clove.yml
-
 - name: Setup schedule run
   ansible.builtin.import_tasks: schedule-run.yml
 

+ 11 - 2
deploy/roles/mint-v2.1/tasks/main.yml

@@ -15,6 +15,9 @@
     remote_src: true
     creates: "{{ app_deploy_target }}"
 
+- name: Setup clove resources
+  ansible.builtin.import_tasks: clove.yml
+
 # ---------------------------------------------------------
 
 # - name: Clone source codes directly
@@ -49,13 +52,19 @@
 
 - name: Upload docker shell script
   ansible.builtin.template:
-    src: containers/shell.sh.j2
-    dest: "{{ app_deploy_target }}/scripts/shell.sh"
+    src: containers/api-v8.sh.j2
+    dest: "{{ app_deploy_target }}/scripts/api-v8.sh"
     mode: "0555"
 
 - name: Setup laravel
   ansible.builtin.import_tasks: laravel.yml
 
+- name: Setup ai-translate
+  ansible.builtin.import_tasks: ai-translate.yml
+
+- name: Setup openai-proxy
+  ansible.builtin.import_tasks: openai-proxy.yml
+
 - name: Force systemd to reread configs
   become: true
   ansible.builtin.systemd:

+ 10 - 3
deploy/roles/mint-v2.1/tasks/openai-proxy.yml

@@ -1,14 +1,19 @@
 - name: Upload script for openai-proxy
   ansible.builtin.template:
     src: containers/openai-proxy.sh.j2
-    dest: "{{ app_deploy_target }}/scripts/openai-proxy.sh"
+    dest: "{{ app_deploy_target }}/open-ai-server/start.sh"
     mode: "0555"
 
 - name: Upload config for openai-proxy
   ansible.builtin.template:
     src: v2/openai-proxy.json.j2
     dest: "{{ app_deploy_target }}/open-ai-server/config.json"
-    mode: "0555"
+    mode: "0444"
+
+- name: Upload openai-proxy dist
+  ansible.posix.synchronize:
+    src: "{{ playbook_dir }}/tmp/mint/open-ai-server/dist/"
+    dest: "{{ app_deploy_target }}/open-ai-server/dist/"
 
 - name: Stop openai-proxy server
   containers.podman.podman_container:
@@ -19,9 +24,11 @@
   containers.podman.podman_container:
     name: "{{ app_domain }}-openai.proxy"
     image: "mint-nodejs-jod"
-    command: "{{ app_deploy_target }}/scripts/openai-proxy.sh"
+    command: "{{ app_deploy_target }}/start.sh"
     volumes:
       - "{{ app_deploy_target }}/open-ai-server:{{ app_deploy_target }}:z"
+    ports:
+      - "0.0.0.0:{{ app_openai_proxy_listen_port }}:8080/tcp"
     workdir: "{{ app_deploy_target }}"
     state: present
     auto_remove: true

+ 3 - 3
deploy/roles/mint-v2.1/tasks/schedule-run.yml

@@ -1,10 +1,10 @@
-- name: Setup schedule run worker service
-  ansible.builtin.include_tasks: workers.yml
+- name: Setup laravel schedule run worker service
+  ansible.builtin.include_tasks: laravel-workers.yml
   vars:
     zone_name: schedule
     worker_name: run
 
-- name: Setup schedule run worker timer
+- name: Setup laravel schedule run worker timer
   ansible.builtin.template:
     src: containers/schedule-run.timer.j2
     dest: "{{ ansible_env.HOME }}/.config/systemd/user/{{ app_domain }}-scheduler.timer"

+ 1 - 1
deploy/roles/mint-v2.1/templates/containers/shell.sh.j2 → deploy/roles/mint-v2.1/templates/containers/api-v8.sh.j2

@@ -1,3 +1,3 @@
 #!/bin/sh
 
-podman run --rm -it --events-backend=file --hostname=mint --network host -w {{ app_deploy_target }} -v /srv/{{ app_domain }}/clove:/srv/{{ app_domain }}/clove:z -v {{ app_deploy_target }}:{{ app_deploy_target }}:z {{ app_mint_image_name }} /bin/bash -l
+podman run --rm -it --events-backend=file --hostname=mint --network host -w {{ app_deploy_target }} -v /srv/{{ app_domain }}/clove:/srv/{{ app_domain }}/clove:z -v {{ app_deploy_target }}/api-v8:{{ app_deploy_target }}/api-v8:z {{ app_mint_image_name }} /bin/bash -l

+ 1 - 1
deploy/roles/mint-v2.1/templates/containers/openai-proxy.sh.j2

@@ -8,6 +8,6 @@ export NVM_DIR="$HOME/.nvm"
 source "$NVM_DIR/nvm.sh"
 
 cd $WORK_DIR/
-node dist/main-*.js config.json
+node dist/main.*.js config.json
 
 exit 0

+ 1 - 1
deploy/roles/mint-v2.1/templates/v2/ai-translate.toml.j2

@@ -12,5 +12,5 @@ host = '{{ app_redis_host }}'
 port = {{ app_redis_port }}
 
 [app]
-api-url = '{{ app_openai_proxy_server }}/api'
+api-url = '{{ app_api_server }}/api'
 openai-proxy-url = '{{ app_openai_proxy_server }}/api/openai'

+ 2 - 2
deploy/roles/mint-v2.1/templates/v2/openai-proxy.json.j2

@@ -1,5 +1,5 @@
 { 
-    "port": 4000, 
+    "port": 8080,
     "debug": false, 
-    "api-server": "{{ app_api_server }}" 
+    "api-url": "{{ app_api_server }}/api" 
 }

+ 27 - 0
deploy/roles/mint-v2.2/tasks/clove.yml

@@ -0,0 +1,27 @@
+- name: Create clove folder
+  become: true
+  ansible.builtin.file:
+    path: "{{ app_deploy_target | dirname }}/clove"
+    state: directory
+    owner: "{{ ansible_user }}"
+    mode: "0755"
+
+- name: Upload clove data
+  ansible.builtin.unarchive:
+    src: "{{ playbook_dir }}/tmp/clove.tar.xz"
+    dest: "{{ app_deploy_target | dirname }}/clove"
+    creates: "{{ app_deploy_target | dirname }}/clove/dict_text"
+
+- name: Setup clove resources for v8
+  ansible.builtin.file:
+    src: "{{ app_deploy_target | dirname }}/clove"
+    dest: "{{ app_deploy_target }}/api-v8/storage/resources"
+    state: link
+    force: true
+
+- name: Setup clove resources for v12
+  ansible.builtin.file:
+    src: "{{ app_deploy_target | dirname }}/clove"
+    dest: "{{ app_deploy_target }}/api-v12/storage/resources"
+    state: link
+    force: true

+ 42 - 0
deploy/roles/mint-v2.2/tasks/laravel.yml

@@ -0,0 +1,42 @@
+- name: Upload .env(v2)
+  ansible.builtin.template:
+    src: v2/env.j2
+    dest: "{{ app_deploy_target }}/api-v8/.env"
+    mode: "0444"
+
+- name: Upload config.php(v1)
+  ansible.builtin.template:
+    src: v1/config.php.j2
+    dest: "{{ app_deploy_target }}/api-v8/public/app/config.php"
+    mode: "0444"
+
+- name: Upload config.js(v1)
+  ansible.builtin.template:
+    src: v1/config.js.j2
+    dest: "{{ app_deploy_target }}/api-v8/public/app/config.js"
+    mode: "0444"
+
+- name: Create logs folder
+  become: true
+  ansible.builtin.file:
+    path: "{{ app_deploy_target | dirname }}/logs/laravel-v8"
+    state: directory
+    owner: www-data
+    mode: "0755"
+
+- name: Remove logs folder
+  become: true
+  ansible.builtin.file:
+    path: "{{ app_deploy_target }}/api-v8/storage/logs"
+    state: absent
+
+- name: Link logs folder
+  become: true
+  ansible.builtin.file:
+    src: "{{ app_deploy_target | dirname }}/logs/laravel-v8"
+    dest: "{{ app_deploy_target }}/api-v8/storage/logs"
+    state: link
+
+- name: "Setup configuration"
+  ansible.builtin.shell:
+    cmd: "docker run --rm -it --hostname=mint --network host -v /srv/{{ app_domain }}/clove:/srv/{{ app_domain }}/clove:z -v {{ app_deploy_target }}:{{ app_deploy_target }}:z {{ app_docker_image_name }} {{ app_deploy_target }}/scripts/run.sh setup"

+ 89 - 0
deploy/roles/mint-v2.2/tasks/main.yml

@@ -0,0 +1,89 @@
+- name: Create mint folder
+  become: true
+  ansible.builtin.file:
+    path: "{{ app_deploy_target }}"
+    state: directory
+    owner: "{{ ansible_user }}"
+    mode: "0755"
+
+- name: Upload mint data
+  ansible.builtin.unarchive:
+    src: "{{ playbook_dir }}/tmp/{{ app_domain }}-mint-{{ mint_version }}.tar.xz"
+    dest: "{{ app_deploy_target }}"
+    creates: "{{ app_deploy_target }}/api-v12"
+
+- name: Setup clove
+  ansible.builtin.import_tasks: clove.yml
+
+# -----------------------------------------------------------------------------
+
+- name: Create scripts folder
+  ansible.builtin.file:
+    path: "{{ app_deploy_target }}/scripts"
+    state: directory
+    mode: "0755"
+
+- name: Upload setup.sh
+  ansible.builtin.template:
+    src: containers/shell.sh.j2
+    dest: "{{ app_deploy_target }}/scripts/shell.sh"
+    mode: "0555"
+
+- name: Upload run.sh
+  ansible.builtin.template:
+    src: containers/run.sh.j2
+    dest: "{{ app_deploy_target }}/scripts/run.sh"
+    mode: "0555"
+
+- name: Upload php-fpm.sh
+  become: true
+  ansible.builtin.template:
+    src: containers/php-fpm.sh.j2
+    dest: "/srv/php-fpm-{{ app_php_version }}.sh"
+    mode: "0555"
+# -----------------------------------------------------------------------------
+
+- name: Setup laravel
+  ansible.builtin.import_tasks: laravel.yml
+
+# -----------------------------------------------------------------------------
+
+- name: Upload config for ai.translate
+  ansible.builtin.template:
+    src: v2/ai-translate.toml.j2
+    dest: "{{ app_deploy_target }}/ai-translate/config.toml"
+    mode: "0444"
+
+- name: Upload config for openai-proxy
+  ansible.builtin.template:
+    src: v2/openai-proxy.json.j2
+    dest: "{{ app_deploy_target }}/open-ai-server/config.json"
+    mode: "0444"
+
+# -----------------------------------------------------------------------------
+
+- name: Upload version.txt(api-v8)
+  ansible.builtin.template:
+    src: version.txt.j2
+    dest: "{{ app_deploy_target }}/api-v8/public/version.txt"
+    mode: "0555"
+
+- name: Upload version.txt(api-v12)
+  ansible.builtin.template:
+    src: version.txt.j2
+    dest: "{{ app_deploy_target }}/api-v12/public/version.txt"
+    mode: "0555"
+
+- name: Upload version.txt(dashboard-v4)
+  ansible.builtin.template:
+    src: version.txt.j2
+    dest: "{{ app_deploy_target }}/dashboard-v4/dashboard/dist/version.txt"
+    mode: "0555"
+
+# -----------------------------------------------------------------------------
+
+- name: Create a current link
+  ansible.builtin.file:
+    src: "{{ app_deploy_target }}"
+    dest: "{{ app_deploy_target | dirname }}/current"
+    state: link

+ 4 - 0
deploy/roles/mint-v2.2/templates/containers/php-fpm.sh.j2

@@ -0,0 +1,4 @@
+#!/bin/sh
+
+docker run --rm -d --network host -v /srv:/srv:z {{ app_docker_image_name }} {{ app_deploy_target }}/scripts/run.sh php-fpm
+   

+ 52 - 0
deploy/roles/mint-v2.2/templates/containers/run.sh.j2

@@ -0,0 +1,52 @@
+#!/bin/bash
+
+set -e
+
+if [ "$#" -ne 1 ]; then
+    echo "USAGE: $0 COMMAND"
+    exit 1
+fi
+
+export WORK_DIR="{{ app_deploy_target }}"
+
+if [[ "$1" == "mq:discussion" || "$1" == "mq:pr" || "$1" == "mq:progress" || "$1" == "mq:wbw.analyses" || "$1" == "mq:export.pali.chapter" || "$1" == "mq:export.article" || "$1" == "schedule:run" ]]; then
+    cd $WORK_DIR/api-v8/    
+    php -d memory_limit={{ app_php_memory_limit }} artisan $1
+elif [ "$1" = "ai-translate" ]; then
+    cd $WORK_DIR/ai-translate/
+    source $PWD/python3/bin/activate
+    # https://github.com/iapt-platform/mint/blob/df8e1cf7ade16d17add360e7a869540c1ddaf1b9/api-v8/config/mint.php#L129
+    python3 -m ai_translate -c config.toml -n ai.translate -q ai_translate_v2
+elif [ "$1" = "openai-proxy" ]; then
+    cd $WORK_DIR/openai-proxy/
+    source "$HOME/.nvm/nvm.sh"    
+    node dist/main.*.js config.json
+elif [ "$1" = "php-fpm" ]; then
+    cd /var/lib/php/
+    sed -i 's/^listen = .*/listen = {{ app_php_fpm_port }}/g' /etc/php/{{ app_php_version }}/fpm/pool.d/www.conf
+    sed -i 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 4/g' /etc/php/{{ app_php_version }}/fpm/pool.d/www.conf
+    sed -i 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 12/g' /etc/php/{{ app_php_version }}/fpm/pool.d/www.conf
+    sed -i 's/^pm.max_children = .*/pm.max_children = 20/g' /etc/php/{{ app_php_version }}/fpm/pool.d/www.conf
+    sed -i 's/^pm.start_servers = .*/pm.start_servers = 8/g' /etc/php/{{ app_php_version }}/fpm/pool.d/www.conf
+    /usr/sbin/php-fpm{{ app_php_version }} --nodaemonize --fpm-config /etc/php/{{ app_php_version }}/fpm/php-fpm.conf
+elif [ "$1" = "setup" ]; then
+    cd $WORK_DIR/api-v8/
+    echo "caching configuration "
+    php artisan config:cache
+    echo "caching events"
+    php artisan event:cache
+    echo "caching routes"
+    php artisan route:cache
+    echo "caching views"
+    php artisan view:cache
+
+    echo "check file permissions"
+    cd $WORK_DIR/api-v8/
+    chown -R www-data:www-data bootstrap/cache storage
+else
+    echo "unknown command $1"
+    exit 1
+fi
+
+echo 'done.'
+exit 0

+ 10 - 0
deploy/roles/mint-v2.2/templates/containers/schedule-run.timer.j2

@@ -0,0 +1,10 @@
+[Unit]
+Description=Runs Mint {{ app_domain }} scheduler every minute
+
+[Timer]
+OnBootSec=15minutes
+OnUnitActiveSec=1minute
+Unit={{ app_container_prefix }}-{{ app_domain }}-worker-schedule-run.service
+
+[Install]
+WantedBy=timers.target

+ 6 - 0
deploy/roles/mint-v2.2/templates/containers/shell.sh.j2

@@ -0,0 +1,6 @@
+#!/bin/sh
+
+docker run --rm -it --network host \
+    -v /srv/{{ app_deploy_target | dirname }}/clove:/srv/{{ app_deploy_target | dirname }}/clove:z \
+    -v {{ app_deploy_target }}:{{ app_deploy_target }}:z \
+    {{ app_docker_image_name }} 

+ 18 - 0
deploy/roles/mint-v2.2/templates/v1/config.js.j2

@@ -0,0 +1,18 @@
+var ICP_CODE = "{{ app_icp_code }}"
+var GRPC_WEB_SERVER = "{{ app_grpc_web_server }}";
+var DOCUMENTS_SERVER = "{{ app_documents_server }}";
+var REACT_APP_QUESTIONNAIRE_LINK = "{{ app_questionnaire_link }}";
+var DASHBOARD_BASE_PATH = "{{ app_dashboard_base_path }}";
+
+/*
+  |---------------
+  |网站资源文件,非用户的图片,音频,视频
+  |---------------
+  |对应/public/tmp/ 目录 开发线可以设置为 http://127.0.0.1:8000/tmp
+  |所有文件存储在 https://drive.google.com/drive/folders/1-4dn4juD-0-lsKndDui2W9nT9wcS_Y33?usp=sharing
+  |开发线可自行下载放到/public/tmp/
+  |或直接引用离您最近的assets server
+  |------------------------
+*/
+
+var ASSETS_SERVER = "{{ app_assets_server }}";

+ 67 - 0
deploy/roles/mint-v2.2/templates/v1/config.php.j2

@@ -0,0 +1,67 @@
+<?php
+
+#域名设置
+define("GRPC_WEB_SERVER","{{ app_grpc_web_server}}");
+define("ASSETS_SERVER","{{ app_assets_server }}");
+define("DOCUMENTS_SERVER","{{ app_documents_server }}");
+define('APP_KEY','{{ app_secret_key }}');
+define('APP_ENV','{{ app_env }}');
+define('CORS_ALLOWED_ORIGINS', '{{ app_cors_allowed_origins }}');
+define('DASHBOARD_BASE_PATH', "{{ app_dashboard_base_path }}");
+
+/*
+电子邮件设置
+PHPMailer
+*/
+define("Email", [
+                                 "Host"=>"{{ app_smtp_host }}",//Set the SMTP server to send through
+                                 "SMTPAuth"=>true,//Enable SMTP authentication
+                                 "Username"=>'{{ app_smtp_user }}',//SMTP username
+                                 "Password"=>'{{ app_smtp_password }}',//SMTP password
+                                 "Port"=>{{ app_smtp_port }},//TCP port to connect to 465; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`
+                                 "From"=>"{{ app_smtp_user }}",
+                                 "Sender"=>"webmaster"
+                                 ]);
+
+/*
+数据库设置
+*/
+define("Database",[
+        "type"=>"pgsql",
+        "server"=>"{{ app_postgresql_host }}",
+        "port"=>{{ app_postgresql_port }},
+        "name"=>"{{ app_postgresql_dbname }}",
+        "sslmode" => "disable",
+        "user" => "{{ app_postgresql_user }}",
+        "password" => "{{ app_postgresql_password }}"
+]);
+
+
+/*
+Redis 设置,
+*/
+define("Redis",[        
+        "host" => "{{ app_redis_host }}",
+        "port" => {{ app_redis_port }},
+        "password" => "",
+        "prefix"=>"{{ app_domain }}://v1/"
+]);
+
+
+# 雪花id
+define("SnowFlake",[
+        "DatacenterId"=>{{ app_snowflake_data_center_id }},
+        "WorkerId"=>{{ app_snowflake_worker_id }}
+]);
+
+#目录设置,不能更改
+require_once __DIR__."/config.dir.php";
+
+/*
+数据表
+*/
+#表设置,此行不能更改
+require_once __DIR__."/config.table.php";
+
+
+?>

+ 16 - 0
deploy/roles/mint-v2.2/templates/v2/ai-translate.toml.j2

@@ -0,0 +1,16 @@
+[rabbitmq]
+host = '{{ app_rabbitmq_host }}'
+port = {{ app_rabbitmq_port }}
+user = '{{ app_rabbitmq_user }}'
+password = '{{ app_rabbitmq_password }}'
+virtual-host = '{{ app_rabbitmq_virtual_host }}'
+customer-timeout = 3600
+
+[redis]
+namespace = '{{ app_domain }}://'
+host = '{{ app_redis_host }}'
+port = {{ app_redis_port }}
+
+[app]
+api-url = '{{ app_api_server }}/api'
+openai-proxy-url = '{{ app_openai_proxy_server }}/api/openai'

+ 97 - 0
deploy/roles/mint-v2.2/templates/v2/env.j2

@@ -0,0 +1,97 @@
+BASE_DIR="{{ app_deploy_target }}/.env.global"
+CACHE_DIR="${BASE_DIR}/cache"
+TMP_DIR="${BASE_DIR}/tmp"
+
+APP_NAME="wikipali"
+APP_ENV={{ app_env }}
+APP_KEY={{ app_secret_key }}
+APP_DEBUG={{ app_debug }}
+APP_URL="https://{{ app_domain }}"
+
+LOG_CHANNEL=daily
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=pgsql
+DB_HOST={{ app_postgresql_host }}
+DB_PORT={{ app_postgresql_port }}
+DB_DATABASE={{ app_postgresql_dbname }}
+DB_USERNAME={{ app_postgresql_user }}
+DB_PASSWORD="{{ app_postgresql_password }}"
+
+BROADCAST_DRIVER=log
+CACHE_DRIVER=redis
+FILESYSTEM_DRIVER=s3
+QUEUE_CONNECTION=sync
+SESSION_DRIVER=cookie
+SESSION_LIFETIME=120
+
+AWS_ACCESS_KEY_ID={{ app_aws_access_key }}
+AWS_SECRET_ACCESS_KEY={{ app_aws_secret_key }}
+AWS_DEFAULT_REGION={{ app_aws_region }}
+AWS_BUCKET=assets
+AWS_URL=
+AWS_ENDPOINT=https://{{ app_aws_endpoint_domain }}
+
+REDIS_HOST={{ app_redis_host }}
+REDIS_PORT={{ app_redis_port }}
+REDIS_PASSWORD=
+REDIS_DB=0
+REDIS_PREFIX="{{ app_domain }}://v20250323/"
+
+REDIS_CACHE_HOST={{ app_redis_cache_host }}
+REDIS_CACHE_PORT={{ app_redis_cache_port }}
+REDIS_CACHE_PASSWORD=
+REDIS_CACHE_DB={{ app_redis_cache_db }}
+REDIS_CACHE_PREFIX="{{ app_domain }}://v20250323/"
+
+
+MAIL_MAILER=smtp
+MAIL_HOST={{ app_smtp_host }}
+MAIL_PORT={{ app_smtp_port }}
+MAIL_USERNAME="{{ app_smtp_user }}"
+MAIL_PASSWORD="{{ app_smtp_password }}"
+MAIL_ENCRYPTION=ssl
+MAIL_FROM_ADDRESS="{{ app_smtp_user }}"
+MAIL_FROM_NAME="wikipali"
+
+PUSHER_APP_ID=
+PUSHER_APP_KEY=
+PUSHER_APP_SECRET=
+PUSHER_APP_CLUSTER=mt1
+
+MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
+MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
+
+DASHBOARD_BASE_PATH="{{ app_dashboard_base_path }}"
+ASSETS_SERVER="{{ app_assets_server }}"
+GRPC_WEB_SERVER="{{ app_grpc_web_server }}"
+DOCUMENTS_SERVER="{{ app_documents_server }}"
+CORS_ALLOWED_ORIGINS="{{ app_cors_allowed_origins }}"
+
+MORUS_GRPC_HOST="{{ app_morus_grpc_host }}"
+MORUS_GRPC_PORT="{{ app_morus_listen_port }}"
+LILY_GRPC_HOST="{{ app_lily_grpc_host }}"
+LILY_GRPC_PORT="{{ app_lily_listen_port }}"
+TULIP_GRPC_HOST="{{ app_tulip_grpc_host }}"
+TULIP_GRPC_PORT="{{ app_tulip_listen_port }}"
+
+SNOWFLAKE_DATA_CENTER_ID={{ app_snowflake_data_center_id }}
+SNOWFLAKE_WORKER_ID={{ app_snowflake_worker_id }}
+
+RABBITMQ_HOST="{{ app_rabbitmq_host }}"
+RABBITMQ_PORT={{ app_rabbitmq_port }}
+RABBITMQ_VIRTUAL_HOST="{{ app_rabbitmq_virtual_host }}"
+RABBITMQ_USER="{{ app_rabbitmq_user }}"
+RABBITMQ_PASSWORD="{{ app_rabbitmq_password }}"
+
+CDN_URLS={{ app_cdn_urls }}
+ATTACHMENTS_TEMPORARY_BUCKET_NAME={{ app_attachments_bucket_name }}-t
+ATTACHMENTS_PERMANTENT_BUCKET_NAME={{ app_attachments_bucket_name }}-p
+
+FTP_HOST="{{ app_ftp_host }}"
+
+APP_ICP_CODE="{{ app_icp_code }}"
+APP_MPS_CODE="{{ app_mps_code }}"
+
+MQ_LOOP_LIMIT_AI_TRANSLATE="{{ app_consumer_loop_limit_ai_translate }}"

+ 5 - 0
deploy/roles/mint-v2.2/templates/v2/openai-proxy.json.j2

@@ -0,0 +1,5 @@
+{ 
+    "port": 8080,
+    "debug": false, 
+    "api-url": "{{ app_api_server }}/api" 
+}

+ 2 - 0
deploy/roles/mint-v2.2/templates/version.txt.j2

@@ -0,0 +1,2 @@
+git version: {{ mint_version }}
+deployed at: {{ ansible_date_time.iso8601 }}

+ 1 - 0
docker/.gitignore

@@ -1,3 +1,4 @@
 *.tar
 *.tar.??
 *.md5
+.tmp-*

+ 8 - 6
docker/mint/Dockerfile

@@ -1,9 +1,9 @@
 FROM ubuntu:latest
 LABEL maintainer="Jeremy Zheng"
 
-ENV DEBIAN_FRONTEND noninteractive
+ENV DEBIAN_FRONTEND="noninteractive"
 # https://launchpad.net/~ondrej/+archive/ubuntu/php
-ARG PHP_VERSION=8.4
+ARG PHP_VERSION=8.5
 
 RUN apt update
 RUN apt -y install lsb-release apt-utils \
@@ -26,9 +26,9 @@ RUN apt -y install git vim locales locales-all tzdata build-essential \
     fonts-tibetan-machine fonts-ddc-uchen fonts-monlam fonts-sambhota-tsugring fonts-sambhota-yigchung \
     imagemagick ffmpeg graphviz pandoc texlive-full \
     python3-full python3-dev \
-    libssl-dev libpq-dev libmysqlclient-dev
-RUN apt -y autoremove
-RUN apt -y clean
+    libssl-dev libpq-dev libmysqlclient-dev \
+    && apt -y autoremove \
+    && apt -y clean
 
 RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
 RUN locale-gen
@@ -42,7 +42,7 @@ RUN wget -q -O $HOME/downloads/composer https://getcomposer.org/installer
 RUN cd $HOME/downloads && php${PHP_VERSION} composer && cp composer.phar /usr/local/bin/composer
 
 # https://github.com/nvm-sh/nvm
-ENV NVM_VERSION "v0.40.3"
+ENV NVM_VERSION="v0.40.3"
 RUN git clone -b ${NVM_VERSION} https://github.com/nvm-sh/nvm.git $HOME/.nvm
 RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $HOME/.bashrc
 RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $HOME/.bashrc
@@ -52,6 +52,8 @@ RUN bash -i -c "npm install -g yarn"
 
 RUN echo "$(date -u +%4Y%m%d%H%M%S)" | tee /VERSION
 
+EXPOSE 9000/tcp 8080/tcp
+
 VOLUME /srv
 WORKDIR /srv
 

+ 12 - 4
docker/mint/README.md

@@ -2,17 +2,25 @@
 
 ## Usage
 
+- Building
+
 ```bash
 cd ~/workspace/mint/docker
 ./build.sh PHP_VERSION
 ```
 
+- Load image
+
+  ```bash
+  docker load -i mint-php8.4-x86_64.tar
+  ```
+
 - Laravel & PHP compatibility
 
-| Laravel | PHP       | Security Fixes Until |
-| ------- | --------- | -------------------- |
-| 8       | 7.3 - 8.1 | Jan, 2023            |
-| 12      | 8.2 - 8.4 | Feb, 2027            |
+| Laravel                                      | PHP       | Security Fixes Until |
+| -------------------------------------------- | --------- | -------------------- |
+| [8](https://laravel.com/docs/8.x/releases)   | 7.3 - 8.1 | Jan, 2023            |
+| [12](https://laravel.com/docs/12.x/releases) | 8.2 - 8.5 | Feb, 2027            |
 
 ## Documents
 

+ 7 - 7
docker/mint/build.sh

@@ -3,18 +3,18 @@
 set -e
 
 # https://laravel.com/docs/master/releases
-if [ "$#" -ne 1 ]; then
-    echo "USAGE: $0 PHP_VERSION"
+if [ "$#" -ne 2 ]; then
+    echo "USAGE: $0 PHP_VERSION ARCH"
     exit 1
 fi
 
 export VERSION=$(date "+%4Y%m%d%H%M%S")
-export CODE="mint-php$1-$(uname -m)"
-export TAR="$CODE-$VERSION"
+export CODE="mint-php$1"
+export TAR="$CODE-$(uname -m)-$VERSION"
 
-podman pull ubuntu:latest
-podman build --build-arg PHP_VERSION=$1 -t $CODE .
-podman save --format=oci-archive -o $TAR.tar $CODE
+docker pull ubuntu:latest
+docker build --platform=linux/$2 --provenance false --build-arg PHP_VERSION=$1 -t $CODE .
+docker save -o $TAR.tar $CODE:latest
 md5sum $TAR.tar >$TAR.md5
 
 echo "done($TAR.tar)."

+ 58 - 0
docker/python/Dockerfile

@@ -0,0 +1,58 @@
+FROM ubuntu:latest
+LABEL maintainer="Jeremy Zheng"
+
+ENV DEBIAN_FRONTEND="noninteractive"
+# https://launchpad.net/%7Edeadsnakes/+archive/ubuntu/ppa
+ARG PYTHON_VERSION=3.12
+
+RUN apt update
+RUN apt -y install lsb-release apt-utils \
+    debian-keyring debian-archive-keyring apt-transport-https software-properties-common curl wget gnupg
+RUN add-apt-repository -y ppa:deadsnakes/ppa
+RUN apt -y upgrade
+RUN apt -y install git vim locales locales-all tzdata build-essential \
+    fonts-dejavu-extra fonts-opensymbol fonts-lxgw-wenkai fonts-smiley-sans \
+    fonts-noto-extra fonts-noto-cjk-extra fonts-noto-color-emoji \
+    fonts-arphic-ukai fonts-arphic-uming \
+    fonts-wqy-microhei fonts-wqy-zenhei \
+    fonts-cns11643-kai fonts-cns11643-sung \
+    fonts-moe-standard-kai fonts-moe-standard-song \
+    fonts-ipaexfont fonts-ipafont fonts-konatu fonts-ipafont-nonfree-jisx0208 fonts-ipafont-nonfree-uigothic \
+    fonts-mikachan \
+    fonts-tibetan-machine fonts-ddc-uchen fonts-monlam fonts-sambhota-tsugring fonts-sambhota-yigchung \
+    imagemagick ffmpeg graphviz pandoc texlive-full \
+    python${PYTHON_VERSION}-full python${PYTHON_VERSION}-dev \
+    libssl-dev libpq-dev libmysqlclient-dev \
+    && apt -y autoremove \
+    && apt -y clean
+
+RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
+RUN locale-gen
+RUN update-locale LANG=en_US.UTF-8
+RUN update-alternatives --set editor /usr/bin/vim.basic
+
+RUN mkdir -p $HOME/downloads $HOME/build $HOME/local $HOME/tmp
+
+# https://pip.pypa.io/en/stable/installation/
+RUN sh -c ". $HOME/.profile \
+    && python3 -m venv $HOME/local/python3 \
+    && . $HOME/local/python3/bin/activate \
+    && pip install --upgrade pip"
+RUN echo '. $HOME/local/python3/bin/activate' >> $HOME/.bashrc
+
+# https://github.com/nvm-sh/nvm
+ENV NVM_VERSION="v0.40.3"
+RUN git clone -b ${NVM_VERSION} https://github.com/nvm-sh/nvm.git $HOME/.nvm
+RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $HOME/.bashrc
+RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $HOME/.bashrc
+RUN echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"' >> $HOME/.bashrc
+RUN bash -i -c "nvm install --lts"
+
+RUN echo "$(date -u +%4Y%m%d%H%M%S)" | tee /VERSION
+
+EXPOSE 9000/tcp 8080/tcp
+
+VOLUME /srv
+WORKDIR /srv
+
+CMD ["/bin/bash", "-l"]

+ 16 - 0
docker/python/README.md

@@ -0,0 +1,16 @@
+# USAGE
+
+## Usage
+
+- Building
+
+```bash
+cd ~/workspace/mint/docker
+./build.sh PYTHON_VERSION
+```
+
+- Load image
+
+  ```bash
+  docker load -i mint-python3.12-x86_64.tar
+  ```

+ 22 - 0
docker/python/build.sh

@@ -0,0 +1,22 @@
+#!/bin/bash
+
+set -e
+
+# https://laravel.com/docs/master/releases
+if [ "$#" -ne 2 ]; then
+    echo "USAGE: $0 PYTHON_VERSION ARCH"
+    exit 1
+fi
+
+export VERSION=$(date "+%4Y%m%d%H%M%S")
+export CODE="mint-python$1"
+export TAR="$CODE-$(uname -m)-$VERSION"
+
+docker pull ubuntu:latest
+docker build --platform=linux/$2 --provenance false --build-arg PYTHON_VERSION=$1 -t $CODE .
+# docker save -o $TAR.tar $CODE:latest
+# md5sum $TAR.tar >$TAR.md5
+
+echo "done($TAR.tar)."
+
+exit 0

+ 128 - 91
docker/spring/Dockerfile

@@ -1,32 +1,47 @@
 FROM archlinux:latest
 LABEL maintainer="Jeremy Zheng"
 
-RUN pacman-key --init
-RUN pacman -Sy --noconfirm archlinux-keyring
-RUN pacman-key --populate
-RUN pacman -Syu --noconfirm
-RUN pacman -S --needed --noconfirm base-devel aarch64-linux-gnu-gcc risc-v \
-    bzip2 bzip3 p7zip unarchiver \
-    vim git cmake thrift capnproto wget zsh man-db man-pages \
-    pwgen sshpass openssl openssh rsync zip unzip tree tmux asciidoc doxygen cpio net-tools bind-tools \
-    clang llvm lld lldb mold bison flex ninja bazel \
-    imagemagick ffmpeg xorg-font-util \
-    ansible ansible-lint \
-    wqy-bitmapfont wqy-microhei wqy-zenhei \
-    ttf-fira-code woff-fira-code woff2-fira-code \
-    adobe-source-code-pro-fonts adobe-source-serif-fonts adobe-source-sans-fonts \
-    adobe-source-han-serif-cn-fonts adobe-source-han-serif-hk-fonts adobe-source-han-serif-jp-fonts adobe-source-han-serif-kr-fonts adobe-source-han-serif-tw-fonts \
-    adobe-source-han-sans-cn-fonts adobe-source-han-sans-hk-fonts adobe-source-han-sans-jp-fonts adobe-source-han-sans-kr-fonts adobe-source-han-sans-tw-fonts \
-    ttf-dejavu nerd-fonts ttf-ubuntu-font-family \
-    ttf-arphic-ukai ttf-arphic-uming \
-    texlive texlive-lang graphviz pandoc \
-    hspell nuspell libvoikko hunspell hunspell-en_us \
-    xdebug php-fpm php-pgsql php-sqlite php-redis php-mongodb php-imagick php-gd php-intl php-enchant php-snmp php-tidy php-xsl php-sodium php-odbc php-pspell \
-    dart haxe lua \
-    erlang elixir rebar \
-    jdk-openjdk libxcrypt-compat \
-    nginx mariadb postgresql rabbitmq redis supervisor
-RUN pacman -Scc --noconfirm
+RUN pacman-key --init \
+    && pacman -Sy --noconfirm archlinux-keyring \
+    && pacman-key --populate \
+    && pacman -Syu --noconfirm
+
+RUN pacman bzip2 bzip3 p7zip unarchiver \
+        git subversion screen vim wget zsh man-db man-pages \
+        pwgen sshpass openssl openssh rsync zip unzip tree tmux asciidoc doxygen cpio net-tools bind-tools \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman imagemagick ffmpeg xorg-font-util \
+        pandoc graphviz \
+        hspell nuspell libvoikko hunspell hunspell-en_us \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman base-devel clang llvm lld lldb mold bison flex ninja bazel \
+        cmake thrift capnproto grpc flatbuffers \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman aarch64-linux-gnu-gcc risc-v \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman xdebug php-fpm php-pgsql php-sqlite php-redis php-mongodb php-imagick php-gd php-intl php-enchant php-snmp php-tidy php-xsl php-sodium php-odbc \
+        libxcrypt-compat \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman dart haxe lua erlang elixir jdk-openjdk \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman nginx mariadb postgresql rabbitmq redis supervisor \
+        ansible ansible-lint \
+    -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman wqy-bitmapfont wqy-microhei wqy-zenhei \
+        ttf-arphic-ukai ttf-arphic-uming \
+        ttf-fira-code woff-fira-code woff2-fira-code \
+        ttf-dejavu ttf-ubuntu-font-family \
+        adobe-source-code-pro-fonts adobe-source-serif-fonts adobe-source-sans-fonts \
+        adobe-source-han-serif-cn-fonts adobe-source-han-serif-hk-fonts adobe-source-han-serif-jp-fonts adobe-source-han-serif-kr-fonts adobe-source-han-serif-tw-fonts \
+        adobe-source-han-sans-cn-fonts adobe-source-han-sans-hk-fonts adobe-source-han-sans-jp-fonts adobe-source-han-sans-kr-fonts adobe-source-han-sans-tw-fonts \
+     -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman texlive -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman texlive-lang -S --needed --noconfirm && pacman -Scc --noconfirm
+RUN pacman nerd-fonts -S --needed --noconfirm && pacman -Scc --noconfirm
+
+
+# RUN pacman -S --needed --noconfirm && pacman -Scc --noconfirm
+
 
 RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
 RUN useradd -s /bin/zsh -m deploy
@@ -36,14 +51,14 @@ USER deploy
 RUN mkdir -p $HOME/downloads $HOME/build $HOME/local $HOME/tmp
 
 # https://pkl-lang.org/main/current/pkl-cli/index.html#linux-executable
-ENV PKL_VERSION="0.25.1"
+ENV PKL_VERSION="0.30.2"
 RUN curl -L -o $HOME/downloads/pkl https://github.com/apple/pkl/releases/download/$PKL_VERSION/pkl-linux-amd64 \
     && sudo cp $HOME/downloads/pkl /usr/local/bin/ \
     && sudo chmod +x /usr/local/bin/pkl
 
 
 # https://github.com/envoyproxy/envoy
-ENV ENVOY_VERSION "1.33.0"
+ENV ENVOY_VERSION="1.36.4"
 RUN wget -q -O $HOME/downloads/envoy https://github.com/envoyproxy/envoy/releases/download/v${ENVOY_VERSION}/envoy-${ENVOY_VERSION}-linux-x86_64 \
     && sudo cp $HOME/downloads/envoy /usr/local/bin/ \
     && sudo chmod +x /usr/local/bin/envoy
@@ -56,16 +71,19 @@ RUN cd $HOME/downloads \
 
 
 # https://github.com/bazelbuild/bazelisk
-ENV BAZEL_VERSION "v1.25.0"
+ENV BAZEL_VERSION="v1.27.0"
 RUN wget -q -O $HOME/downloads/bazel \
     https://github.com/bazelbuild/bazelisk/releases/download/${BAZEL_VERSION}/bazelisk-linux-amd64
 RUN sudo cp $HOME/downloads/bazel /usr/local/bin && sudo chmod +x /usr/local/bin/bazel
 
 
 # https://min.io/download#/linux
-RUN wget -q -O $HOME/downloads/minio https://dl.min.io/server/minio/release/linux-amd64/minio \
+RUN wget -q -P $HOME/downloads/ https://dl.min.io/aistor/minio/release/linux-amd64/minio \
+    && chmod +x $HOME/downloads/minio \
     && sudo cp $HOME/downloads/minio /usr/local/bin/ \
-    && sudo chmod +x /usr/local/bin/minio
+    && wget -q -P $HOME/downloads/ https://dl.min.io/aistor/mc/release/linux-amd64/mc \
+    && chmod +x $HOME/downloads/mc \
+    && sudo cp $HOME/downloads/mc /usr/local/bin/
 
 # https://github.com/amacneil/dbmate
 RUN curl -fsSL -o $HOME/downloads/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 \
@@ -73,7 +91,7 @@ RUN curl -fsSL -o $HOME/downloads/dbmate https://github.com/amacneil/dbmate/rele
     && sudo chmod +x /usr/local/bin/dbmate
 
 # https://aur.archlinux.org/packages/ttf-arphic-tex-extra
-ENV ARPHIC_TTF_VERSION "20161212-1"
+ENV ARPHIC_TTF_VERSION="20161212-1"
 RUN git clone https://aur.archlinux.org/ttf-arphic-tex-extra.git $HOME/downloads/ttf-arphic-tex-extra \
     && cd $HOME/downloads/ttf-arphic-tex-extra/ \
     && makepkg \
@@ -89,25 +107,26 @@ RUN echo 'export LANG=en_US.UTF-8' >> $HOME/.zshrc \
 
 RUN git config --global core.quotepath false \
     && git config --global http.version HTTP/1.1 \
-    && git config --global pull.rebase false \
+    && git config --global pull.rebase true \
     && git config --global url."https://".insteadOf git://
 RUN echo 'set-option -g history-limit 102400' > $HOME/.tmux.conf \
     && echo 'set-option -g default-shell "/bin/zsh"' >> $HOME/.tmux.conf
 
-# https://musl.cc/
-RUN wget -q -P $HOME/downloads https://more.musl.cc/x86_64-linux-musl/x86_64-linux-musl-cross.tgz \
-    && tar xf $HOME/downloads/x86_64-linux-musl-cross.tgz -C $HOME/local \
+# https://github.com/cross-tools/musl-cross
+RUN git clone https://github.com/cross-tools/musl-cross.git $HOME/build/musl-cross
+ENV MUSL_TOOLCHAIN_VERSION=20250929
+RUN wget -q -P $HOME/downloads https://github.com/cross-tools/musl-cross/releases/download/${MUSL_TOOLCHAIN_VERSION}/x86_64-unknown-linux-musl.tar.xz \
+    && tar xf $HOME/downloads/x86_64-unknown-linux-musl.tar.xz -C $HOME/local \
+    && echo 'export PATH=$HOME/local/x86_64-unknown-linux-musl/bin:$PATH' >> $HOME/.zshrc
+RUN wget -q -P $HOME/downloads https://github.com/cross-tools/musl-cross/releases/download/${MUSL_TOOLCHAIN_VERSION}/aarch64-unknown-linux-musl.tar.xz \
+    && tar xf $HOME/downloads/aarch64-unknown-linux-musl.tar.xz -C $HOME/local \
+    && echo 'export PATH=$HOME/local/aarch64-unknown-linux-musl/bin:$PATH' >> $HOME/.zshrc
+RUN wget -q -P $HOME/downloads https://github.com/cross-tools/musl-cross/releases/download/${MUSL_TOOLCHAIN_VERSION}/riscv64-unknown-linux-musl.tar.xz \
+    && tar xf $HOME/downloads/riscv64-unknown-linux-musl.tar.xz -C $HOME/local \
     && echo 'export PATH=$HOME/local/x86_64-linux-musl-cross/bin:$PATH' >> $HOME/.zshrc
-RUN wget -q -P $HOME/downloads https://more.musl.cc/x86_64-linux-musl/armv7l-linux-musleabihf-cross.tgz \
-    && tar xf $HOME/downloads/armv7l-linux-musleabihf-cross.tgz -C $HOME/local \
-    && echo 'export PATH=$HOME/local/armv7l-linux-musleabihf-cross/bin:$PATH' >> $HOME/.zshrc
-RUN wget -q -P $HOME/downloads https://more.musl.cc/x86_64-linux-musl/aarch64-linux-musl-cross.tgz \
-    && tar xf $HOME/downloads/aarch64-linux-musl-cross.tgz -C $HOME/local \
-    && echo 'export PATH=$HOME/local/aarch64-linux-musl-cross/bin:$PATH' >> $HOME/.zshrc
-RUN wget -q -P $HOME/downloads https://more.musl.cc/x86_64-linux-musl/riscv64-linux-musl-cross.tgz \
-    && tar xf $HOME/downloads/riscv64-linux-musl-cross.tgz -C $HOME/local \
-    && echo 'export PATH=$HOME/local/riscv64-linux-musl-cross/bin:$PATH' >> $HOME/.zshrc
-
+RUN wget -q -P $HOME/downloads https://github.com/cross-tools/musl-cross/releases/download/${MUSL_TOOLCHAIN_VERSION}/loongarch64-unknown-linux-musl.tar.xz \
+    && tar xf $HOME/downloads/loongarch64-unknown-linux-musl.tar.xz -C $HOME/local \
+    && echo 'export PATH=$HOME/local/loongarch64-unknown-linux-musl/bin:$PATH' >> $HOME/.zshrc
 
 # https://pip.pypa.io/en/stable/installation/
 RUN zsh -c ". $HOME/.zshrc \
@@ -117,13 +136,14 @@ RUN zsh -c ". $HOME/.zshrc \
     && pip install ansible paramiko conan \
     && pip install grpcio-tools grpcio-health-checking"
 RUN echo 'source $HOME/local/python3/bin/activate' >> $HOME/.zshrc
+RUN zsh -c ". $HOME/.zshrc && ansible-galaxy collection install ansible.posix"
 RUN echo 'export ANSIBLE_HOST_KEY_CHECKING=False' >> $HOME/.zshrc \
     && echo 'alias peony="ANSIBLE_LOG_PATH=$HOME/tmp/$(date +%Y%m%d%H%M%S).log ansible-playbook"' >> $HOME/.zshrc
 
 
 # https://github.com/rbenv/rbenv
 # https://github.com/rbenv/ruby-build/tree/master/share/ruby-build
-ENV RUBY_VERSION "3.4.1"
+ENV RUBY_VERSION="4.0.0"
 RUN git clone https://github.com/rbenv/rbenv.git $HOME/.rbenv \
     && git clone https://github.com/rbenv/ruby-build.git $HOME/.rbenv/plugins/ruby-build \
     && git clone https://github.com/rbenv/rbenv-vars.git $HOME/.rbenv/plugins/rbenv-vars 
@@ -135,12 +155,12 @@ RUN zsh -c "source $HOME/.zshrc \
     && gem install bundler"
 
 # https://go.dev/doc/install
-ENV GO_VERSION "1.23.5"
+ENV GO_VERSION="1.25.5"
 RUN wget -q -P $HOME/downloads https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
 RUN tar xf $HOME/downloads/go${GO_VERSION}.linux-amd64.tar.gz -C $HOME/local
 RUN echo 'export PATH=$HOME/local/go/bin:$PATH' >> $HOME/.zshrc \
     && echo 'export GOPATH=$HOME/go' >> $HOME/.zshrc \
-    && echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> $HOME/.zshrc
+    && echo 'export PATH=$(go env GOPATH)/bin:$PATH' >> $HOME/.zshrc
 # https://code.visualstudio.com/docs/languages/go
 RUN zsh -c "source $HOME/.zshrc \
     && go install golang.org/x/tools/gopls@latest \
@@ -149,25 +169,35 @@ RUN zsh -c "source $HOME/.zshrc \
 
 # https://github.com/sdkman/sdkman-cli
 # https://docs.gradle.org/current/userguide/compatibility.html
-ENV JDK_VERSION "23.0.1-open"
-ENV GRADLE_VERSION "8.12.1"
-ENV THRIFT_JAVA_VERSION "19.0.2-open"
-ENV THRIFT_GRADLE_VERSION "7.6.4"
+# https://thrift.apache.org/lib/java.html
+ENV JDK_VERSION="25.0.1-amzn"
+ENV GRADLE_VERSION="9.2.1"
 RUN curl -s "https://get.sdkman.io" | bash
 RUN sed -i -e 's/sdkman_auto_answer=false/sdkman_auto_answer=true/g' $HOME/.sdkman/etc/config
 RUN zsh -c "source $HOME/.zshrc \
     && sdk install java ${JDK_VERSION} \
-    && sdk install ant \
-    && sdk install maven \
     && sdk install gradle ${GRADLE_VERSION} \
+    && sdk install maven \
     && sdk install kotlin \
-    && sdk install java  ${THRIFT_JAVA_VERSION} \
-    && sdk install gradle ${THRIFT_GRADLE_VERSION} \
     && sdk default java ${JDK_VERSION} \
     && sdk default gradle ${GRADLE_VERSION}"
 
+# https://asdf-vm.com/guide/getting-started.html
+RUN zsh -c "source $HOME/.zshrc && go install github.com/asdf-vm/asdf/cmd/asdf@v0.18.0" \
+    && echo 'export PATH=${ASDF_DATA_DIR:-$HOME/.asdf}/shims:$PATH' >> $HOME/.zshrc
+# asdf list all XXX
+ENV ERLANG_VERSION="28.3"
+ENV ELIXIR_VERSION="1.19.4"
+RUN zsh -c "source $HOME/.zshrc \
+    && asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git \
+    && asdf install erlang ${ERLANG_VERSION} \
+    && asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git \
+    && asdf install elixir ${ELIXIR_VERSION}"
+RUN echo "export HEX_HTTP_CONCURRENCY=1" >> $HOME/.zshrc \
+    && echo "export HEX_HTTP_TIMEOUT=120" >> $HOME/.zshrc
+
 # https://github.com/nvm-sh/nvm
-ENV NVM_VERSION "v0.40.1"
+ENV NVM_VERSION="v0.40.3"
 RUN git clone -b ${NVM_VERSION} https://github.com/nvm-sh/nvm.git $HOME/.nvm
 RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $HOME/.zshrc \
     && echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $HOME/.zshrc \
@@ -197,13 +227,17 @@ RUN zsh -c "source $HOME/.cargo/env \
 RUN zsh -c "source $HOME/.zshrc \
     && cargo install --locked cargo-outdated \
     && cargo install diesel_cli \
-    && cargo install mdbook"
+    && cargo install mdbook \
+    && cargo install cross"
 
-# https://www.swift.org/download/#releases
-ENV SWIFT_VERSION "6.0.3"
-RUN wget -q -P $HOME/downloads https://download.swift.org/swift-${SWIFT_VERSION}-release/ubuntu2404/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04.tar.gz \
-    && tar xf $HOME/downloads/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04.tar.gz -C $HOME/local \
-    && echo "export PATH=\$HOME/local/swift-${SWIFT_VERSION}-RELEASE-ubuntu24.04/bin:\$PATH" >> $HOME/.zshrc
+# FIXME Unable to find release information from file /etc/os-release
+# https://www.swift.org/install/linux/
+# RUN wget -q -P $HOME/downloads/ https://download.swift.org/swiftly/linux/swiftly-x86_64.tar.gz \
+#     && mkdir -p $HOME/local/swift \
+#     && tar xf $HOME/downloads/swiftly-x86_64.tar.gz -C $HOME/local/swift \
+#     && cd $HOME/local/swift/ \
+#     && ./swiftly init --quiet-shell-followup -y \
+#     && echo '. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"' >> $HOME/.zshrc
 
 
 # https://xmake.io/#/guide/installation?id=via-curl
@@ -215,38 +249,37 @@ RUN $HOME/local/vcpkg/bootstrap-vcpkg.sh \
     && echo 'export VCPKG_DISABLE_METRICS=1' >> $HOME/.zshrc
 
 # https://opensearch.org/downloads.html#opensearch
-ENV OPENSEARCH_VERSION "2.18.0"
+ENV OPENSEARCH_VERSION="3.4.0"
 RUN wget -q -P $HOME/downloads \
     https://artifacts.opensearch.org/releases/bundle/opensearch/${OPENSEARCH_VERSION}/opensearch-${OPENSEARCH_VERSION}-linux-x64.tar.gz
 RUN tar xf $HOME/downloads/opensearch-${OPENSEARCH_VERSION}-linux-x64.tar.gz -C $HOME/tmp \
     && sudo mv $HOME/tmp/opensearch-${OPENSEARCH_VERSION} /opt/opensearch
 
 # https://github.com/grpc/grpc
-ENV GRPC_VERSION "v1.70.0"
-RUN git clone --recurse-submodules -b $GRPC_VERSION https://github.com/grpc/grpc.git $HOME/downloads/grpc
+ENV GRPC_VERSION="v1.76.0"
+RUN git clone -b $GRPC_VERSION https://github.com/grpc/grpc.git $HOME/downloads/grpc
+RUN cd $HOME/downloads/grpc/ && git submodule update --init --recursive
 # ENV PROTOBUF_VERSION "v3.21.8"
 # RUN cd $HOME/downloads/grpc/third_party/protobuf \
 #     && git checkout ${PROTOBUF_VERSION} \
 #     && git submodule update --init --recursive
 RUN zsh -c "source $HOME/.zshrc \
     && mkdir -pv $HOME/build/grpc \
-    && cd $HOME/build/grpc \
-    && CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=Release \
-    -DgRPC_INSTALL=ON \
-    -DgRPC_SSL_PROVIDER=package \
-    -DgRPC_BUILD_TESTS=OFF \
-    -DCMAKE_INSTALL_PREFIX=$HOME/.local $HOME/downloads/grpc \
-    && make -j $(nproc --ignore=2) \
-    && make install"
+    && CC=clang CXX=clang++ cmake -DCMAKE_BUILD_TYPE=Release -G Ninja \
+        -DgRPC_INSTALL=ON -DgRPC_SSL_PROVIDER=package -DgRPC_BUILD_TESTS=OFF \
+        -DCMAKE_INSTALL_PREFIX=$HOME/.local -B $HOME/build/grpc -S $HOME/downloads/grpc \
+    && cmake --build $HOME/build/grpc \
+    && cmake --install $HOME/build/grpc \
+    && rm -r $HOME/build/grpc"
 
 # https://github.com/grpc/grpc-web#code-generator-plugin
-ENV GRPC_WEB_PLUGIN_VERSION "1.5.0"
+ENV GRPC_WEB_PLUGIN_VERSION="2.0.2"
 RUN wget -q -O $HOME/.local/bin/protoc-gen-grpc-web \
     https://github.com/grpc/grpc-web/releases/download/${GRPC_WEB_PLUGIN_VERSION}/protoc-gen-grpc-web-${GRPC_WEB_PLUGIN_VERSION}-linux-x86_64
 RUN chmod +x $HOME/.local/bin/protoc-gen-grpc-web
 
 # https://github.com/protocolbuffers/protobuf-javascript
-ENV GRPC_JS_PLUGIN_VERSION "3.21.4"
+ENV GRPC_JS_PLUGIN_VERSION="4.0.1"
 RUN wget -q -P $HOME/downloads \
     https://github.com/protocolbuffers/protobuf-javascript/releases/download/v${GRPC_JS_PLUGIN_VERSION}/protobuf-javascript-${GRPC_JS_PLUGIN_VERSION}-linux-x86_64.tar.gz
 RUN mkdir -p $HOME/build/protobuf-javascript \
@@ -255,7 +288,7 @@ RUN mkdir -p $HOME/build/protobuf-javascript \
 
 # https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/
 # https://github.com/grpc/grpc-java
-ENV GRPC_JAVA_PLUGIN_VERSION "1.70.1"
+ENV GRPC_JAVA_PLUGIN_VERSION="1.78.0"
 RUN wget -q -O $HOME/.local/bin/grpc_java_plugin \
     https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/${GRPC_JAVA_PLUGIN_VERSION}/protoc-gen-grpc-java-${GRPC_JAVA_PLUGIN_VERSION}-linux-x86_64.exe
 RUN chmod +x $HOME/.local/bin/grpc_java_plugin
@@ -274,22 +307,23 @@ RUN zsh -c "source $HOME/.zshrc \
     go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest"
 
 # https://github.com/google/flatbuffers
-ENV FLATBUFFERS_VERSION "v25.2.10"
+ENV FLATBUFFERS_VERSION="v25.12.19"
 RUN git clone -b $FLATBUFFERS_VERSION https://github.com/google/flatbuffers.git $HOME/downloads/flatbuffers
 RUN zsh -c "source $HOME/.zshrc \
     && mkdir -pv $HOME/build/flatbuffers \
-    && cd $HOME/build/flatbuffers \
-    && CC=clang CXX=clang++ cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release -DFLATBUFFERS_BUILD_TESTS=OFF \
-    -DCMAKE_INSTALL_PREFIX=$HOME/.local $HOME/downloads/flatbuffers \
-    && make -j $(nproc --ignore=2) \
-    && make install"
-
+    && CC=clang CXX=clang++ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
+        -DFLATBUFFERS_BUILD_TESTS=OFF \
+        -DCMAKE_INSTALL_PREFIX=$HOME/.local -B $HOME/build/flatbuffers -S $HOME/downloads/flatbuffers \
+    && cmake --build $HOME/build/flatbuffers \
+    && cmake --install $HOME/build/flatbuffers \
+    && rm -r $HOME/build/flatbuffers"
 
 # https://opensearch.org/docs/latest/opensearch/install/tar/
 RUN echo "network.host: 0.0.0.0" >> /opt/opensearch/config/opensearch.yml \
     && echo "discovery.type: single-node" >> /opt/opensearch/config/opensearch.yml \
-    && echo "plugins.security.disabled: true" >> /opt/opensearch/config/opensearch.yml \
-    && sudo chown -R nobody:nobody /opt/opensearch
+    && echo "plugins.security.disabled: true" >> /opt/opensearch/config/opensearch.yml
+# https://github.com/infinilabs/analysis-stconvert
+RUN cd /opt/opensearch/ && bin/opensearch-plugin install https://get.infini.cloud/opensearch/analysis-stconvert/${OPENSEARCH_VERSION}
 
 RUN sudo mkdir -p /var/lib/minio/data && sudo chown -R nobody:nobody /var/lib/minio
 
@@ -297,7 +331,9 @@ RUN sudo mkdir -p /var/lib/minio/data && sudo chown -R nobody:nobody /var/lib/mi
 RUN echo 'loopback_users = none' | sudo tee -a /etc/rabbitmq/rabbitmq.conf \
     && sudo sed -i "s/NODENAME.*/NODENAME=palm/g" /etc/rabbitmq/rabbitmq-env.conf \
     && sudo rabbitmq-plugins enable rabbitmq_mqtt \
-    && sudo rabbitmq-plugins enable rabbitmq_management
+    && sudo rabbitmq-plugins enable rabbitmq_management \
+    && sudo rabbitmq-plugins enable rabbitmq_stream \
+    && sudo rabbitmq-plugins enable rabbitmq_shovel rabbitmq_shovel_management
 
 RUN cd /usr && sudo mariadb-install-db --datadir=/var/lib/mysql --user=mysql
 
@@ -310,13 +346,14 @@ RUN echo "listen_addresses = '0.0.0.0'" | sudo tee -a /var/lib/postgres/data/pos
 ADD etc/redis/* /etc/redis/
 RUN sudo mkdir -p /var/lib/redis \
     && sudo mkdir /var/lib/redis/single /var/lib/redis/node-1 /var/lib/redis/node-2 /var/lib/redis/node-3 /var/lib/redis/node-4 /var/lib/redis/node-5 /var/lib/redis/node-6 \
-    && sudo chown -R redis:redis /var/lib/redis \
+    && sudo chown -R valkey:valkey /var/lib/redis \
     && sudo chmod 750 /var/lib/redis
 
 RUN sudo mkdir -p /var/www/html \
     && echo "<?php phpinfo(); ?>" | sudo tee /var/www/html/info.php \
+    && sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig \
     && echo "daemon off;" | sudo tee -a /etc/nginx/nginx.conf \
-    && sudo sed -i '35,79d' /etc/nginx/nginx.conf \
+    && sudo sed -i '35,82d' /etc/nginx/nginx.conf \
     && sudo sed -i '35i include sites-enabled/*.conf;' /etc/nginx/nginx.conf \
     && sudo sed -i "s/error_log =.*/error_log = log\/php-fpm.log/g" /etc/php/php-fpm.conf
 ADD etc/nginx/sites-enabled/localhost.conf /etc/nginx/sites-enabled/localhost.conf
@@ -343,7 +380,6 @@ RUN sudo sed -i 's/memory_limit =.*/memory_limit = 1G/' /etc/php/php.ini \
     && sudo sed -i 's/;extension=pdo_pgsql/extension=pdo_pgsql/' /etc/php/php.ini \
     && sudo sed -i 's/;extension=pdo_sqlite/extension=pdo_sqlite/' /etc/php/php.ini \
     && sudo sed -i 's/;extension=pgsql/extension=pgsql/' /etc/php/php.ini \
-    && sudo sed -i 's/;extension=pspell/extension=pspell/' /etc/php/php.ini \
     && sudo sed -i 's/;extension=snmp/extension=snmp/' /etc/php/php.ini \
     && sudo sed -i 's/;extension=soap/extension=soap/' /etc/php/php.ini \
     && sudo sed -i 's/;extension=sockets/extension=sockets/' /etc/php/php.ini \
@@ -376,6 +412,7 @@ RUN mkdir -p $HOME/.ssh \
 ADD etc/envoy.yaml /etc/
 RUN sudo mkdir -p /var/log/envoy
 ADD etc/supervisor /etc/supervisor
+ADD opt/bin /etc/bin
 
 RUN echo "$(date -u +%4Y%m%d%H%M%S)" | sudo tee /VERSION
 

+ 1 - 0
magnolia/.gitignore

@@ -0,0 +1 @@
+/clients/

+ 2 - 0
magnolia/docker/.gitignore

@@ -0,0 +1,2 @@
+*.tar
+*.md5

+ 71 - 0
magnolia/docker/Dockerfile

@@ -0,0 +1,71 @@
+FROM ubuntu:latest
+LABEL maintainer="Kassapa"
+
+ENV DEBIAN_FRONTEND="noninteractive"
+
+RUN apt update
+RUN apt -y upgrade
+RUN apt install -y lsb-release apt-utils \
+    debian-keyring debian-archive-keyring apt-transport-https software-properties-common curl wget gnupg
+RUN add-apt-repository -y ppa:ondrej/php
+# https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa
+RUN add-apt-repository -y ppa:deadsnakes/ppa
+# https://launchpad.net/~ondrej/+archive/ubuntu/php
+RUN apt install -y zip unzip vim locales locales-all rsync openssh-client telnet sshpass pwgen screen \
+    tzdata tree asciidoc doxygen imagemagick ffmpeg fonts-dejavu-extra texlive-full \
+    git cmake build-essential ninja-build \
+    redis-tools postgresql-client mariadb-client \
+    php8.1-fpm php8.1-mbstring php8.1-xml php8.1-bcmath php8.1-curl php8.1-zip \
+    php8.1-mysql php8.1-pgsql php8.1-sqlite3 php8.1-redis php8.1-mongodb php8.1-amqp php8.1-zmq \
+    php8.1-imagick php8.1-gd php8.1-intl php8.1-soap \
+    php8.4-fpm php8.4-mbstring php8.4-xml php8.4-bcmath php8.4-curl php8.4-zip \
+    php8.4-mysql php8.4-pgsql php8.4-sqlite3 php8.4-redis php8.4-mongodb php8.4-amqp php8.4-zmq \
+    php8.4-imagick php8.4-gd php8.4-intl php8.4-soap \
+    python3.13-venv python3.13-dev
+RUN apt clean
+RUN apt autoremove
+
+RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
+RUN locale-gen
+RUN update-alternatives --set editor /usr/bin/vim.basic
+RUN python3.13 -m ensurepip --upgrade
+
+# RUN useradd -s /bin/bash -m deploy
+# RUN passwd -l deploy
+# RUN echo 'deploy ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/101-deploy
+# USER deploy
+
+RUN mkdir $HOME/local $HOME/tmp $HOME/downloads
+
+# https://github.com/sdkman/sdkman-cli
+# https://docs.gradle.org/current/userguide/compatibility.html
+ENV JDK_VERSION="21.0.2-open"
+RUN curl -s "https://get.sdkman.io" | bash
+RUN sed -i -e 's/sdkman_auto_answer=false/sdkman_auto_answer=true/g' $HOME/.sdkman/etc/config
+RUN bash -c ". $HOME/.sdkman/bin/sdkman-init.sh \
+    && sdk install java ${JDK_VERSION} \
+    && sdk install maven \
+    && sdk install gradle \
+    && sdk install kotlin"
+
+# https://github.com/nvm-sh/nvm
+ENV NVM_VERSION="v0.40.3"
+RUN git clone -b ${NVM_VERSION} https://github.com/nvm-sh/nvm.git $HOME/.nvm
+RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $HOME/.bashrc 
+RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $HOME/.bashrc 
+RUN echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"' >> $HOME/.bashrc
+RUN bash -c ". $HOME/.nvm/nvm.sh \
+    && nvm install --lts \
+    && npm install -g yarn"
+
+# https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos
+RUN wget -q -O $HOME/downloads/composer https://getcomposer.org/installer
+RUN cd $HOME/downloads && php composer && mv composer.phar /usr/local/bin/composer
+
+RUN echo "$(date -u +%4Y%m%d%H%M%S)" | tee /VERSION
+
+VOLUME /srv
+WORKDIR /srv
+
+CMD ["/bin/bash", "-l"]
+

+ 25 - 0
magnolia/docker/README.md

@@ -0,0 +1,25 @@
+# Usage
+
+## Setup
+
+```bash
+# archlinux
+sudo pacman -S buildkit docker-buildx
+# ubuntu
+sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+
+sudo gpasswd -a xxx docker
+
+docker image load -i xxx.tar
+docker image ls --tree
+
+```
+
+## Documents
+
+### Laravel-PHP Compatibility
+
+| Laravel                                                     | PHP |
+| ----------------------------------------------------------- | --- |
+| [8](https://laravel.com/docs/10.x/releases#support-policy)  | 8.1 |
+| [12](https://laravel.com/docs/12.x/releases#support-policy) | 8.4 |

+ 16 - 0
magnolia/docker/build.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+
+set -e
+
+export VERSION=$(date "+%4Y%m%d%H%M%S")
+export CODE="magnolia"
+export TAR="$CODE-$(uname -m)-$(date +"%Y%m%d%H%M%S")"
+
+docker pull ubuntu:latest
+DOCKER_BUILDKIT=1 docker build -t $CODE .
+docker save -o $TAR.tar $CODE
+md5sum $TAR.tar >$TAR.md5
+
+echo "done($TAR.tar)."
+
+exit 0

+ 5 - 0
magnolia/docker/start.sh

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+export CODE="magnolia"
+
+docker run --rm -it --hostname=mint --network host -v $PWD:/srv/:z $CODE

+ 7 - 0
magnolia/ec2.yml

@@ -0,0 +1,7 @@
+- name: Testing ssh connections
+  hosts: all,!localhost
+  roles:
+    - os
+    - ubuntu
+    - docker
+    - random-passwords

+ 3 - 0
magnolia/group_vars/all.yml

@@ -0,0 +1,3 @@
+ansible_user: "deploy"
+ansible_python_interpreter: /usr/bin/python3
+ansible_ssh_private_key_file: "{{ inventory_dir }}/.ssh/id_ed25519"

+ 4 - 0
magnolia/open-search.yml

@@ -0,0 +1,4 @@
+- name: OpenSearch node
+  hosts: all
+  roles:
+    - opensearch

+ 9 - 0
magnolia/ping.yml

@@ -0,0 +1,9 @@
+- name: Ping
+  hosts: all
+  tasks:
+    - name: Test ssh connection
+      ansible.builtin.ping:
+
+    - name: Show facts available on the system
+      ansible.builtin.debug:
+        var: ansible_facts

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor