This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Build and publish Docker image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.botsu.cloud
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.botsu.cloud/${{ github.repository }}:latest
|
||||||
|
git.botsu.cloud/${{ github.repository }}:${{ github.sha }}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1/healthz || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Botsu Cloud
|
||||||
|
|
||||||
|
Petit portail React pour les liens `botsu.cloud`, compile en statique puis servi par nginx.
|
||||||
|
|
||||||
|
## Lancer en local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build statique
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Le resultat est dans `dist/`.
|
||||||
|
|
||||||
|
## Docker nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t git.botsu.cloud/koka/botsu.cloud:latest .
|
||||||
|
docker run --rm -p 8087:80 git.botsu.cloud/koka/botsu.cloud:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis ouvre `http://localhost:8087`.
|
||||||
|
|
||||||
|
## Compose / Watchtower
|
||||||
|
|
||||||
|
Un exemple est fourni dans `deploy/docker-compose.yml`. Ajuste `IMAGE_NAME` si ton namespace Gitea n'est pas `koka`, puis lance :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
IMAGE_NAME=git.botsu.cloud/koka/botsu.cloud:latest docker compose -f deploy/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
L'image expose nginx sur le port interne `80`. Le port hôte `127.0.0.1:8087` est volontairement simple à reverse-proxy depuis ton nginx principal.
|
||||||
|
|
||||||
|
## Reverse proxy nginx
|
||||||
|
|
||||||
|
Un exemple de vhost est dans `deploy/nginx-reverse-proxy.conf`. Il proxy `botsu.cloud` vers `127.0.0.1:8087`.
|
||||||
|
|
||||||
|
## Gitea Actions
|
||||||
|
|
||||||
|
Le workflow `.gitea/workflows/docker-publish.yml` construit et pousse l'image sur `git.botsu.cloud/${repository}:latest` à chaque push sur `main`.
|
||||||
|
|
||||||
|
Secrets à créer dans le dépôt Gitea :
|
||||||
|
|
||||||
|
```text
|
||||||
|
REGISTRY_USER
|
||||||
|
REGISTRY_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Police
|
||||||
|
|
||||||
|
La police embarquée est Velvelyne, fournie localement dans `src/assets/fonts/`.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
botsu-cloud:
|
||||||
|
image: ${IMAGE_NAME:-git.botsu.cloud/koka/botsu.cloud:latest}
|
||||||
|
container_name: botsu-cloud
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8087:80"
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.enable=true"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name botsu.cloud www.botsu.cloud;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8087;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
ecmaFeatures: { jsx: true },
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Portail Botsu Cloud pour acceder aux services d'administration, monitoring, git, wiki et outils IA."
|
||||||
|
/>
|
||||||
|
<title>Botsu Cloud</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location = /healthz {
|
||||||
|
access_log off;
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
return 200 "ok\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|ico|svg|webp|woff2?)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+3218
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "botsu-cloud",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --host 0.0.0.0",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"typescript-eslint": "^8.18.2",
|
||||||
|
"vite": "^6.0.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
import logoUrl from './assets/botsu6.png';
|
||||||
|
|
||||||
|
type ServiceLink = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceLinks: ServiceLink[] = [
|
||||||
|
{
|
||||||
|
label: 'Administration Proxmox',
|
||||||
|
href: 'https://admin.botsu.cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monitoring',
|
||||||
|
href: 'https://monitoring.botsu.cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uptime',
|
||||||
|
href: 'https://uptime.botsu.cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Git',
|
||||||
|
href: 'https://git.botsu.cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Wiki Perso',
|
||||||
|
href: 'https://wiki.botsu.cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Génération d'image",
|
||||||
|
href: 'https://comfyui.botsu.cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Génération de texte',
|
||||||
|
href: 'https://codex.botsu.cloud',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function ArrowIcon() {
|
||||||
|
return (
|
||||||
|
<svg className="arrow" viewBox="0 0 28 18" aria-hidden="true" focusable="false">
|
||||||
|
<path
|
||||||
|
d="M1.5 9H25M18 2L25 9L18 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<main className="page-shell">
|
||||||
|
<header className="site-header" aria-label="Botsu Cloud">
|
||||||
|
<img className="site-logo" src={logoUrl} alt="Botsu Cloud" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section className="intro" aria-labelledby="page-title">
|
||||||
|
<h1 id="page-title">botsu.cloud</h1>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section aria-label="Services Botsu Cloud">
|
||||||
|
<ul className="service-list">
|
||||||
|
{serviceLinks.map((link) => (
|
||||||
|
<li key={link.href}>
|
||||||
|
<a href={link.href}>
|
||||||
|
<span>{link.label}</span>
|
||||||
|
<ArrowIcon />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
+184
@@ -0,0 +1,184 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Velvelyne";
|
||||||
|
src: url("./assets/fonts/Velvelyne-Light.otf") format("opentype");
|
||||||
|
font-display: swap;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Velvelyne";
|
||||||
|
src: url("./assets/fonts/Velvelyne-Book.otf") format("opentype");
|
||||||
|
font-display: swap;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Velvelyne";
|
||||||
|
src: url("./assets/fonts/Velvelyne-Regular.otf") format("opentype");
|
||||||
|
font-display: swap;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Velvelyne";
|
||||||
|
src: url("./assets/fonts/Velvelyne-Bold.otf") format("opentype");
|
||||||
|
font-display: swap;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color: #070707;
|
||||||
|
background: #ffffff;
|
||||||
|
font-family: "Velvelyne", Arial, Helvetica, sans-serif;
|
||||||
|
font-synthesis: none;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-shell {
|
||||||
|
width: min(100%, 920px);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(12px, 2vw, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 104px;
|
||||||
|
padding: 0 0 clamp(6px, 1vw, 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-logo {
|
||||||
|
width: clamp(86px, 10vw, 118px);
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
margin-bottom: clamp(12px, 2vw, 18px);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #121212;
|
||||||
|
font-size: clamp(3.1rem, 9vw, 6.3rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
border-top: 1px solid #d9decc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list li {
|
||||||
|
border-bottom: 1px solid #ecefe5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list a {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 8px 0;
|
||||||
|
outline-offset: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list a > span {
|
||||||
|
max-width: calc(100% - 72px);
|
||||||
|
text-align: center;
|
||||||
|
color: #070707;
|
||||||
|
font-size: clamp(1.14rem, 2.3vw, 1.65rem);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list a:hover > span,
|
||||||
|
.service-list a:focus-visible > span {
|
||||||
|
color: #5ea800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
width: clamp(18px, 2.6vw, 24px);
|
||||||
|
height: auto;
|
||||||
|
color: #91ff00;
|
||||||
|
display: block;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.page-shell {
|
||||||
|
padding: 12px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
min-height: 96px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list a {
|
||||||
|
min-height: 50px;
|
||||||
|
padding-block: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.page-shell {
|
||||||
|
padding-inline: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list a > span {
|
||||||
|
max-width: calc(100% - 58px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user