Split codice/contenuti: repo pubblico (codice) — articoli in astro-blog-content (privato)
Some checks failed
deploy-blog / deploy (push) Failing after 0s

This commit is contained in:
forgejo-admin 2026-08-10 02:37:19 +00:00
commit cb8da8fa6d
30 changed files with 1311 additions and 0 deletions

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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, '-');
}

View file

14
src/content/config.ts Normal file
View 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 };

View 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>

View 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
View 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
View 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
View 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>

View 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-&lt;nome&gt;.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>

View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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/&lt;utente&gt;/&lt;repo&gt;.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>

View 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>

View 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
View 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
View file

@ -0,0 +1,3 @@
{
"extends": "astro/tsconfigs/base"
}