Split codice/contenuti: repo pubblico (codice) — articoli in astro-blog-content (privato)
Some checks failed
deploy-blog / deploy (push) Failing after 0s
Some checks failed
deploy-blog / deploy (push) Failing after 0s
This commit is contained in:
commit
cb8da8fa6d
22
.forgejo/workflows/deploy.yml
Normal file
22
.forgejo/workflows/deploy.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: deploy-blog
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- "src/**"
|
||||
- "package.json"
|
||||
- "Dockerfile"
|
||||
- "astro.config.mjs"
|
||||
- "docker-compose.yml"
|
||||
- "nginx.conf"
|
||||
- ".forgejo/workflows/**"
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Deploy blog (script condiviso, host)
|
||||
run: |
|
||||
set -e
|
||||
bash /root/.hermes/scripts/deploy-blog.sh
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# .gitignore — astro-blog (repo pubblico: SOLO codice)
|
||||
# Gli articoli vivono nel repo privato forgejo-admin/astro-blog-content:
|
||||
# la build li copia in src/content/blog/ (mai committati qui).
|
||||
src/content/blog/*
|
||||
!src/content/blog/.gitkeep
|
||||
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# ---- Stage 1: build del sito statico con Node.js ----
|
||||
FROM docker.io/library/node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install --no-audit --no-fund
|
||||
|
||||
COPY astro.config.mjs tsconfig.json ./
|
||||
COPY src ./src
|
||||
COPY public ./public
|
||||
|
||||
RUN npx astro build
|
||||
|
||||
# ---- Stage 2: serve con nginx ----
|
||||
FROM docker.io/library/nginx:1.27-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
190
GUIDA-REDATTORE-BLOG.md
Normal file
190
GUIDA-REDATTORE-BLOG.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# Guida del redattore — Blog di my-vps
|
||||
|
||||
Guida per chi (umano o agente AI) deve scrivere e pubblicare articoli sul
|
||||
blog di my-vps.
|
||||
|
||||
Ultimo aggiornamento: 2026-08-10
|
||||
|
||||
---
|
||||
|
||||
## 1. Panoramica
|
||||
|
||||
- **Blog**:
|
||||
- sul **web** (con password): `https://blog.194.164.167.80.nip.io` —
|
||||
accesso tramite **Authelia** (SSO, login unico).
|
||||
- in **VPN** (senza password, rete privata): `http://100.64.0.2:8083`.
|
||||
- **Sito statico** generato con **Astro**, servito da **nginx** in un
|
||||
container, esposto da **Traefik**.
|
||||
- **Due repo Forgejo separati**:
|
||||
- **codice** (PUBBLICO): `forgejo-admin/astro-blog`
|
||||
(`https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog`) —
|
||||
tema, layout, configurazione. Non contiene articoli.
|
||||
- **articoli** (PRIVATO): `forgejo-admin/astro-blog-content`
|
||||
(`https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog-content`) —
|
||||
i post in Markdown, in `blog/`.
|
||||
- **Copia di lavoro sul server**: `/opt/astro-blog` (codice) e
|
||||
`/opt/astro-blog-content` (articoli).
|
||||
- **Pipeline**: a ogni push su `main` di **uno dei due repo**, **Forgejo
|
||||
Actions** (runner self-hosted) esegue `/root/.hermes/scripts/deploy-blog.sh`
|
||||
che ricostruisce l'immagine e ricrea il container in automatico.
|
||||
|
||||
> La regola d'oro: **ogni post = un file Markdown nel repo PRIVATO
|
||||
> `astro-blog-content` + `git push` su `main`**. Il deploy è automatico.
|
||||
|
||||
## 2. Struttura di un post
|
||||
|
||||
### 2.1 File e URL
|
||||
|
||||
- Il post va in `blog/<slug>.md` nella copia di lavoro del repo **privato**
|
||||
(o su Forgejo: repo `astro-blog-content` → cartella `blog/`).
|
||||
- `<slug>` determina l'URL finale: `/posts/<slug>/`.
|
||||
- Lo slug: minuscolo, trattini al posto degli spazi, niente spazi/caratteri
|
||||
speciali. Es. `come-si-installa-podman.md` → `/posts/come-si-installa-podman/`.
|
||||
- Non usare slug duplicati (la build fallisce).
|
||||
|
||||
### 2.2 Frontmatter (schema obbligatorio)
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Titolo del post"
|
||||
description: "Una o due frasi di riassunto (opzionale ma consigliato)."
|
||||
pubDate: 2026-08-10T10:30:00+02:00
|
||||
author: "forgejo-admin"
|
||||
tags: ["infrastruttura", "podman"]
|
||||
---
|
||||
```
|
||||
|
||||
| Campo | Obbligatorio | Note |
|
||||
|---|---|---|
|
||||
| `title` | si | testo tra virgolette |
|
||||
| `description` | no | mostrata in lista e nel feed RSS; consigliata |
|
||||
| `pubDate` | si | data ISO **con offset**, ora esatta al secondo in `Europe/Rome` |
|
||||
| `author` | no | default `admin`; usare `forgejo-admin` |
|
||||
| `tags` | no | array di stringhe, es. `["meta", "infrastruttura"]` |
|
||||
|
||||
**Attenzione agli orari**: il sito usa `Europe/Rome`. In estate l'offset è
|
||||
`+02:00`, in inverno `+01:00`. Usare sempre l'offset esatto del momento in cui
|
||||
si vuole che il post risulti pubblicato.
|
||||
|
||||
Esempio valido: `pubDate: 2026-08-10T10:30:00+02:00`.
|
||||
|
||||
### 2.3 Corpo del post
|
||||
|
||||
- Markdown standard: titoli `##`, elenchi, citazioni, `code fence`, link, grassetto.
|
||||
- Non serve altro: il rendering è automatico.
|
||||
- Il post viene ordinato per `pubDate` **decrescente** (il più recente in cima),
|
||||
in home, archivio e RSS.
|
||||
|
||||
### 2.4 Modello (post esistente)
|
||||
|
||||
In `blog/` del repo privato ci sono i post già pubblicati: usarli come
|
||||
riferimento per struttura e frontmatter.
|
||||
|
||||
## 3. Workflow di pubblicazione
|
||||
|
||||
### 3.1 Procedura standard (deploy automatico)
|
||||
|
||||
```sh
|
||||
# SUL SERVER (copia di lavoro del repo PRIVATO)
|
||||
ssh root@194.164.167.80
|
||||
|
||||
cd /opt/astro-blog-content
|
||||
git pull origin main
|
||||
|
||||
# crea o modifica il post
|
||||
nano blog/<slug>.md
|
||||
|
||||
# versiona e pubblica: il push fa partire il deploy automatico
|
||||
git add blog/<slug>.md
|
||||
git commit -m "Nuovo post: <titolo>"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Fatto: il runner ricostruisce l'immagine e ricrea il container da solo
|
||||
(qualche decina di secondi). Verificare l'esito con il par. 4.
|
||||
|
||||
### 3.2 Procedura per l'agente AI (Hermes/opencode)
|
||||
|
||||
Stessi passi, via strumenti:
|
||||
|
||||
1. `git pull` in `/opt/astro-blog-content`.
|
||||
2. Creare il file Markdown `blog/<slug>.md` con frontmatter corretto (par. 2).
|
||||
3. `git add` + `git commit` + `git push origin main` (deploy automatico).
|
||||
4. **Verificare sempre** il risultato (par. 4) e riportarlo all'utente.
|
||||
|
||||
### 3.3 Fallback manuale (solo in emergenza)
|
||||
|
||||
Se l'automazione non funziona, deploy a mano dal server:
|
||||
|
||||
```sh
|
||||
cd /opt/astro-blog
|
||||
git pull origin main
|
||||
git -C /opt/astro-blog-content pull origin main
|
||||
rm -f src/content/blog/*.md
|
||||
cp /opt/astro-blog-content/blog/*.md src/content/blog/
|
||||
podman build -t astro-blog:latest .
|
||||
systemctl restart container-astro-blog.service
|
||||
sleep 5
|
||||
systemctl is-active container-astro-blog.service
|
||||
```
|
||||
|
||||
Il container è gestito da systemd (`container-astro-blog.service`):
|
||||
il restart lo ricrea con l'immagine appena costruita.
|
||||
|
||||
## 4. Verifica della pubblicazione
|
||||
|
||||
```sh
|
||||
# nuovo post raggiungibile? (via VPN, senza password)
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://100.64.0.2:8083/posts/<slug>/
|
||||
|
||||
# home e archivio (via VPN)
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://100.64.0.2:8083/
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://100.64.0.2:8083/archivio/
|
||||
|
||||
# feed RSS (contiene il nuovo post?)
|
||||
curl -s http://100.64.0.2:8083/rss.xml | grep -c "<item>"
|
||||
|
||||
# sul web: deve chiedere il login Authelia (302/401)
|
||||
curl -s -o /dev/null -w "%{http_code}\n" https://blog.194.164.167.80.nip.io/
|
||||
|
||||
# esito del run CI
|
||||
journalctl -u forgejo-runner -n 30
|
||||
# oppure UI: https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog-content/actions
|
||||
```
|
||||
|
||||
## 5. Regole editoriali
|
||||
|
||||
- **Lingua**: italiano. Tono semplice e diretto, stile "appunti".
|
||||
- **Date**: sempre esatte al secondo, fuso `Europe/Rome` con offset (vedi 2.2).
|
||||
- **Description**: 1-2 frasi, utile per lista e RSS.
|
||||
- **Tags**: usare un vocabolario coerente e riusare i tag esistenti
|
||||
(`meta`, `infrastruttura`, ...). Evitare tag inventati per ogni post.
|
||||
- **Immagini**: il layout attuale non prevede gallerie; gli asset statici
|
||||
possono stare in `public/` del repo del codice e riferirsi con percorso
|
||||
assoluto (`/nome.png`).
|
||||
- **Niente segreti**: non pubblicare password, chiavi, token o dati sensibili.
|
||||
Il blog sul web è protetto da login, ma la prudenza resta la regola.
|
||||
- **Attribuzione**: `author` di default `forgejo-admin` (o il nome del redattore).
|
||||
- **Coerenza**: se si modifica un post già pubblicato, aggiornare `pubDate`
|
||||
solo se il contenuto è stato riscritto in modo significativo.
|
||||
|
||||
## 6. Problemi frequenti
|
||||
|
||||
| Sintomo | Causa probabile | Rimedio |
|
||||
|---|---|---|
|
||||
| Il push non fa partire il deploy | runner spento o errore CI | `journalctl -u forgejo-runner`; UI `/actions` |
|
||||
| Post non visibile dopo il push | deploy fallito in CI | console del run; correggere e ri-pushare |
|
||||
| `404` su `/posts/<slug>/` | slug/nome file diverso da quanto atteso | verificare il nome del file |
|
||||
| Build fallita con errore `zod`/frontmatter | campo mancante o `pubDate` non valido | correggere il frontmatter (2.2) |
|
||||
| Slug duplicato | due file generano lo stesso slug | rinominare uno dei file |
|
||||
| Blog `502` su tutto | socket Podman di Traefik | vedi TRAEFIK-FORGEJO-BLOG.md (Nota operativa socket) |
|
||||
| Home ok ma post vecchi | deploy non eseguito (CI fallita) | fallback manuale (3.3) |
|
||||
|
||||
## 7. Riferimenti
|
||||
|
||||
- Documentazione operativa del server: `/root/README.md` e `/root/*.md`.
|
||||
- Deploy/blog (architettura): `/root/TRAEFIK-FORGEJO-BLOG.md`.
|
||||
- Automazione CI: `/root/FORGEJO-ACTIONS.md`.
|
||||
- Repo codice (pubblico): `https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog`.
|
||||
- Repo articoli (privato): `https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog-content`.
|
||||
- Astro content collections: https://docs.astro.build (schema e Markdown).
|
||||
61
README.md
Normal file
61
README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# astro-blog
|
||||
|
||||
Blog statico in **Astro**, containerizzato, ospitato su **Forgejo** e servito
|
||||
via **Traefik** sul VPS my-vps (194.164.167.80).
|
||||
|
||||
- Repo **pubblico** (questo): solo il **codice** del blog.
|
||||
- Repo **privato**: `forgejo-admin/astro-blog-content` — gli **articoli**
|
||||
(Markdown in `blog/`).
|
||||
|
||||
## Accesso al blog
|
||||
|
||||
| Canale | URL | Autenticazione |
|
||||
|---|---|---|
|
||||
| Web (pubblico) | https://blog.194.164.167.80.nip.io | password Authelia (SSO) |
|
||||
| VPN | http://100.64.0.2:8083 | nessuna (rete privata) |
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
astro-blog/
|
||||
├── Dockerfile # build multistage: Node.js → nginx
|
||||
├── nginx.conf # config nginx del container
|
||||
├── docker-compose.yml # deploy con label Traefik (+ Authelia)
|
||||
├── astro.config.mjs
|
||||
├── src/
|
||||
│ ├── content/config.ts # schema collezione blog
|
||||
│ ├── content/blog/ # NON versionato qui (arriva dal repo privato in build)
|
||||
│ ├── layouts/ # BaseLayout, PostLayout
|
||||
│ ├── lib/posts.ts # ordinamento post
|
||||
│ ├── consts.ts # titolo, descrizione, timezone, formatDateTime
|
||||
│ ├── pages/ # index, archivio, posts/[...slug], rss.xml.js
|
||||
│ └── styles/global.css
|
||||
└── public/favicon.svg
|
||||
```
|
||||
|
||||
## Come funziona la build (separazione codice/contenuti)
|
||||
|
||||
A ogni push su `main` (di QUESTO repo **o** del repo contenuti), il runner
|
||||
Forgejo Actions esegue `/root/.hermes/scripts/deploy-blog.sh` che:
|
||||
|
||||
1. `git pull` in `/opt/astro-blog` (codice) e `/opt/astro-blog-content` (articoli)
|
||||
2. copia `blog/*.md` → `src/content/blog/`
|
||||
3. `podman build -t astro-blog:latest` + `systemctl restart container-astro-blog.service`
|
||||
|
||||
## Comandi (fallback manuale, sul server)
|
||||
|
||||
```sh
|
||||
cd /opt/astro-blog && git pull origin main
|
||||
git -C /opt/astro-blog-content pull origin main
|
||||
rm -f src/content/blog/*.md && cp /opt/astro-blog-content/blog/*.md src/content/blog/
|
||||
podman build -t astro-blog:latest .
|
||||
systemctl restart container-astro-blog.service
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
- Tutti gli orari nel sito sono in `Europe/Rome`, al secondo
|
||||
(`formatDateTime` in `src/consts.ts`).
|
||||
- L'immagine è stateless: ogni articolo richiede una nuova build (automatica).
|
||||
- Scrivere articoli: vedi `GUIDA-REDATTORE-BLOG.md` (pubblica) e il README
|
||||
del repo privato `astro-blog-content`.
|
||||
9
astro.config.mjs
Normal file
9
astro.config.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'http://blog.194.164.167.80.nip.io',
|
||||
output: 'static',
|
||||
build: {
|
||||
format: 'directory',
|
||||
},
|
||||
});
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
services:
|
||||
blog:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: astro-blog:latest
|
||||
container_name: astro-blog
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.blog.rule=Host(`blog.194.164.167.80.nip.io`)"
|
||||
- "traefik.http.routers.blog.entrypoints=web"
|
||||
- "traefik.http.routers.blog.middlewares=authelia@docker"
|
||||
- "traefik.http.routers.blog-tls.rule=Host(`blog.194.164.167.80.nip.io`)"
|
||||
- "traefik.http.routers.blog-tls.entrypoints=websecure"
|
||||
- "traefik.http.routers.blog-tls.middlewares=authelia@docker"
|
||||
- "traefik.http.routers.blog-tls.tls=true"
|
||||
- "traefik.http.routers.blog-tls.tls.certresolver=le"
|
||||
- "traefik.http.routers.blog-tls.service=blog"
|
||||
- "traefik.http.services.blog.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
23
nginx.conf
Normal file
23
nginx.conf
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Forwarded-Proto "http" always;
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "astro-blog",
|
||||
"version": "1.0.0",
|
||||
"description": "Blog statico in Astro, containerizzato su my-vps",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.16.0",
|
||||
"@astrojs/rss": "^4.0.7"
|
||||
}
|
||||
}
|
||||
4
public/favicon.svg
Normal file
4
public/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<circle cx="50" cy="50" r="46" fill="#fc5d5d"/>
|
||||
<text x="50" y="68" font-size="52" text-anchor="middle" fill="#fff" font-family="sans-serif">B</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 224 B |
16
src/consts.ts
Normal file
16
src/consts.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export const SITE_TITLE = 'Il Blog di my-vps';
|
||||
export const SITE_DESCRIPTION = 'Blog statico in Astro, ospitato su Forgejo e servito via Traefik.';
|
||||
export const SITE_TIMEZONE = 'Europe/Rome';
|
||||
|
||||
export function formatDateTime(d: Date): string {
|
||||
return new Intl.DateTimeFormat('it-IT', {
|
||||
timeZone: SITE_TIMEZONE,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
}).format(d).replace(/\//g, '-');
|
||||
}
|
||||
0
src/content/blog/.gitkeep
Normal file
0
src/content/blog/.gitkeep
Normal file
14
src/content/config.ts
Normal file
14
src/content/config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
pubDate: z.coerce.date(),
|
||||
author: z.string().default('admin'),
|
||||
tags: z.array(z.string()).default([]),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
||||
118
src/layouts/BaseLayout.astro
Normal file
118
src/layouts/BaseLayout.astro
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
---
|
||||
import { SITE_TITLE, SITE_DESCRIPTION, formatDateTime, SITE_TIMEZONE } from '../consts';
|
||||
import '../styles/global.css';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { title = SITE_TITLE, description = SITE_DESCRIPTION } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<nav>
|
||||
<a href="/" class="brand">{SITE_TITLE}</a>
|
||||
<div class="links">
|
||||
<a href="/">Home</a>
|
||||
<a href="/archivio/">Archivio</a>
|
||||
<a href="/servizi/">Servizi</a>
|
||||
<a href="/infrastruttura/">Infrastruttura</a>
|
||||
<a href="https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog">Repo sorgente</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner">
|
||||
<div class="footer-col">
|
||||
<p class="footer-title">{SITE_TITLE}</p>
|
||||
<p>© {new Date().getFullYear()} — generato con Astro, servito via Traefik.</p>
|
||||
<p>Codice sorgente su <a href="https://git.194.164.167.80.nip.io/forgejo-admin/astro-blog">Forgejo</a>.</p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<p class="footer-title">Servizi</p>
|
||||
<ul class="footer-links">
|
||||
<li><a href="https://git.194.164.167.80.nip.io/">Forgey — git</a></li>
|
||||
<li><a href="https://blog.194.164.167.80.nip.io/">Blog — questo sito</a></li>
|
||||
<li><a href="https://ide.194.164.167.80.nip.io/">IDE VSCodium + RooCode</a></li>
|
||||
<li><a href="https://logseq.194.164.167.80.nip.io/">Logseq — note</a></li>
|
||||
<li><a href="https://adminer.194.164.167.80.nip.io/">Adminer — PostgreSQL</a></li>
|
||||
<li><a href="https://traefik.194.164.167.80.nip.io/dashboard/">Dashboard Traefik</a></li>
|
||||
</ul>
|
||||
<p class="footer-servizi"><a href="/servizi/">Tutte le schede dei servizi →</a></p>
|
||||
</div>
|
||||
<div class="footer-col">
|
||||
<p class="footer-title">Server e tecnologie</p>
|
||||
<p class="footer-tech">Debian · Podman · systemd · Astro · Forgejo · Traefik · PostgreSQL · Adminer</p>
|
||||
<p><a href="/infrastruttura/">Dettagli sull'infrastruttura →</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-build">Ultima build: <time datetime={new Date().toISOString()}>{formatDateTime(new Date())} ({SITE_TIMEZONE})</time></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--accent: #fc5d5d;
|
||||
--fg: #333;
|
||||
--bg: #fff;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
}
|
||||
.site-header { border-bottom: 1px solid #eee; }
|
||||
.site-header nav {
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.brand { font-weight: 700; color: var(--fg); text-decoration: none; font-size: 1.1rem; }
|
||||
.links { display: flex; gap: 1rem; flex-wrap: wrap; }
|
||||
.links a { color: var(--accent); text-decoration: none; font-size: .95rem; }
|
||||
main { max-width: 42rem; margin: 0 auto; padding: 2rem 1rem; }
|
||||
.site-footer { border-top: 1px solid #eee; margin-top: 3rem; background: #fafafa; }
|
||||
.footer-inner {
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.footer-title { font-weight: 700; margin: 0 0 .5rem; }
|
||||
.footer-col p { font-size: .9rem; }
|
||||
.footer-links { list-style: none; padding: 0; margin: 0; }
|
||||
.footer-links li { margin-bottom: .25rem; }
|
||||
.footer-links a, .footer-col a { color: var(--accent); text-decoration: none; }
|
||||
.footer-servizi { margin: .6rem 0 0; }
|
||||
.footer-servizi a { font-size: .85rem; color: var(--accent); text-decoration: none; }
|
||||
.footer-links a:hover, .footer-col a:hover { text-decoration: underline; }
|
||||
.footer-build { max-width: 42rem; margin: 0 auto; padding: 0 1rem 1.5rem; color: #999; font-size: .8rem; }
|
||||
</style>
|
||||
42
src/layouts/PostLayout.astro
Normal file
42
src/layouts/PostLayout.astro
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { formatDateTime, SITE_TIMEZONE } from '../consts';
|
||||
|
||||
interface Props {
|
||||
post: CollectionEntry<'blog'>;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { title, description, pubDate, author, tags } = post.data;
|
||||
---
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1 class="title">{title}</h1>
|
||||
<p class="meta">
|
||||
<time datetime={pubDate.toISOString()}>{formatDateTime(pubDate)} ({SITE_TIMEZONE})</time>
|
||||
{' · '}{author}
|
||||
</p>
|
||||
{description && <p class="description">{description}</p>}
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{tags.length > 0 && (
|
||||
<footer>
|
||||
<ul class="tags">
|
||||
{tags.map((t) => <li><span>{t}</span></li>)}
|
||||
</ul>
|
||||
</footer>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.title { font-size: 2rem; line-height: 1.2; margin: 0 0 .5rem; }
|
||||
.meta { color: #888; font-size: .9rem; margin: 0 0 1rem; }
|
||||
.description { font-style: italic; color: #666; }
|
||||
.tags { list-style: none; padding: 0; display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
.tags li span { background: #eef; padding: .2rem .6rem; border-radius: 999px; font-size: .8rem; }
|
||||
</style>
|
||||
8
src/lib/posts.ts
Normal file
8
src/lib/posts.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { getCollection } from 'astro:content';
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
export function getSortedPosts(posts: CollectionEntry<'blog'>[]) {
|
||||
return [...posts].sort(
|
||||
(a, b) => new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime(),
|
||||
);
|
||||
}
|
||||
29
src/pages/archivio.astro
Normal file
29
src/pages/archivio.astro
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getSortedPosts } from '../lib/posts';
|
||||
import { formatDateTime } from '../consts';
|
||||
|
||||
const posts = getSortedPosts(await getCollection('blog'));
|
||||
---
|
||||
|
||||
<BaseLayout title="Archivio">
|
||||
<h1>Archivio</h1>
|
||||
<ul class="archive">
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li>
|
||||
<time datetime={post.data.pubDate.toISOString()}>{formatDateTime(post.data.pubDate)}</time>
|
||||
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.archive { list-style: none; padding: 0; }
|
||||
.archive li { display: flex; gap: 1rem; padding: .5rem 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.archive time { color: #999; white-space: nowrap; font-size: .85rem; }
|
||||
.archive a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
45
src/pages/index.astro
Normal file
45
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getSortedPosts } from '../lib/posts';
|
||||
import { formatDateTime, SITE_TIMEZONE } from '../consts';
|
||||
|
||||
const posts = getSortedPosts(await getCollection('blog'));
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<h1 class="hero">Benvenuti nel blog</h1>
|
||||
<p class="lead">
|
||||
Raccolta di appunti e articoli. Il sito è un progetto <strong>Astro</strong>
|
||||
containerizzato, con il codice sorgente ospitato su <strong>Forgejo</strong>
|
||||
e il routing gestito da <strong>Traefik</strong>.
|
||||
</p>
|
||||
|
||||
<ul class="post-list">
|
||||
{
|
||||
posts.map((post) => (
|
||||
<li>
|
||||
<a href={`/posts/${post.slug}/`}>
|
||||
<span class="date">{formatDateTime(post.data.pubDate)}</span>
|
||||
<span class="title">{post.data.title}</span>
|
||||
</a>
|
||||
{post.data.description && <p class="desc">{post.data.description}</p>}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<p class="tz">Ora: <time datetime={new Date().toISOString()}>{formatDateTime(new Date())}</time> ({SITE_TIMEZONE})</p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
.post-list { list-style: none; padding: 0; }
|
||||
.post-list li { padding: .75rem 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.post-list a { text-decoration: none; color: var(--accent); display: flex; gap: 1rem; align-items: baseline; }
|
||||
.post-list .date { color: #999; font-size: .85rem; white-space: nowrap; }
|
||||
.post-list .title { font-weight: 600; }
|
||||
.post-list .desc { margin: .25rem 0 0; color: #666; font-size: .9rem; }
|
||||
.tz { color: #aaa; font-size: .8rem; margin-top: 1.5rem; }
|
||||
</style>
|
||||
99
src/pages/infrastruttura.astro
Normal file
99
src/pages/infrastruttura.astro
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { SITE_TITLE } from '../consts';
|
||||
|
||||
const services = [
|
||||
{ name: 'Authelia (login unico)', url: 'https://auth.194.164.167.80.nip.io/', desc: 'Portale SSO: una password + 2FA sbloccano tutti i servizi riservati.' },
|
||||
{ name: 'Forgejo', url: 'https://git.194.164.167.80.nip.io/', desc: 'Hosting git con Forgejo Actions per il deploy automatico del blog.' },
|
||||
{ name: 'Blog', url: 'https://blog.194.164.167.80.nip.io/', desc: 'Questo sito: statico, generato con Astro.' },
|
||||
{ name: 'Adminer', url: 'https://adminer.194.164.167.80.nip.io/', desc: 'Amministrazione del database PostgreSQL da browser (accesso con SSO).' },
|
||||
{ name: 'Dashboard Traefik', url: 'https://traefik.194.164.167.80.nip.io/dashboard/', desc: 'Vista del reverse proxy e delle route attive (accesso con SSO).' },
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout title="Infrastruttura — Il Blog di my-vps" description="Il server, le tecnologie e i servizi che fanno girare my-vps.">
|
||||
<h1 class="hero">L'infrastruttura</h1>
|
||||
<p class="lead">
|
||||
Tutto gira su un piccolo server (il "my-vps") come container gestiti da
|
||||
systemd e dietro un reverse proxy. Questa pagina riassume, in modo sicuro
|
||||
e senza dettagli riservati, com'è fatta l'infrastruttura.
|
||||
</p>
|
||||
|
||||
<h2>Il server</h2>
|
||||
<ul>
|
||||
<li>Una VM Linux (Debian) con <strong>Podman</strong> e <strong>systemd</strong>.</li>
|
||||
<li>Ogni servizio è un container supervisionato da un'unità systemd
|
||||
(<code>container-<nome>.service</code>): riavvio automatico al boot
|
||||
e in caso di crash.</li>
|
||||
<li>Il traffico pubblico passa da <strong>Traefik</strong>, che termina le
|
||||
connessioni e instrada verso i container.</li>
|
||||
<li>Backup automatici giornalieri e una suite di sicurezza attiva
|
||||
(firewall, fail2ban, accessi protetti).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Tecnologie</h2>
|
||||
<ul>
|
||||
<li><strong>Astro</strong> — generatore statico di questo blog.</li>
|
||||
<li><strong>Forgejo</strong> — server git con CI integrata (Actions).</li>
|
||||
<li><strong>Traefik</strong> — reverse proxy e gateway TLS.</li>
|
||||
<li><strong>PostgreSQL</strong> — database relazionale (contenuto in una
|
||||
rete interna, non esposto).</li>
|
||||
<li><strong>Adminer</strong> — interfaccia web leggera per PostgreSQL.</li>
|
||||
<li><strong>Authelia</strong> — login unico (SSO) con 2FA per i servizi
|
||||
riservati.</li>
|
||||
<li><strong>Hermes</strong> e <strong>OmniRoute</strong> — agenti e proxy
|
||||
per l'integrazione con modelli di linguaggio.</li>
|
||||
<li>Infrastruttura descritta anche tramite <strong>Ansible</strong> e
|
||||
documentata sul server.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Servizi pubblici</h2>
|
||||
<table class="services">
|
||||
<thead>
|
||||
<tr><th>Servizio</th><th>Indirizzo</th><th>Cosa fa</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
services.map((s) => (
|
||||
<tr>
|
||||
<td><a href={s.url}>{s.name}</a></td>
|
||||
<td><code>{s.url.replace(/^https?:\/\//, '')}</code></td>
|
||||
<td>{s.desc}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">
|
||||
I servizi con <strong>SSO</strong> (dashboard, Adminer, IDE, Logseq e
|
||||
Forgejo) si sbloccano dal portale <code>auth.…nip.io</code> con una sola
|
||||
password + 2FA: le credenziali non vengono mai pubblicate su questo sito.
|
||||
</p>
|
||||
|
||||
<h2>Sicurezza</h2>
|
||||
<ul>
|
||||
<li><strong>HTTPS</strong> attivo su tutti i servizi pubblici con
|
||||
certificati <strong>Let's Encrypt</strong> rinnovati automaticamente.</li>
|
||||
<li>I servizi riservati sono protetti da un <strong>login unico</strong>
|
||||
(SSO) con doppia autenticazione.</li>
|
||||
<li>Le porte di servizi interni (database, API) non sono raggiungibili da
|
||||
internet: solo rete interna o localhost.</li>
|
||||
<li><strong>fail2ban</strong> protegge l'accesso SSH dal brute-force.</li>
|
||||
<li>Backup giornalieri e procedure di disaster recovery documentate.</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/">← Torna alla home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.services { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.services th, table.services td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.services a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
||||
table.services code { font-size: .85rem; color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: .5rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
23
src/pages/posts/[...slug].astro
Normal file
23
src/pages/posts/[...slug].astro
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import PostLayout from '../../layouts/PostLayout.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { getSortedPosts } from '../../lib/posts';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = getSortedPosts(await getCollection('blog'));
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
---
|
||||
|
||||
<BaseLayout title={post.data.title} description={post.data.description}>
|
||||
<PostLayout post={post}>
|
||||
<Content />
|
||||
</PostLayout>
|
||||
</BaseLayout>
|
||||
18
src/pages/rss.xml.ts
Normal file
18
src/pages/rss.xml.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
|
||||
|
||||
export async function GET(context: any) {
|
||||
const posts = await getCollection('blog');
|
||||
return rss({
|
||||
title: SITE_TITLE,
|
||||
description: SITE_DESCRIPTION,
|
||||
site: context.site,
|
||||
items: posts.map((post) => ({
|
||||
title: post.data.title,
|
||||
description: post.data.description,
|
||||
pubDate: post.data.pubDate,
|
||||
link: `/posts/${post.slug}/`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
134
src/pages/servizi.astro
Normal file
134
src/pages/servizi.astro
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
|
||||
const servizi = [
|
||||
{
|
||||
slug: 'auth',
|
||||
name: 'Login unico (SSO)',
|
||||
url: 'https://auth.194.164.167.80.nip.io/',
|
||||
desc: 'Portale di accesso con una sola password + doppia autenticazione (2FA). Sblocca tutti i servizi riservati.',
|
||||
tags: ['accesso'],
|
||||
},
|
||||
{
|
||||
slug: 'ide',
|
||||
name: 'IDE VSCodium + RooCode',
|
||||
url: 'https://ide.194.164.167.80.nip.io/',
|
||||
desc: 'Editor di codice in browser (VSCodium) con l\'assistente AI RooCode collegato a OmniRoute. Accesso con SSO.',
|
||||
tags: ['editor', 'AI'],
|
||||
},
|
||||
{
|
||||
slug: 'logseq',
|
||||
name: 'Logseq',
|
||||
url: 'https://logseq.194.164.167.80.nip.io/',
|
||||
desc: 'Appunti e conoscenza in formato testo (outliner) con note tracciate su Forgejo. Accesso con SSO.',
|
||||
tags: ['note', 'wiki'],
|
||||
},
|
||||
{
|
||||
slug: 'git',
|
||||
name: 'Forgejo — git',
|
||||
url: 'https://git.194.164.167.80.nip.io/',
|
||||
desc: 'Server git con CI integrata (Actions): è il cuore dei repository, incluso questo sito.',
|
||||
tags: ['git', 'CI'],
|
||||
},
|
||||
{
|
||||
slug: 'adminer',
|
||||
name: 'Adminer + PostgreSQL',
|
||||
url: 'https://adminer.194.164.167.80.nip.io/',
|
||||
desc: 'Amministrazione del database PostgreSQL da browser. Accesso con SSO.',
|
||||
tags: ['database'],
|
||||
},
|
||||
{
|
||||
slug: 'dashboard',
|
||||
name: 'Dashboard Traefik',
|
||||
url: 'https://traefik.194.164.167.80.nip.io/dashboard/',
|
||||
desc: 'Vista del reverse proxy: rotte attive, certificati e stato dei servizi. Accesso con SSO.',
|
||||
tags: ['infra'],
|
||||
},
|
||||
{
|
||||
slug: 'blog',
|
||||
name: 'Questo blog',
|
||||
url: 'https://blog.194.164.167.80.nip.io/',
|
||||
desc: 'Il sito che stai leggendo: statico, generato con Astro e pubblicato automaticamente via CI.',
|
||||
tags: ['web'],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<BaseLayout title="Servizi — Il Blog di my-vps" description="Tutti i servizi offerti da my-vps: come accederci e cosa fanno.">
|
||||
<h1 class="hero">Servizi di my-vps</h1>
|
||||
<p class="lead">
|
||||
Tutti i servizi sono raggiungibili da questa pagina. I servizi riservati
|
||||
si sbloccano con un <strong>login unico (SSO)</strong>: una sola password
|
||||
+ doppia autenticazione, gestita dal portale
|
||||
<a href="https://auth.194.164.167.80.nip.io/"><code>auth.…nip.io</code></a>.
|
||||
</p>
|
||||
|
||||
<div class="cards">
|
||||
{
|
||||
servizi.map((s) => (
|
||||
<a class="card" href={`/servizi/${s.slug}/`}>
|
||||
<div class="card-head">
|
||||
<span class="card-name">{s.name}</span>
|
||||
<span class="card-url">{s.url.replace(/^https?:\/\//, '')}</span>
|
||||
</div>
|
||||
<p class="card-desc">{s.desc}</p>
|
||||
<p class="card-tags">
|
||||
{s.tags.map((t) => <span class="tag">{t}</span>)}
|
||||
<span class="more">→ scheda dedicata</span>
|
||||
</p>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<h2>Accesso rapido</h2>
|
||||
<table class="services">
|
||||
<thead>
|
||||
<tr><th>Servizio</th><th>Indirizzo</th><th>Accesso</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
servizi.map((s) => (
|
||||
<tr>
|
||||
<td><a href={`/servizi/${s.slug}/`}>{s.name}</a></td>
|
||||
<td><code>{s.url.replace(/^https?:\/\//, '')}</code></td>
|
||||
<td>{s.tags.includes('editor') || s.tags.includes('note') || s.tags.includes('database') || s.tags.includes('infra') || s.tags.includes('accesso') ? 'SSO + 2FA' : 'pubblico'}</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="note">
|
||||
Vedi anche la pagina <a href="/infrastruttura/">Infrastruttura</a> per
|
||||
server, tecnologie e sicurezza nel loro insieme.
|
||||
</p>
|
||||
<p class="back"><a href="/">← Torna alla home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.card {
|
||||
border: 1px solid #eee; border-radius: 8px; padding: 1rem 1.1rem;
|
||||
color: var(--fg); text-decoration: none; background: #fff;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
.card:hover { border-color: var(--accent); box-shadow: 0 2px 10px rgba(0,0,0,.06); }
|
||||
.card-head { display: flex; justify-content: space-between; gap: .5rem; align-items: baseline; }
|
||||
.card-name { font-weight: 700; color: var(--accent); }
|
||||
.card-url { font-size: .78rem; color: #999; word-break: break-all; }
|
||||
.card-desc { font-size: .92rem; margin: .4rem 0 .6rem; }
|
||||
.card-tags { display: flex; gap: .4rem; align-items: center; font-size: .78rem; }
|
||||
.tag { background: #f3f3f3; border-radius: 20px; padding: .1rem .55rem; color: #666; }
|
||||
.more { margin-left: auto; color: var(--accent); }
|
||||
table.services { border-collapse: collapse; width: 100%; font-size: .95rem; margin-top: .5rem; }
|
||||
table.services th, table.services td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; }
|
||||
table.services a { color: var(--accent); text-decoration: none; font-weight: 600; }
|
||||
table.services code { font-size: .85rem; color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: 1rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
61
src/pages/servizi/adminer.astro
Normal file
61
src/pages/servizi/adminer.astro
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Adminer + PostgreSQL — Servizi di my-vps" description="Il database relazionale del server con la sua interfaccia di amministrazione da browser.">
|
||||
<h1 class="hero">Adminer + PostgreSQL</h1>
|
||||
<p class="lead">
|
||||
my-vps offre un <strong>database PostgreSQL</strong> e un'interfaccia di
|
||||
amministrazione comoda da browser (<strong>Adminer</strong>).
|
||||
</p>
|
||||
|
||||
<h2>Cosa è</h2>
|
||||
<ul>
|
||||
<li><strong>PostgreSQL</strong> 17 gira in un container dedicato, in una
|
||||
rete interna separata: non è mai esposto su internet.</li>
|
||||
<li><strong>Adminer</strong> è un frontend leggero per gestire il database
|
||||
(tabelle, query, utenti, backup) da qualunque browser.</li>
|
||||
<li>Adatto ad applicazioni e sperimentazioni che hanno bisogno di un
|
||||
database vero, persistente e di cui si fa il backup automatico.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Come accedere</h2>
|
||||
<table class="kv">
|
||||
<tbody>
|
||||
<tr><th>Indirizzo</th><td><code>https://adminer.194.164.167.80.nip.io/</code></td></tr>
|
||||
<tr><th>Accesso</th><td>login unico (<strong>SSO Authelia</strong>) + in pagina il login del database: le credenziali sono nella documentazione privata del server.</td></tr>
|
||||
<tr><th>Host database</th><td><code>postgres</code> (rete interna <code>db</code>), porta 5432 locale</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">
|
||||
Alla prima apertura Adminer reindirizza al portale di login
|
||||
(<code>auth.…nip.io</code>); poi si inserisce host/utente/password del
|
||||
database (documentazione privata del server).
|
||||
</p>
|
||||
|
||||
<h2>Sicurezza</h2>
|
||||
<ul>
|
||||
<li>Il database ascolta solo su <code>127.0.0.1:5432</code> e sulla rete
|
||||
interna: irraggiungibile da internet.</li>
|
||||
<li>Adminer è protetto dal <strong>SSO</strong> (login unico + 2FA) e da HTTPS (Let's Encrypt).</li>
|
||||
<li>Autenticazione del database con <code>scram-sha-256</code> (richiesta
|
||||
anche in locale).</li>
|
||||
<li>Backup automatico del database (dump logico) con il backup giornaliero
|
||||
del server.</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/servizi/">← Tutti i servizi</a> · <a href="/">Home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.kv { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.kv th, table.kv td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.kv th { width: 10rem; background: #fafafa; }
|
||||
table.kv code { color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: .5rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
57
src/pages/servizi/auth.astro
Normal file
57
src/pages/servizi/auth.astro
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Login unico (SSO) — Servizi di my-vps" description="Il portale di accesso unificato ai servizi riservati di my-vps.">
|
||||
<h1 class="hero">Login unico (SSO)</h1>
|
||||
<p class="lead">
|
||||
Un solo accesso per tutti i servizi riservati: una <strong>password</strong>
|
||||
+ <strong>doppia autenticazione</strong> (codice temporaneo). Il portale è
|
||||
gestito da <strong>Authelia</strong>, il servizio SSO di my-vps.
|
||||
</p>
|
||||
|
||||
<h2>Cosa è</h2>
|
||||
<ul>
|
||||
<li><strong>Authelia</strong> è il "portone" dell'infrastruttura: chi si
|
||||
autentica qui ottiene l'accesso a dashboard, amministrazione, IDE e note
|
||||
senza ripetere il login.</li>
|
||||
<li>La protezione è <strong>doppia</strong>: password più codice temporaneo
|
||||
(TOTP) da un'app authenticator.</li>
|
||||
<li>Ogni servizio riservato, quando aperto senza login, reindirizza a
|
||||
questo portale.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Come si usa</h2>
|
||||
<table class="kv">
|
||||
<tbody>
|
||||
<tr><th>Indirizzo</th><td><code>https://auth.194.164.167.80.nip.io/</code></td></tr>
|
||||
<tr><th>Primo accesso</th><td>aprire uno dei servizi riservati (es. dashboard) → si viene reindirizzati qui → password + codice 2FA.</td></tr>
|
||||
<tr><th>Dopo il login</th><td>dashboard, Adminer, IDE, Logseq e Forgejo sono sbloccati per la sessione.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Sicurezza</h2>
|
||||
<ul>
|
||||
<li>Una sola coppia di credenziali da custodire (password master + app
|
||||
authenticator), invece di una password per ogni servizio.</li>
|
||||
<li>Il portale gira in un container isolato nella rete interna: non è mai
|
||||
esposto al di fuori del reverse proxy.</li>
|
||||
<li>Le credenziali non vengono pubblicate su questo sito: sono nella
|
||||
documentazione privata del server.</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/servizi/">← Tutti i servizi</a> · <a href="/">Home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.kv { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.kv th, table.kv td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.kv th { width: 10rem; background: #fafafa; }
|
||||
table.kv code { color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: .5rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
57
src/pages/servizi/dashboard.astro
Normal file
57
src/pages/servizi/dashboard.astro
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Dashboard Traefik — Servizi di my-vps" description="Il pannello del reverse proxy che instrada tutti i servizi.">
|
||||
<h1 class="hero">Dashboard Traefik</h1>
|
||||
<p class="lead">
|
||||
Il pannello del <strong>reverse proxy</strong> che riceve tutto il traffico
|
||||
e lo instrada verso i container.
|
||||
</p>
|
||||
|
||||
<h2>Cosa è</h2>
|
||||
<ul>
|
||||
<li><strong>Traefik</strong> è il gateway di my-vps: accetta le richieste
|
||||
su HTTP/HTTPS e le consegna al servizio giusto in base al nome
|
||||
(es. <code>git.…</code> → Forgejo, <code>blog.…</code> → Astro).</li>
|
||||
<li>Termina il <strong>TLS</strong> e rinnova automaticamente i
|
||||
certificati Let's Encrypt.</li>
|
||||
<li>La dashboard mostra rotte, servizi e stato di configurazione in tempo
|
||||
reale.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Come accedere</h2>
|
||||
<table class="kv">
|
||||
<tbody>
|
||||
<tr><th>Indirizzo</th><td><code>https://traefik.194.164.167.80.nip.io/dashboard/</code></td></tr>
|
||||
<tr><th>Accesso</th><td>login unico (<strong>SSO Authelia</strong>): password + codice 2FA, una sola volta per tutti i servizi riservati. Le credenziali sono nella documentazione privata del server.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">
|
||||
Alla prima apertura il servizio reindirizza al portale di login
|
||||
(<code>auth.…nip.io</code>): dopo l'autenticazione si torna qui.
|
||||
</p>
|
||||
|
||||
<h2>Cosa ci vedi</h2>
|
||||
<ul>
|
||||
<li>Le <strong>rotte HTTP/HTTPS</strong> attive (blog, git, adminer, ide,
|
||||
logseq, dashboard).</li>
|
||||
<li>I <strong>certificati TLS</strong> e le loro scadenze.</li>
|
||||
<li>Lo stato dei <strong>servizi</strong> (healthy/unhealthy).</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/servizi/">← Tutti i servizi</a> · <a href="/">Home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.kv { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.kv th, table.kv td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.kv th { width: 10rem; background: #fafafa; }
|
||||
table.kv code { color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: .5rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
60
src/pages/servizi/git.astro
Normal file
60
src/pages/servizi/git.astro
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Forgejo — Servizi di my-vps" description="Il server git self-hosted con CI integrata.">
|
||||
<h1 class="hero">Forgejo — git</h1>
|
||||
<p class="lead">
|
||||
Il server <strong>git self-hosted</strong> di my-vps, con CI integrata
|
||||
(Actions) che pubblica automaticamente questo blog.
|
||||
</p>
|
||||
|
||||
<h2>Cosa è</h2>
|
||||
<ul>
|
||||
<li>Hosting di repository git (pubblici e privati) in un container dietro
|
||||
Traefik.</li>
|
||||
<li>Integra <strong>Forgejo Actions</strong>: un runner esegue i workflow
|
||||
definiti nei repository (es. il deploy del blog a ogni push).</li>
|
||||
<li>Hosting "first-party": il codice e le Actions non dipendono da servizi
|
||||
esterni.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Come accedere</h2>
|
||||
<table class="kv">
|
||||
<tbody>
|
||||
<tr><th>Indirizzo</th><td><code>https://git.194.164.167.80.nip.io/</code></td></tr>
|
||||
<tr><th>Login web</th><td><strong>SSO</strong>: pulsante "Sign in with Authelia" (password + 2FA del login unico).</td></tr>
|
||||
<tr><th>Clone SSH/HTTPS</th><td><code>git clone http://git.194.164.167.80.nip.io/<utente>/<repo>.git</code></td></tr>
|
||||
<tr><th>Registrazione</th><td>disabilitata: gli account si creano solo dall'amministratore.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Repository attivi</h2>
|
||||
<ul>
|
||||
<li><code>astro-blog</code> — sorgente di questo sito (deploy automatico via Actions).</li>
|
||||
<li><code>roo-workspace</code> — workspace dell'IDE VSCodium (sync automatica).</li>
|
||||
<li><code>logseq-notes</code> — note di Logseq (sync automatica).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Perché self-hosted</h2>
|
||||
<ul>
|
||||
<li>I repository restano sul server, senza limiti o sorveglianza di terze
|
||||
parti.</li>
|
||||
<li>La CI è locale: i workflow girano nel runner di my-vps.</li>
|
||||
<li>È il punto di aggancio di molti altri servizi (workspace, blog).</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/servizi/">← Tutti i servizi</a> · <a href="/">Home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.kv { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.kv th, table.kv td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.kv th { width: 10rem; background: #fafafa; }
|
||||
table.kv code { color: #666; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
72
src/pages/servizi/ide.astro
Normal file
72
src/pages/servizi/ide.astro
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="IDE VSCodium — Servizi di my-vps" description="Editor di codice in browser con assistente AI RooCode collegato a OmniRoute.">
|
||||
<h1 class="hero">IDE VSCodium + RooCode</h1>
|
||||
<p class="lead">
|
||||
Un editor di codice completo, <strong>VSCodium</strong> (la build aperta di
|
||||
VS Code), eseguito in un container e usato dal browser.
|
||||
</p>
|
||||
|
||||
<h2>Cosa è</h2>
|
||||
<ul>
|
||||
<li>L'editor gira come container (<code>vscodium</code>) gestito da
|
||||
systemd, dietro il reverse proxy <strong>Traefik</strong>.</li>
|
||||
<li>L'interfaccia è un desktop in browser (KasmVNC): VSCodium si apre nella
|
||||
finestra del browser, senza installare nulla in locale.</li>
|
||||
<li>Include l'estensione <strong>RooCode</strong> (assistente AI di
|
||||
programmazione) già configurata per usare <strong>OmniRoute</strong> come
|
||||
endpoint di modelli.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Come accedere</h2>
|
||||
<table class="kv">
|
||||
<tbody>
|
||||
<tr><th>Indirizzo</th><td><code>https://ide.194.164.167.80.nip.io/</code></td></tr>
|
||||
<tr><th>Accesso</th><td>solo login unico (<strong>SSO Authelia</strong>): niente altre password.</td></tr>
|
||||
<tr><th>Workspace</th><td><code>/config/workspace</code> — repository <code>roo-workspace</code> su Forgejo.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">
|
||||
Alla prima apertura il servizio reindirizza al portale di login
|
||||
(<code>auth.…nip.io</code>); poi il desktop si apre direttamente.
|
||||
</p>
|
||||
|
||||
<h2>Workspace tracciato con git</h2>
|
||||
<ul>
|
||||
<li>La cartella di lavoro è un repository git (privato) su
|
||||
<strong>Forgejo</strong>: <code>forgejo-admin/roo-workspace</code>.</li>
|
||||
<li>Le modifiche vengono committate e caricate automaticamente ogni 15
|
||||
minuti (timer <code>git-sync.timer</code>), quindi il lavoro è sempre
|
||||
salvato e versionato.</li>
|
||||
<li>Si può clonare il workspace da qualsiasi macchina per lavorarci anche
|
||||
fuori dal browser.</li>
|
||||
</ul>
|
||||
|
||||
<h2>RooCode e OmniRoute</h2>
|
||||
<ul>
|
||||
<li>RooCode è configurato con un provider "OpenAI Compatible" che punta a
|
||||
<code>http://omniroute:20128/v1</code> (solo rete interna del server,
|
||||
mai esposto).</li>
|
||||
<li>Modello predefinito: <code>auto/best-coding</code> (scelto da OmniRoute
|
||||
fra i modelli disponibili).</li>
|
||||
<li>La configurazione si può cambiare in RooCode → Settings → Providers
|
||||
senza toccare il server.</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/servizi/">← Tutti i servizi</a> · <a href="/">Home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.kv { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.kv th, table.kv td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.kv th { width: 10rem; background: #fafafa; }
|
||||
table.kv code { color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: .5rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
72
src/pages/servizi/logseq.astro
Normal file
72
src/pages/servizi/logseq.astro
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Logseq — Servizi di my-vps" description="Appunti e conoscenza in formato testo, con note tracciate su Forgejo.">
|
||||
<h1 class="hero">Logseq</h1>
|
||||
<p class="lead">
|
||||
<strong>Logseq</strong> è un'applicazione per prendere appunti in stile
|
||||
"outliner": tutto è testo collegato, cercabile e versionabile. Qui gira in
|
||||
un container e si usa dal browser.
|
||||
</p>
|
||||
|
||||
<h2>Cosa è</h2>
|
||||
<ul>
|
||||
<li>Logseq desktop eseguito in un container (<code>logseq</code>) gestito
|
||||
da systemd, con accesso in browser tramite <strong>KasmVNC</strong>.</li>
|
||||
<li>Le note vivono come <strong>file Markdown</strong> nella cartella
|
||||
<code>notes</code> del server: niente database proprietario, il contenuto
|
||||
resta tuo e portabile.</li>
|
||||
<li>Struttura standard Logseq: <code>pages/</code>, <code>journals/</code>,
|
||||
<code>assets/</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Come accedere</h2>
|
||||
<table class="kv">
|
||||
<tbody>
|
||||
<tr><th>Indirizzo</th><td><code>https://logseq.194.164.167.80.nip.io/</code></td></tr>
|
||||
<tr><th>Accesso</th><td>solo login unico (<strong>SSO Authelia</strong>): niente altre password.</td></tr>
|
||||
<tr><th>Note</th><td><code>/config/notes</code> — repository <code>logseq-notes</code> su Forgejo.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="note">
|
||||
Alla prima apertura il servizio reindirizza al portale di login
|
||||
(<code>auth.…nip.io</code>); poi il desktop si apre direttamente.
|
||||
</p>
|
||||
|
||||
<h2>Note tracciate con git</h2>
|
||||
<ul>
|
||||
<li>La cartella delle note è un repository git (privato) su
|
||||
<strong>Forgejo</strong>: <code>forgejo-admin/logseq-notes</code>.</li>
|
||||
<li>Le modifiche vengono committate e caricate automaticamente ogni 15
|
||||
minuti (timer <code>git-sync.timer</code>): cronologia completa di ogni
|
||||
nota, ripristinabile in qualsiasi momento.</li>
|
||||
<li>Si può clonare il repository per leggere le note anche senza aprire
|
||||
Logseq.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Perché testo + git</h2>
|
||||
<ul>
|
||||
<li>I file Markdown si aprono con qualunque editor e sono facili da
|
||||
migrare.</li>
|
||||
<li>Il versionamento dà sicurezza: niente perdita di appunti e possibilità
|
||||
di tornare a qualsiasi stato precedente.</li>
|
||||
<li>La conoscenza resta sul server (self-hosted), senza dipendere da
|
||||
servizi esterni.</li>
|
||||
</ul>
|
||||
|
||||
<p class="back"><a href="/servizi/">← Tutti i servizi</a> · <a href="/">Home</a></p>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.hero { font-size: 2.2rem; margin-bottom: .25rem; }
|
||||
.lead { color: #555; margin-bottom: 2rem; }
|
||||
h2 { margin-top: 2rem; font-size: 1.3rem; }
|
||||
table.kv { border-collapse: collapse; width: 100%; font-size: .95rem; }
|
||||
table.kv th, table.kv td { border: 1px solid #eee; padding: .5rem .75rem; text-align: left; vertical-align: top; }
|
||||
table.kv th { width: 10rem; background: #fafafa; }
|
||||
table.kv code { color: #666; }
|
||||
.note { color: #888; font-size: .85rem; margin-top: .5rem; }
|
||||
.back { margin-top: 2.5rem; }
|
||||
.back a { color: var(--accent); text-decoration: none; }
|
||||
</style>
|
||||
3
src/styles/global.css
Normal file
3
src/styles/global.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
html { -webkit-text-size-adjust: 100%; }
|
||||
body { margin: 0; }
|
||||
time { font-variant-numeric: tabular-nums; }
|
||||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/base"
|
||||
}
|
||||
Loading…
Reference in a new issue