Initial Botsu Cloud portal
Build and publish Docker image / docker (push) Waiting to run

This commit is contained in:
cnstiout
2026-06-30 17:25:18 +02:00
commit 1cbeb4a0f9
24 changed files with 3777 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
.git
.gitea
.DS_Store
npm-debug.log*
+30
View File
@@ -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 }}
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
.DS_Store
*.log
+20
View File
@@ -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;"]
+56
View File
@@ -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/`.
+9
View File
@@ -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"
+12
View File
@@ -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;
}
}
+31
View File
@@ -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
View File
@@ -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>
+28
View File
@@ -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;
}
}
+3218
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -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
View File
@@ -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.
+10
View File
@@ -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
View File
@@ -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);
}
}
+21
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+14
View File
@@ -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"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});