Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s

Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA),
apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/;
infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e
workflow Gitea Actions per il deploy via SSH a ogni push su master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
This commit is contained in:
Lorenzo Sanesi
2026-10-20 09:30:48 +02:00
co-authored by Claude Opus 5.5
commit b83ce039fb
505 changed files with 84544 additions and 0 deletions
+300
View File
@@ -0,0 +1,300 @@
// Base globale condivisa da apps/public e apps/private: custom properties dai token, reset essenziale,
// tipografia, utility di layout, pulsanti e form. Ogni app la include dal proprio styles.scss con `@use 'base';`.
@use 'sass:map';
@use 'tokens';
@use 'mixins' as mq;
:root {
@each $name, $value in tokens.$colors {
--color-#{$name}: #{$value};
}
@each $name, $value in tokens.$font-sizes {
--font-size-#{$name}: #{$value};
}
@each $name, $value in tokens.$line-heights {
--line-height-#{$name}: #{$value};
}
@each $name, $value in tokens.$font-weights {
--font-weight-#{$name}: #{$value};
}
@each $name, $value in tokens.$spaces {
--space-#{$name}: #{$value};
}
@each $name, $value in tokens.$radii {
--radius-#{$name}: #{$value};
}
@each $name, $value in tokens.$shadows {
--shadow-#{$name}: #{$value};
}
--font-sans: #{tokens.$font-sans};
--font-mono: #{tokens.$font-mono};
--container-max: #{tokens.$container-max};
--header-height: #{tokens.$header-height};
--gutter: var(--space-4);
color-scheme: light;
@include mq.up(md) {
--gutter: var(--space-5);
}
}
// --- Reset essenziale ---
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
scroll-padding-top: calc(var(--header-height) + var(--space-4));
}
@media (prefers-reduced-motion: no-preference) {
html {
scroll-behavior: smooth;
}
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--font-sans);
font-size: var(--font-size-md);
line-height: var(--line-height-normal);
color: var(--color-ink);
background: var(--color-surface);
-webkit-font-smoothing: antialiased;
}
img,
svg,
video {
display: block;
max-width: 100%;
height: auto;
}
input,
button,
textarea,
select {
font: inherit;
}
// --- Tipografia ---
h1,
h2,
h3,
h4 {
margin: 0 0 var(--space-4);
line-height: var(--line-height-tight);
font-weight: var(--font-weight-bold);
color: var(--color-ink);
text-wrap: balance;
}
h1 {
font-size: var(--font-size-4xl);
}
h2 {
font-size: var(--font-size-3xl);
}
h3 {
font-size: var(--font-size-2xl);
}
h4 {
font-size: var(--font-size-xl);
}
p {
margin: 0 0 var(--space-4);
}
a {
color: var(--color-primary);
text-underline-offset: 0.15em;
&:hover {
color: var(--color-primary-strong);
}
@include mq.focus-ring;
}
button {
@include mq.focus-ring;
}
// --- Utility ---
.container {
width: 100%;
max-width: var(--container-max);
margin-inline: auto;
padding-inline: var(--gutter);
}
.visually-hidden {
@include mq.visually-hidden;
}
.lead {
font-size: var(--font-size-lg);
color: var(--color-ink-muted);
max-width: 65ch;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
min-height: 44px;
padding: var(--space-2) var(--space-5);
border: 2px solid transparent;
border-radius: var(--radius-pill);
font-weight: var(--font-weight-semibold);
text-decoration: none;
cursor: pointer;
transition:
background-color 0.15s ease,
color 0.15s ease;
&--primary {
background: var(--color-primary);
color: var(--color-on-primary);
&:hover {
background: var(--color-primary-strong);
color: var(--color-on-primary);
}
}
&--accent {
background: var(--color-accent);
color: var(--color-on-accent);
&:hover {
background: #{map.get(tokens.$colors, 'accent')};
color: var(--color-on-accent);
filter: brightness(0.95);
}
}
&--outline {
border-color: currentColor;
background: transparent;
color: var(--color-primary);
}
}
// --- Form ---
// Campi: <app-form-field> (shared/form-field.ts) ha già la classe .form-field.
.form-grid {
display: grid;
gap: var(--space-4);
@include mq.up(md) {
grid-template-columns: 1fr 1fr;
> .span-2 {
grid-column: 1 / -1;
}
}
}
.form-field {
display: flex;
flex-direction: column;
gap: var(--space-1);
label {
font-weight: var(--font-weight-semibold);
}
.optional {
font-weight: var(--font-weight-regular);
color: var(--color-ink-muted);
}
.hint {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-ink-muted);
}
input,
select,
textarea {
min-height: 44px;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-ink-muted);
border-radius: var(--radius-md);
background: var(--color-surface);
color: var(--color-ink);
font: inherit;
&[aria-invalid='true'] {
border-color: var(--color-danger);
box-shadow: 0 0 0 1px var(--color-danger);
}
}
.error {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-danger);
}
}
.field-error {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-danger);
}
fieldset {
// min-width: 0 → il contenuto largo (es. fila di giorni scorrevole) non allarga il fieldset (default min-content)
min-width: 0;
margin: 0 0 var(--space-5);
padding: 0;
border: 0;
legend {
margin-bottom: var(--space-3);
padding: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
}
}
// Casella o radio con etichetta, area cliccabile ≥ 44 px
.check {
display: flex;
align-items: flex-start;
gap: var(--space-3);
min-height: 44px;
padding-block: var(--space-2);
cursor: pointer;
input {
flex: none;
width: 1.25rem;
height: 1.25rem;
margin: 0.15rem 0 0;
accent-color: var(--color-primary);
}
}
.alert {
margin: 0 0 var(--space-4);
padding: var(--space-3) var(--space-4);
border-left: 4px solid currentColor;
border-radius: var(--radius-md);
background: var(--color-surface-alt);
&--error {
color: var(--color-danger);
}
&--success {
color: var(--color-success);
}
}
+35
View File
@@ -0,0 +1,35 @@
@use 'sass:map';
@use 'tokens';
/// Media query mobile-first: stili applicati da quel breakpoint in su.
/// @example @include mq.up(md) { … }
@mixin up($name) {
$value: map.get(tokens.$breakpoints, $name);
@if not $value {
@error "Breakpoint sconosciuto: #{$name}";
}
@media (min-width: $value) {
@content;
}
}
/// Testo leggibile solo dagli screen reader.
@mixin visually-hidden {
position: absolute !important;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/// Anello di focus visibile e coerente (solo da tastiera).
@mixin focus-ring {
&:focus-visible {
outline: 3px solid var(--color-focus);
outline-offset: 2px;
}
}
+104
View File
@@ -0,0 +1,104 @@
// Tipografia del corpo degli articoli del blog: HTML generato dal markdown dall'API (marked + sanitize-html).
// Usata dal sito pubblico (pagina articolo) e dall'anteprima dell'editor admin: stesso aspetto nei due punti.
// Il contenuto è inserito con [innerHTML], senza attributi di incapsulamento: includere dentro `::ng-deep`.
/// Stili del contenitore (larghezza di lettura, corpo del testo).
@mixin container {
max-width: 72ch;
font-size: var(--font-size-lg);
line-height: var(--line-height-normal);
}
/// Stili degli elementi generati dal markdown.
/// @example .prose { @include prose.container; } .prose ::ng-deep { @include prose.content; }
@mixin content {
h2,
h3,
h4 {
margin-top: var(--space-6);
margin-bottom: var(--space-3);
}
h2 {
font-size: var(--font-size-2xl);
}
h3 {
font-size: var(--font-size-xl);
}
p,
ul,
ol,
blockquote,
pre,
table,
figure {
margin-block: 0 var(--space-4);
}
li + li {
margin-top: var(--space-2);
}
img {
display: block;
max-width: 100%;
height: auto;
border-radius: var(--radius-md);
}
figure {
margin-inline: 0;
}
figcaption {
margin-top: var(--space-2);
font-size: var(--font-size-sm);
color: var(--color-ink-muted);
}
blockquote {
margin-inline: 0;
padding: var(--space-3) var(--space-5);
border-left: 4px solid var(--color-primary);
background: var(--color-primary-soft);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
p:last-child {
margin-bottom: 0;
}
}
code {
font-family: var(--font-mono);
font-size: 0.9em;
padding: 0.1em 0.3em;
border-radius: var(--radius-sm);
background: var(--color-surface-alt);
}
pre {
overflow-x: auto;
padding: var(--space-4);
border-radius: var(--radius-md);
background: var(--color-surface-alt);
font-size: var(--font-size-sm);
code {
padding: 0;
background: none;
}
}
table {
display: block;
max-width: 100%;
overflow-x: auto;
border-collapse: collapse;
font-size: var(--font-size-md);
}
th,
td {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-line);
text-align: left;
}
th {
background: var(--color-surface-alt);
}
hr {
margin-block: var(--space-6);
border: 0;
border-top: 1px solid var(--color-line);
}
}
+105
View File
@@ -0,0 +1,105 @@
// Design tokens condivisi da apps/public e apps/private.
// I valori vivono come custom properties CSS (:root, generate in _base.scss): i componenti usano var(--…).
// Le mappe SCSS servono per generarle e per i breakpoint (le media query non accettano var()).
// --- Colori ---
// Palette: blu acqua (brand, coerente con le email: #0b5c7a), verde sostenibilità, ambra energia solare.
$colors: (
// Brand
'primary': #0b5c7a,
'primary-strong': #08445b,
'primary-soft': #e6f2f6,
'secondary': #1f7a4d,
'secondary-soft': #e8f5ee,
'accent': #f2a900,
'accent-soft': #fff6dc,
// Macro-aree (badge, icone, bordi)
'area-acqua': #0e7fa6,
'area-energia': #c98a00,
'area-comfort': #c2410c,
'area-mobilita': #1f7a4d,
'area-industria': #475569,
// Neutri
'ink': #12212b,
'ink-muted': #4a5a66,
'line': #d9e1e6,
'surface': #ffffff,
'surface-alt': #f4f7f9,
'on-primary': #ffffff,
'on-accent': #12212b,
// Stati
'success': #1f7a4d,
'warning': #9a6700,
'danger': #b42318,
'focus': #f2a900,
);
// --- Tipografia ---
// Font di sistema: nessuna richiesta a servizi esterni (GDPR, prestazioni). Si potrà adottare un font self-hosted.
$font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
$font-mono: ui-monospace, 'Cascadia Code', 'SFMono-Regular', Menlo, Consolas, monospace;
// Scala tipografica fluida (mobile → desktop)
$font-sizes: (
'xs': 0.8125rem,
'sm': 0.875rem,
'md': 1rem,
'lg': 1.125rem,
'xl': clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem),
'2xl': clamp(1.5rem, 1.3rem + 1vw, 2rem),
'3xl': clamp(1.875rem, 1.5rem + 1.8vw, 2.75rem),
'4xl': clamp(2.25rem, 1.7rem + 2.6vw, 3.5rem),
);
$line-heights: (
'tight': 1.2,
'snug': 1.35,
'normal': 1.6,
);
$font-weights: (
'regular': 400,
'medium': 500,
'semibold': 600,
'bold': 700,
);
// --- Spaziature (scala 4px) ---
$spaces: (
'0': 0,
'1': 0.25rem,
'2': 0.5rem,
'3': 0.75rem,
'4': 1rem,
'5': 1.5rem,
'6': 2rem,
'7': 3rem,
'8': 4rem,
'9': 6rem,
);
// --- Forme ed effetti ---
$radii: (
'sm': 4px,
'md': 8px,
'lg': 16px,
'pill': 999px,
);
$shadows: (
'sm': 0 1px 2px rgb(18 33 43 / 8%),
'md': 0 4px 12px rgb(18 33 43 / 10%),
'lg': 0 12px 32px rgb(18 33 43 / 14%),
);
// --- Layout ---
$container-max: 1200px;
$header-height: 64px;
// Breakpoint mobile-first (min-width)
$breakpoints: (
'sm': 576px,
'md': 768px,
'lg': 1024px,
'xl': 1280px,
);