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