Initial import after prompting Claude

This commit is contained in:
Gérald Colangelo
2026-03-16 16:11:19 +01:00
commit abd2e30b26
14 changed files with 1867 additions and 0 deletions

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM python:3.12-slim
WORKDIR /app
# Copie et installation des dépendances d'abord (cache Docker efficace)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copie du reste du code
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]

72
README.md Normal file
View File

@@ -0,0 +1,72 @@
# Ospow — Site web
## Lancement rapide
```bash
# 1. Installer les dépendances Python
pip install -r requirements.txt
# 2. Copier et remplir la configuration
cp .env.example .env
# → éditez .env avec vos infos SMTP
# 3. Lancer le serveur de développement
python app.py
# → http://localhost:5000
```
## Ajouter une nouvelle page
1. Créer `templates/ma_page.html` en commençant par :
```html
{% extends "base.html" %}
{% block title %}Mon titre{% endblock %}
{% block content %}
...votre contenu...
{% endblock %}
```
2. Ajouter une route dans `app.py` :
```python
@app.route("/ma-page")
def ma_page():
return render_template("ma_page.html")
```
3. *(Optionnel)* Ajouter un lien dans `templates/base.html` dans la `<nav>`.
## Structure des fichiers
```
ospow/
├── app.py ← Serveur Flask (routes + formulaire contact)
├── requirements.txt ← Dépendances Python
├── .env.example ← Modèle de configuration
├── templates/
│ ├── base.html ← Header, nav, footer (modifié UNE fois pour toutes)
│ ├── index.html ← Page d'accueil
│ ├── surveillance.html ← Page Vidéosurveillance
│ ← cloud.html ← Page Cloud souverain
│ ├── opensource.html ← Page Open-Source
│ └── contact.html ← Formulaire de contact
└── static/
├── css/style.css ← Tous les styles
├── js/main.js ← Menu burger + animations
└── img/ ← Vos images
```
## Configuration email (formulaire de contact)
Éditez `.env` :
| Variable | Description |
|-----------------|--------------------------------------------|
| `SECRET_KEY` | Clé secrète Flask (changez-la !) |
| `MAIL_SERVER` | Serveur SMTP (ex: `smtp.gmail.com`) |
| `MAIL_PORT` | Port SMTP (587 pour TLS) |
| `MAIL_USERNAME` | Votre adresse email |
| `MAIL_PASSWORD` | Mot de passe (ou mot de passe d'app Gmail) |
| `CONTACT_EMAIL` | Adresse qui reçoit les messages |
> **Gmail** : activez la validation en deux étapes puis créez un
> "Mot de passe d'application" dans les paramètres de votre compte.

138
app.py Normal file
View File

@@ -0,0 +1,138 @@
"""
Point d'entrée principal de l'application Ospow.
Chaque route = une page du site.
Pour ajouter une page : créer templates/ma_page.html et ajouter une route ici.
"""
from flask import Flask, render_template, request, flash, redirect, url_for, Response
from flask_mail import Mail, Message
from datetime import date
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "changez-moi-en-production")
# --- Configuration de l'envoi d'emails ---
app.config["MAIL_SERVER"] = os.environ.get("MAIL_SERVER", "smtp.gmail.com")
app.config["MAIL_PORT"] = int(os.environ.get("MAIL_PORT", 587))
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USERNAME"] = os.environ.get("MAIL_USERNAME", "")
app.config["MAIL_PASSWORD"] = os.environ.get("MAIL_PASSWORD", "")
app.config["MAIL_DEFAULT_SENDER"] = os.environ.get("MAIL_USERNAME", "")
mail = Mail(app)
CONTACT_EMAIL = os.environ.get("CONTACT_EMAIL", "contact@ospow.fr")
# ─── Context global (disponible dans tous les templates) ──────────────────────
@app.context_processor
def inject_globals():
return {"current_year": date.today().year}
# ─── Pages ────────────────────────────────────────────────────────────────────
@app.route("/")
def index():
return render_template("index.html")
@app.route("/surveillance")
def surveillance():
return render_template("surveillance.html")
@app.route("/cloud")
def cloud():
return render_template("cloud.html")
@app.route("/opensource")
def opensource():
return render_template("opensource.html")
@app.route("/equipe")
def equipe():
return render_template("equipe.html")
@app.route("/contact", methods=["GET", "POST"])
def contact():
if request.method == "POST":
nom = request.form.get("nom", "").strip()
email = request.form.get("email", "").strip()
sujet = request.form.get("sujet", "").strip()
message = request.form.get("message", "").strip()
# Validation basique
errors = []
if not nom:
errors.append("Le nom est obligatoire.")
if not email or "@" not in email:
errors.append("Adresse email invalide.")
if not message:
errors.append("Le message ne peut pas être vide.")
if errors:
for e in errors:
flash(e, "error")
return render_template("contact.html", form_data=request.form)
# Envoi de l'email
try:
msg = Message(
subject=f"[Ospow Contact] {sujet or 'Nouveau message'}",
recipients=[CONTACT_EMAIL],
body=f"De : {nom} <{email}>\n\n{message}",
reply_to=email,
)
mail.send(msg)
flash("Votre message a bien été envoyé. Nous vous répondrons rapidement !", "success")
return redirect(url_for("contact"))
except Exception as exc:
app.logger.error("Erreur envoi mail : %s", exc)
flash("Une erreur est survenue lors de l'envoi. Réessayez plus tard.", "error")
return render_template("contact.html", form_data={})
# ─── SEO : sitemap & robots ───────────────────────────────────────────────────
@app.route("/sitemap.xml")
def sitemap():
pages = [
(url_for("index", _external=True), "weekly", "1.0"),
(url_for("surveillance",_external=True), "monthly", "0.9"),
(url_for("cloud", _external=True), "monthly", "0.9"),
(url_for("opensource", _external=True), "monthly", "0.9"),
(url_for("equipe", _external=True), "monthly", "0.7"),
(url_for("contact", _external=True), "monthly", "0.6"),
]
today = date.today().isoformat()
lines = ['<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
for loc, freq, priority in pages:
lines.append(
f" <url><loc>{loc}</loc>"
f"<lastmod>{today}</lastmod>"
f"<changefreq>{freq}</changefreq>"
f"<priority>{priority}</priority></url>"
)
lines.append("</urlset>")
return Response("\n".join(lines), mimetype="application/xml")
@app.route("/robots.txt")
def robots():
txt = (
"User-agent: *\n"
"Allow: /\n"
f"Sitemap: {url_for('sitemap', _external=True)}\n"
)
return Response(txt, mimetype="text/plain")
# ─── Lancement ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)

12
docker-compose.yaml Normal file
View File

@@ -0,0 +1,12 @@
services:
web:
build: .
ports:
- "5000:5000"
# Monte le code source en direct : chaque modification est visible sans rebuild
volumes:
- .:/app
env_file:
- .env
environment:
FLASK_ENV: development

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
Flask>=3.0
Flask-Mail>=0.10
python-dotenv>=1.0

639
static/css/style.css Normal file
View File

@@ -0,0 +1,639 @@
/* ============================================================
OSPOW — Feuille de styles principale
============================================================
Structure :
1. Variables & Reset
2. Typographie
3. Mise en page (container, sections)
4. Header & Navigation
5. Hero (page d'accueil)
6. Page-hero (sous-pages)
7. Cards
8. Features
9. Footer
10. Formulaire de contact
11. Composants utilitaires
12. Media queries (mobile)
============================================================ */
/* ──────────────────────────────────────────────────────────────
1. Variables & Reset
────────────────────────────────────────────────────────────── */
:root {
--color-bg: #F5F7FA;
--color-bg-alt: #EBF0F8;
--color-surface: #FFFFFF;
--color-primary: #0A1628; /* bleu nuit */
--color-secondary: #1E3A5F; /* bleu marine */
--color-accent: #0EA5E9; /* cyan électrique */
--color-accent-dark: #0284C7;
--color-text: #1A2332;
--color-text-muted: #64748B;
--color-border: #D1DCF0;
--color-success: #16A34A;
--color-error: #DC2626;
--font-sans: 'Segoe UI', system-ui, -apple-system, sans-serif;
--radius: 0.5rem;
--radius-lg: 1rem;
--shadow: 0 2px 12px rgba(10,22,40,0.08);
--shadow-lg: 0 8px 32px rgba(10,22,40,0.14);
--transition: 0.2s ease;
--container-width: 1100px;
--header-height: 64px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: var(--font-sans);
font-size: 1rem;
line-height: 1.65;
color: var(--color-text);
background: var(--color-bg);
}
img { max-width: 100%; display: block; }
a { color: var(--color-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
ul, ol { padding-left: 1.25rem; }
/* ──────────────────────────────────────────────────────────────
2. Typographie
────────────────────────────────────────────────────────────── */
h1, h2, h3, h4 {
line-height: 1.2;
font-weight: 700;
color: var(--color-primary);
}
h1 { font-size: clamp(2rem, 5vw, 3rem); }
h2 { font-size: clamp(1.5rem, 3vw, 2rem); }
h3 { font-size: 1.15rem; }
.accent { color: var(--color-accent); }
/* ──────────────────────────────────────────────────────────────
3. Mise en page
────────────────────────────────────────────────────────────── */
.container {
width: min(var(--container-width), 100% - 2rem);
margin-inline: auto;
}
.section {
padding-block: 4rem;
}
.section--alt {
background: var(--color-bg-alt);
}
.section--cta {
background: var(--color-primary);
color: #fff;
}
.section--cta h2,
.section--cta p { color: #fff; }
.section-title {
text-align: center;
margin-bottom: 0.5rem;
}
.section-sub {
text-align: center;
color: var(--color-text-muted);
margin-bottom: 2.5rem;
}
.text-center { text-align: center; }
/* Prose (pages de détail) */
.prose > * + * { margin-top: 1.5rem; }
.prose h2 { margin-top: 2.5rem; }
/* ──────────────────────────────────────────────────────────────
4. Header & Navigation
────────────────────────────────────────────────────────────── */
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--color-primary);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
height: var(--header-height);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
}
/* Logo */
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
color: #fff;
font-size: 1.3rem;
font-weight: 800;
letter-spacing: -0.02em;
text-decoration: none;
}
.logo-icon { color: var(--color-accent); font-size: 1.5rem; }
.logo-text { color: #fff; }
/* Nav links */
.main-nav ul {
list-style: none;
display: flex;
gap: 0.25rem;
padding: 0;
}
.main-nav a {
color: #C8D8EE;
font-size: 0.9rem;
font-weight: 500;
padding: 0.4rem 0.75rem;
border-radius: var(--radius);
transition: background var(--transition), color var(--transition);
text-decoration: none;
}
.main-nav a:hover,
.main-nav a.active {
background: rgba(14,165,233,0.15);
color: #fff;
}
/* Bouton "Contact" dans la nav */
.main-nav li:last-child a {
background: var(--color-accent);
color: #fff;
font-weight: 600;
}
.main-nav li:last-child a:hover {
background: var(--color-accent-dark);
}
/* Burger (caché par défaut) */
.nav-toggle {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.nav-toggle span {
display: block;
width: 24px;
height: 2px;
background: #fff;
border-radius: 2px;
transition: transform var(--transition), opacity var(--transition);
}
/* ──────────────────────────────────────────────────────────────
5. Hero (page d'accueil)
────────────────────────────────────────────────────────────── */
.hero {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
color: #fff;
padding-block: 5rem 4rem;
overflow: hidden;
}
.hero-inner {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 3rem;
}
.hero h1 { color: #fff; }
.hero-lead {
color: #C8D8EE;
font-size: 1.1rem;
margin-top: 1rem;
margin-bottom: 2rem;
max-width: 480px;
}
.hero-cta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* Déco hexagonale (pure CSS) */
.hero-visual { position: relative; height: 280px; }
.hex {
position: absolute;
width: 120px;
height: 120px;
border-radius: 1rem;
opacity: 0.15;
transform: rotate(45deg);
}
.hex--1 { background: var(--color-accent); top: 20px; left: 60px; width: 140px; height: 140px; opacity: 0.2; }
.hex--2 { background: #fff; top: 100px; left: 160px; opacity: 0.08; }
.hex--3 { background: var(--color-accent); top: 60px; left: 240px; width: 80px; height: 80px; }
/* ──────────────────────────────────────────────────────────────
6. Page-hero (sous-pages)
────────────────────────────────────────────────────────────── */
.page-hero {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
color: #fff;
padding-block: 3rem;
text-align: center;
}
.page-hero h1 { color: #fff; }
.page-hero p { color: #C8D8EE; margin-top: 0.5rem; font-size: 1.1rem; }
.page-hero-icon { font-size: 3rem; display: block; margin-bottom: 0.5rem; }
.page-hero--sm { padding-block: 2rem; }
/* ──────────────────────────────────────────────────────────────
7. Cards
────────────────────────────────────────────────────────────── */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.cards--2col { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.cards--3col { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 1.75rem;
box-shadow: var(--shadow);
transition: transform var(--transition), box-shadow var(--transition);
}
.card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
.card--sm { padding: 1.25rem; }
.card-icon { font-size: 2.5rem; margin-bottom: 0.75rem; }
.card h3 { margin-bottom: 0.5rem; }
.card p { color: var(--color-text-muted); font-size: 0.95rem; }
.card-link {
display: inline-block;
margin-top: 1rem;
font-weight: 600;
font-size: 0.9rem;
color: var(--color-accent);
}
/* ──────────────────────────────────────────────────────────────
8. Features
────────────────────────────────────────────────────────────── */
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-top: 1rem;
}
.features--2col { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.feature { text-align: center; }
.feature-icon { font-size: 2.5rem; display: block; margin-bottom: 0.75rem; }
.feature h3 { margin-bottom: 0.25rem; }
.feature p { color: var(--color-text-muted); font-size: 0.95rem; }
/* Steps (ol numéroté stylisé) */
.steps {
list-style: none;
padding: 0;
counter-reset: step;
}
.steps li {
counter-increment: step;
padding: 1rem 1rem 1rem 3.5rem;
position: relative;
border-left: 2px solid var(--color-border);
margin-left: 1rem;
margin-bottom: 1rem;
}
.steps li::before {
content: counter(step);
position: absolute;
left: -1.2rem;
top: 1rem;
width: 2rem;
height: 2rem;
background: var(--color-accent);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.85rem;
}
.steps li strong { display: block; margin-bottom: 0.25rem; }
.steps li p { color: var(--color-text-muted); font-size: 0.95rem; margin: 0; }
/* Tech-list */
.tech-list { list-style: none; padding: 0; }
.tech-list li {
padding: 0.6rem 1rem;
border-left: 3px solid var(--color-accent);
margin-bottom: 0.5rem;
background: var(--color-bg-alt);
border-radius: 0 var(--radius) var(--radius) 0;
}
/* CTA inline */
.cta-block {
background: var(--color-primary);
color: #fff;
border-radius: var(--radius-lg);
padding: 2rem;
text-align: center;
margin-top: 2rem;
}
.cta-block p { color: #C8D8EE; margin-bottom: 1rem; font-size: 1.05rem; }
/* ──────────────────────────────────────────────────────────────
9. Footer
────────────────────────────────────────────────────────────── */
.site-footer {
background: var(--color-primary);
color: #C8D8EE;
padding-top: 3rem;
}
.footer-inner {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
gap: 2rem;
padding-bottom: 2rem;
}
.footer-brand .logo-text { color: #fff; }
.footer-brand p { color: #8AA3C0; font-size: 0.9rem; margin-top: 0.5rem; }
.footer-nav h3,
.footer-contact h3 { color: #fff; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.75rem; }
.footer-nav ul { list-style: none; padding: 0; }
.footer-nav li { margin-bottom: 0.4rem; }
.footer-nav a,
.footer-contact a { color: #8AA3C0; font-size: 0.9rem; }
.footer-nav a:hover,
.footer-contact a:hover { color: #fff; text-decoration: none; }
.footer-contact p { font-size: 0.9rem; margin-bottom: 0.4rem; }
.footer-bottom {
border-top: 1px solid rgba(255,255,255,0.08);
padding-block: 1rem;
text-align: center;
font-size: 0.8rem;
color: #4A637F;
}
/* ──────────────────────────────────────────────────────────────
10. Formulaire de contact
────────────────────────────────────────────────────────────── */
.contact-layout {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 3rem;
align-items: start;
}
.contact-form-wrapper { background: var(--color-surface); border-radius: var(--radius-lg); padding: 2rem; box-shadow: var(--shadow); }
.form-group { margin-bottom: 1.25rem; }
.form-group label { display: block; font-weight: 600; font-size: 0.9rem; margin-bottom: 0.4rem; color: var(--color-text); }
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.6rem 0.85rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
font-family: inherit;
font-size: 1rem;
color: var(--color-text);
background: var(--color-bg);
transition: border-color var(--transition), box-shadow var(--transition);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(14,165,233,0.15);
}
.form-group textarea { resize: vertical; }
.form-note { font-size: 0.8rem; color: var(--color-text-muted); margin-top: 0.75rem; }
.contact-info {
background: var(--color-surface);
border-radius: var(--radius-lg);
padding: 2rem;
box-shadow: var(--shadow);
}
.contact-info h2 { margin-bottom: 0.75rem; font-size: 1.4rem; }
.contact-details { list-style: none; padding: 0; margin: 1rem 0; }
.contact-details li { display: flex; align-items: flex-start; gap: 0.6rem; margin-bottom: 0.75rem; font-size: 0.95rem; }
.contact-icon { font-size: 1.1rem; flex-shrink: 0; }
.contact-promise { background: var(--color-bg-alt); border-radius: var(--radius); padding: 1rem; margin-top: 1.5rem; }
.contact-promise h3 { font-size: 0.95rem; margin-bottom: 0.4rem; }
.contact-promise p { font-size: 0.9rem; color: var(--color-text-muted); line-height: 1.8; }
/* Flash messages */
.flash-messages { margin-block: 1rem; }
.flash {
padding: 0.8rem 1rem;
border-radius: var(--radius);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.flash--success { background: #DCFCE7; color: #166534; border: 1px solid #86EFAC; }
.flash--error { background: #FEE2E2; color: #991B1B; border: 1px solid #FECACA; }
/* ──────────────────────────────────────────────────────────────
11. Boutons
────────────────────────────────────────────────────────────── */
.btn {
display: inline-block;
padding: 0.65rem 1.5rem;
border-radius: var(--radius);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: background var(--transition), color var(--transition), border-color var(--transition);
text-decoration: none;
text-align: center;
}
.btn--primary {
background: var(--color-accent);
color: #fff;
border-color: var(--color-accent);
}
.btn--primary:hover {
background: var(--color-accent-dark);
border-color: var(--color-accent-dark);
text-decoration: none;
}
.btn--ghost {
background: transparent;
color: #fff;
border-color: rgba(255,255,255,0.4);
}
.btn--ghost:hover {
border-color: #fff;
text-decoration: none;
}
.btn--lg { padding: 0.85rem 2rem; font-size: 1.05rem; }
.btn--full { width: 100%; }
/* ──────────────────────────────────────────────────────────────
12. Media queries (mobile)
────────────────────────────────────────────────────────────── */
/* ──────────────────────────────────────────────────────────────
Valeurs (page équipe)
────────────────────────────────────────────────────────────── */
.values {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.value-item {
background: var(--color-bg-alt);
border-radius: var(--radius-lg);
padding: 1.5rem;
border-left: 4px solid var(--color-accent);
}
.value-item span { font-size: 1.8rem; display: block; margin-bottom: 0.5rem; }
.value-item p { font-size: 0.95rem; color: var(--color-text-muted); margin: 0; }
.value-item strong { color: var(--color-primary); }
/* ──────────────────────────────────────────────────────────────
Team cards (page équipe)
────────────────────────────────────────────────────────────── */
.team {
display: flex;
flex-direction: column;
gap: 2rem;
}
.team-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: 2rem;
box-shadow: var(--shadow);
display: grid;
grid-template-columns: 100px 1fr;
gap: 2rem;
align-items: start;
}
.team-avatar { text-align: center; }
.avatar-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: var(--color-secondary);
color: #fff;
font-size: 1.4rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.avatar-placeholder.avatar--teal { background: #0D9488; }
.avatar-placeholder.avatar--orange { background: #D97706; }
.avatar-placeholder.avatar--purple { background: #7C3AED; }
.team-avatar img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto;
}
.team-info h3 { margin-bottom: 0.2rem; }
.team-role { color: var(--color-accent); font-weight: 600; font-size: 0.9rem; margin-bottom: 0.75rem; }
.team-bio { color: var(--color-text-muted); font-size: 0.95rem; margin-bottom: 1rem; }
.team-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.team-tags span {
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
border-radius: 999px;
padding: 0.2rem 0.7rem;
font-size: 0.8rem;
color: var(--color-text-muted);
}
@media (max-width: 600px) {
.team-card { grid-template-columns: 1fr; }
.team-avatar { margin-bottom: 0.5rem; }
}
/* Animation d'apparition au scroll */
.fade-in {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.fade-in.visible {
opacity: 1;
transform: none;
}
/* Burger actif → croix */
.nav-toggle.is-active span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle.is-active span:nth-child(2) { opacity: 0; }
.nav-toggle.is-active span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
/* ──────────────────────────────────────────────────────────────
12. Media queries (mobile)
────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
/* Header mobile */
.nav-toggle { display: flex; }
.main-nav {
display: none;
position: absolute;
top: var(--header-height);
left: 0;
right: 0;
background: var(--color-primary);
padding: 1rem;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
}
.main-nav.is-open { display: block; }
.main-nav ul { flex-direction: column; gap: 0.25rem; }
.main-nav a { display: block; padding: 0.6rem 1rem; }
/* Hero */
.hero-inner { grid-template-columns: 1fr; }
.hero-visual { display: none; }
/* Cards */
.cards,
.cards--2col,
.cards--3col { grid-template-columns: 1fr; }
/* Footer */
.footer-inner { grid-template-columns: 1fr; gap: 1.5rem; }
/* Contact */
.contact-layout { grid-template-columns: 1fr; }
}

67
static/js/main.js Normal file
View File

@@ -0,0 +1,67 @@
/**
* Ospow — JavaScript principal
*
* Fonctionnalités :
* - Menu burger (mobile)
* - Fermeture du menu au clic sur un lien
* - Marquage actif automatique de la nav sur la page courante
*/
document.addEventListener("DOMContentLoaded", function () {
// ─── Menu burger ───────────────────────────────────────────────────────────
const toggle = document.querySelector(".nav-toggle");
const nav = document.querySelector(".main-nav");
if (toggle && nav) {
toggle.addEventListener("click", function () {
const isOpen = nav.classList.toggle("is-open");
toggle.setAttribute("aria-expanded", isOpen);
toggle.setAttribute("aria-label", isOpen ? "Fermer le menu" : "Ouvrir le menu");
// Animation du burger → croix
toggle.classList.toggle("is-active", isOpen);
});
// Ferme le menu si on clique sur un lien (utile sur mobile)
nav.querySelectorAll("a").forEach(function (link) {
link.addEventListener("click", function () {
nav.classList.remove("is-open");
toggle.setAttribute("aria-expanded", "false");
toggle.classList.remove("is-active");
});
});
// Ferme le menu si on clique en dehors
document.addEventListener("click", function (event) {
if (!nav.contains(event.target) && !toggle.contains(event.target)) {
nav.classList.remove("is-open");
toggle.setAttribute("aria-expanded", "false");
toggle.classList.remove("is-active");
}
});
}
// ─── Animation d'apparition au scroll (optionnel) ──────────────────────────
// Utilise IntersectionObserver pour faire apparaître les cards en douceur
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target); // une seule fois
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll(".card, .feature").forEach(function (el) {
el.classList.add("fade-in");
observer.observe(el);
});
}
});

136
templates/base.html Normal file
View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- ── SEO : titre & description ─────────────────────────────────────────── -->
<title>{% block title %}Ospow — Cloud souverain, Vidéosurveillance & Open-Source{% endblock %}</title>
<meta name="description" content="{% block meta_description %}Ospow conçoit et déploie des solutions open-source souveraines : vidéosurveillance IP locale, cloud personnel auto-hébergé, déploiement de logiciels libres pour particuliers et entreprises partout en France.{% endblock %}" />
<link rel="canonical" href="{{ request.host_url.rstrip('/') }}{{ request.path }}" />
<!-- ── Open Graph (Facebook, LinkedIn, WhatsApp…) ────────────────────────── -->
<meta property="og:site_name" content="Ospow" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:type" content="{% block og_type %}website{% endblock %}" />
<meta property="og:title" content="{{ self.title() }}" />
<meta property="og:description" content="{{ self.meta_description() }}" />
<meta property="og:url" content="{{ request.url }}" />
<!-- ── Twitter Card ──────────────────────────────────────────────────────── -->
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ self.title() }}" />
<meta name="twitter:description" content="{{ self.meta_description() }}" />
<!-- ── Feuille de style ──────────────────────────────────────────────────── -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" />
<!-- ── JSON-LD : Organisation (présent sur toutes les pages) ─────────────── -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "Ospow",
"url": "{{ request.host_url.rstrip('/') }}",
"description": "Collectif d'auto-entrepreneurs spécialisés dans les solutions open-source souveraines : vidéosurveillance IP, cloud personnel auto-hébergé et déploiement de logiciels libres.",
"areaServed": "France",
"contactPoint": {
"@type": "ContactPoint",
"email": "contact@ospow.fr",
"contactType": "customer service",
"availableLanguage": "French"
}
}
</script>
<!-- ── JSON-LD spécifique à la page (breadcrumb, service, person…) ────────── -->
{% block json_ld %}{% endblock %}
</head>
<body>
<!-- ═══════════════════════════════ HEADER / NAV ═══════════════════════════ -->
<header class="site-header">
<div class="container header-inner">
<!-- Logo -->
<a href="{{ url_for('index') }}" class="logo" aria-label="Ospow — Accueil">
<span class="logo-icon"></span>
<span class="logo-text">Ospow</span>
</a>
<!-- Burger (mobile) -->
<button class="nav-toggle" aria-label="Ouvrir le menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<!-- Navigation principale -->
<nav class="main-nav" id="main-nav" aria-label="Navigation principale">
<ul>
<li><a href="{{ url_for('index') }}" {% if request.endpoint == 'index' %}class="active" aria-current="page"{% endif %}>Accueil</a></li>
<li><a href="{{ url_for('surveillance') }}" {% if request.endpoint == 'surveillance' %}class="active" aria-current="page"{% endif %}>Vidéosurveillance</a></li>
<li><a href="{{ url_for('cloud') }}" {% if request.endpoint == 'cloud' %}class="active" aria-current="page"{% endif %}>Cloud souverain</a></li>
<li><a href="{{ url_for('opensource') }}" {% if request.endpoint == 'opensource' %}class="active" aria-current="page"{% endif %}>Open-Source</a></li>
<li><a href="{{ url_for('equipe') }}" {% if request.endpoint == 'equipe' %}class="active" aria-current="page"{% endif %}>L'équipe</a></li>
<li><a href="{{ url_for('contact') }}" {% if request.endpoint == 'contact' %}class="active" aria-current="page"{% endif %}>Contact</a></li>
</ul>
</nav>
</div>
</header>
<!-- ═══════════════════════════════ BANNIÈRE (optionnelle) ════════════════ -->
{% block banner %}{% endblock %}
<!-- ═══════════════════════════════ CONTENU PRINCIPAL ════════════════════ -->
<main id="main-content">
<!-- Messages flash (succès / erreur formulaire) -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="container flash-messages" role="alert">
{% for category, message in messages %}
<div class="flash flash--{{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<!-- ═══════════════════════════════ FOOTER ════════════════════════════════ -->
<footer class="site-footer" aria-label="Pied de page">
<div class="container footer-inner">
<div class="footer-brand">
<span class="logo-icon" aria-hidden="true"></span>
<span class="logo-text">Ospow</span>
<p>Solutions numériques souveraines et open-source.</p>
</div>
<nav class="footer-nav" aria-label="Services">
<h3>Services</h3>
<ul>
<li><a href="{{ url_for('surveillance') }}">Vidéosurveillance</a></li>
<li><a href="{{ url_for('cloud') }}">Cloud souverain</a></li>
<li><a href="{{ url_for('opensource') }}">Déploiement Open-Source</a></li>
<li><a href="{{ url_for('equipe') }}">L'équipe</a></li>
</ul>
</nav>
<div class="footer-contact">
<h3>Contact</h3>
<p><a href="{{ url_for('contact') }}">Formulaire de contact</a></p>
<p><a href="mailto:contact@ospow.fr">contact@ospow.fr</a></p>
</div>
</div>
<div class="footer-bottom">
<p>&copy; {{ current_year }} Ospow — Tous droits réservés</p>
</div>
</footer>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

112
templates/cloud.html Normal file
View File

@@ -0,0 +1,112 @@
{% extends "base.html" %}
{% block title %}Cloud Souverain Personnel Auto-hébergé — Nextcloud, Mail, Vaultwarden | Ospow{% endblock %}
{% block meta_description %}Déployez votre propre cloud sur votre matériel ou un serveur français : fichiers (Nextcloud), e-mail souverain, agenda CalDAV et gestionnaire de mots de passe Vaultwarden. Vos données ne quittent jamais votre infrastructure.{% endblock %}
{% block json_ld %}
<script type="application/ld+json">
[
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Accueil", "item": "{{ url_for('index', _external=True) }}" },
{ "@type": "ListItem", "position": 2, "name": "Cloud souverain", "item": "{{ url_for('cloud', _external=True) }}" }
]
},
{
"@context": "https://schema.org",
"@type": "Service",
"name": "Cloud souverain personnel auto-hébergé",
"provider": { "@type": "Organization", "name": "Ospow" },
"description": "Déploiement et maintenance d'une infrastructure cloud personnelle : Nextcloud, serveur mail, CalDAV/CardDAV, Vaultwarden. Hébergement chez vous ou sur serveur dédié français.",
"areaServed": "France",
"serviceType": "Hébergement cloud souverain",
"offers": { "@type": "Offer", "availability": "https://schema.org/InStock", "priceCurrency": "EUR" }
}
]
</script>
{% endblock %}
{% block banner %}
<div class="page-hero">
<div class="container">
<span class="page-hero-icon">☁️</span>
<h1>Cloud souverain personnel</h1>
<p>Google Drive, Dropbox... mais chez vous.</p>
</div>
</div>
{% endblock %}
{% block content %}
<section class="section">
<div class="container prose">
<h2>Vos données vous appartiennent</h2>
<p>
Chaque jour, des milliards de fichiers personnels transitent par des
serveurs dont vous ne contrôlez pas les conditions d'accès. Ospow vous
installe un cloud <strong>hébergé sur votre matériel</strong>, accessible
partout, aussi simple que les outils grand public.
</p>
<div class="cards cards--2col">
<article class="card">
<h3>📁 Fichiers & Photos</h3>
<p>
Synchronisation multi-appareils (PC, Mac, iOS, Android) via
Nextcloud. Interface identique à Google Drive, données chez vous.
</p>
</article>
<article class="card">
<h3>📧 Email souverain</h3>
<p>
Serveur mail complet (Stalwart / Mailcow) avec antispam, DKIM, DMARC.
Votre propre domaine, hébergé par vous.
</p>
</article>
<article class="card">
<h3>📅 Agenda & Contacts</h3>
<p>
CalDAV / CardDAV intégré à Nextcloud. Compatible avec tous vos
appareils sans passer par Google ou Apple.
</p>
</article>
<article class="card">
<h3>🔐 Gestion des mots de passe</h3>
<p>
Déploiement de Vaultwarden (compatible Bitwarden). Un coffre-fort
partageable en famille ou en équipe, sur votre serveur.
</p>
</article>
</div>
<h2>Options d'hébergement</h2>
<div class="features features--2col">
<div class="feature">
<span class="feature-icon">🏠</span>
<h3>Chez vous</h3>
<p>Sur un mini-PC ou un NAS dans votre domicile ou locaux. Investissement unique, aucun abonnement cloud.</p>
</div>
<div class="feature">
<span class="feature-icon">🖥️</span>
<h3>Serveur dédié</h3>
<p>Sur un serveur dédié hébergé en France, choisi par vous. Vous restez propriétaire de vos données.</p>
</div>
</div>
<div class="cta-block">
<p>Prêt à reprendre le contrôle de vos données ?</p>
<a href="{{ url_for('contact') }}" class="btn btn--primary">Demander un devis</a>
</div>
</div>
</section>
{% endblock %}

102
templates/contact.html Normal file
View File

@@ -0,0 +1,102 @@
{% extends "base.html" %}
{% block title %}Contactez Ospow — Devis gratuit & sans engagement{% endblock %}
{% block meta_description %}Discutez de votre projet avec l'équipe Ospow : vidéosurveillance IP, cloud souverain ou déploiement open-source. Premier échange gratuit, réponse sous 48 h. Devis clair et sans jargon.{% endblock %}
{% block json_ld %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ContactPage",
"name": "Contactez Ospow",
"url": "{{ url_for('contact', _external=True) }}",
"description": "Formulaire de contact pour demander un devis ou poser une question à l'équipe Ospow."
}
</script>
{% endblock %}
{% block banner %}
<div class="page-hero page-hero--sm">
<div class="container">
<h1>Contactez-nous</h1>
<p>Un premier échange est toujours gratuit et sans engagement.</p>
</div>
</div>
{% endblock %}
{% block content %}
<section class="section">
<div class="container contact-layout">
<!-- Formulaire -->
<div class="contact-form-wrapper">
<form method="POST" action="{{ url_for('contact') }}" novalidate class="contact-form">
<div class="form-group">
<label for="nom">Votre nom *</label>
<input type="text" id="nom" name="nom"
value="{{ form_data.get('nom', '') }}"
placeholder="Jean Dupont" required />
</div>
<div class="form-group">
<label for="email">Votre adresse email *</label>
<input type="email" id="email" name="email"
value="{{ form_data.get('email', '') }}"
placeholder="jean@exemple.fr" required />
</div>
<div class="form-group">
<label for="sujet">Sujet</label>
<select id="sujet" name="sujet">
<option value="" {% if not form_data.get('sujet') %}selected{% endif %}>— Choisissez un sujet —</option>
<option value="Vidéosurveillance" {% if form_data.get('sujet') == 'Vidéosurveillance' %}selected{% endif %}>Vidéosurveillance</option>
<option value="Cloud souverain" {% if form_data.get('sujet') == 'Cloud souverain' %}selected{% endif %}>Cloud souverain</option>
<option value="Déploiement open-source" {% if form_data.get('sujet') == 'Déploiement open-source' %}selected{% endif %}>Déploiement open-source</option>
<option value="Autre" {% if form_data.get('sujet') == 'Autre' %}selected{% endif %}>Autre</option>
</select>
</div>
<div class="form-group">
<label for="message">Votre message *</label>
<textarea id="message" name="message" rows="6"
placeholder="Décrivez votre projet ou votre question..." required>{{ form_data.get('message', '') }}</textarea>
</div>
<button type="submit" class="btn btn--primary btn--full">Envoyer le message</button>
<p class="form-note">* Champs obligatoires. Vos données sont utilisées uniquement pour répondre à votre demande.</p>
</form>
</div>
<!-- Infos de contact -->
<aside class="contact-info">
<h2>Ospow</h2>
<p>Nous intervenons partout en France, principalement à distance.</p>
<ul class="contact-details">
<li>
<span class="contact-icon">✉️</span>
<a href="mailto:contact@ospow.fr">contact@ospow.fr</a>
</li>
<li>
<span class="contact-icon">⏱️</span>
Réponse sous 48 h ouvrées
</li>
</ul>
<div class="contact-promise">
<h3>Notre engagement</h3>
<p>
Premier échange gratuit &amp; sans engagement.<br>
Devis clair et détaillé.<br>
Pas de jargon inutile.
</p>
</div>
</aside>
</div>
</section>
{% endblock %}

218
templates/equipe.html Normal file
View File

@@ -0,0 +1,218 @@
{% extends "base.html" %}
{% block title %}L'équipe Ospow — Collectif d'experts open-source & sécurité informatique{% endblock %}
{% block meta_description %}Découvrez le collectif Ospow : des auto-entrepreneurs passionnés d'open-source, forts de 20 ans d'expérience cumulée en sécurité des systèmes, infrastructure Linux et déploiement logiciel. Basés en France.{% endblock %}
{% block json_ld %}
<script type="application/ld+json">
[
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Accueil", "item": "{{ url_for('index', _external=True) }}" },
{ "@type": "ListItem", "position": 2, "name": "L'équipe", "item": "{{ url_for('equipe', _external=True) }}" }
]
},
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Gérald Colangelo",
"jobTitle": "Fondateur — RSSI & Expert infrastructure",
"worksFor": { "@type": "Organization", "name": "Ospow" },
"knowsAbout": ["GNU/Linux", "Sécurité réseau", "TCP/IP", "Python", "Firewalling"]
},
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Sofia Martel",
"jobTitle": "Administratrice systèmes & Cloud souverain",
"worksFor": { "@type": "Organization", "name": "Ospow" },
"knowsAbout": ["Proxmox", "Nextcloud", "Ansible", "Docker", "ZFS"]
},
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Théo Lasserre",
"jobTitle": "Expert vidéosurveillance & Domotique",
"worksFor": { "@type": "Organization", "name": "Ospow" },
"knowsAbout": ["Frigate", "ZoneMinder", "ONVIF", "WireGuard"]
},
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Camille Fontaine",
"jobTitle": "Déploiement applicatif & Formatrice",
"worksFor": { "@type": "Organization", "name": "Ospow" },
"knowsAbout": ["Gitea", "Mattermost", "Matomo", "Python", "Formation"]
}
]
</script>
{% endblock %}
{% block banner %}
<div class="page-hero">
<div class="container">
<span class="page-hero-icon">🤝</span>
<h1>Qui sommes-nous ?</h1>
<p>Un collectif passionné, au service de votre souveraineté numérique.</p>
</div>
</div>
{% endblock %}
{% block content %}
<!-- Notre histoire -->
<section class="section">
<div class="container prose">
<h2>Le collectif Ospow</h2>
<p>
Ospow n'est pas une SSII comme les autres. C'est un collectif d'<strong>auto-entrepreneurs
passionnés par le logiciel libre</strong>, réunis autour d'une conviction commune :
la technologie doit servir ses utilisateurs, pas les enfermer.
</p>
<p>
Chacun d'entre nous cumule plusieurs années d'expérience dans des domaines
complémentaires — sécurité des systèmes, infrastructure réseau, administration
Linux, développement — acquises dans des contextes exigeants : industrie,
défense, conseil IT, startups. Nous avons vu de l'intérieur comment les
grandes entreprises gèrent (ou maltraitent) les données de leurs clients.
C'est précisément ce qui nous a conduits à choisir une autre voie.
</p>
<p>
Nous intervenons principalement à distance, sur toute la France, avec un
engagement simple : <strong>pas de jargon inutile, pas de sur-ingénierie,
des solutions qui durent</strong>.
</p>
<div class="values">
<div class="value-item">
<span>📖</span>
<p><strong>Open-source d'abord</strong><br>On n'installe que ce qu'on peut auditer, modifier et redistribuer librement.</p>
</div>
<div class="value-item">
<span>🔒</span>
<p><strong>Souveraineté par défaut</strong><br>Vos données restent sous votre contrôle, toujours.</p>
</div>
<div class="value-item">
<span>🤝</span>
<p><strong>Collectif, pas entreprise</strong><br>Pas de hiérarchie. Chaque membre est libre, responsable et impliqué.</p>
</div>
</div>
</div>
</section>
<!-- Core team -->
<section class="section section--alt">
<div class="container">
<h2 class="section-title">La core team</h2>
<p class="section-sub">Les visages derrière les terminaux.</p>
<div class="team">
<!-- Gérald Colangelo -->
<article class="team-card">
<div class="team-avatar">
{# Remplacez l'URL ci-dessous par une vraie photo (ex: /static/img/gerald.jpg) #}
<div class="avatar-placeholder">GC</div>
</div>
<div class="team-info">
<h3>Gérald Colangelo</h3>
<p class="team-role">Fondateur — Sécurité &amp; Infrastructure</p>
<p class="team-bio">
RSSI de formation et de terrain, Gérald cumule plus de vingt ans
d'expérience dans les systèmes d'information critiques, avec des
passages chez Sopra Steria et ECA Group. Expert GNU/Linux, réseaux
TCP/IP et sécurité périmétrique, il a développé une conviction forte :
la meilleure sécurité est celle que l'on maîtrise de bout en bout.
C'est de là qu'est née Ospow — l'envie de mettre cette expertise au
service de ceux qui refusent de sous-traiter leur souveraineté
numérique à des tiers opaques.
</p>
<div class="team-tags">
<span>GNU/Linux</span><span>Sécurité réseau</span><span>Python</span><span>TCP/IP</span><span>Firewalling</span>
</div>
</div>
</article>
<!-- Sofia Martel (fictif) -->
<article class="team-card">
<div class="team-avatar">
<div class="avatar-placeholder avatar--teal">SM</div>
</div>
<div class="team-info">
<h3>Sofia Martel</h3>
<p class="team-role">Infrastructure &amp; Cloud souverain</p>
<p class="team-bio">
Administratrice systèmes et DevOps depuis dix ans, Sofia est la
référente de l'équipe sur tout ce qui touche à l'auto-hébergement.
Nextcloud, Proxmox, ZFS, Kubernetes bare-metal : elle a tout cassé,
tout réparé, et en a fait une documentation. Elle croit que le droit
à la vie privée devrait être un paramètre par défaut, pas une option payante.
</p>
<div class="team-tags">
<span>Proxmox</span><span>Nextcloud</span><span>Ansible</span><span>Docker</span><span>ZFS</span>
</div>
</div>
</article>
<!-- Théo Lasserre (fictif) -->
<article class="team-card">
<div class="team-avatar">
<div class="avatar-placeholder avatar--orange">TL</div>
</div>
<div class="team-info">
<h3>Théo Lasserre</h3>
<p class="team-role">Vidéosurveillance &amp; Domotique</p>
<p class="team-bio">
Électronicien reconverti dans l'IT, Théo s'est spécialisé dans les
systèmes embarqués et la vidéosurveillance IP. Il a déployé des
installations Frigate et ZoneMinder dans des contextes allant du
logement individuel aux entrepôts industriels. Partisan du matériel
réparable, il évite soigneusement les caméras à abonnement obligatoire
et les NVR dont le firmware n'est pas auditable.
</p>
<div class="team-tags">
<span>Frigate</span><span>ZoneMinder</span><span>ONVIF</span><span>WireGuard</span><span>Raspberry Pi</span>
</div>
</div>
</article>
<!-- Camille Fontaine (fictif) -->
<article class="team-card">
<div class="team-avatar">
<div class="avatar-placeholder avatar--purple">CF</div>
</div>
<div class="team-info">
<h3>Camille Fontaine</h3>
<p class="team-role">Déploiement applicatif &amp; Formation</p>
<p class="team-bio">
Développeuse fullstack et formatrice occasionnelle, Camille est la
personne qui traduit les besoins métier en solutions concrètes.
Elle choisit les bons outils open-source parmi les dizaines disponibles,
les configure pour qu'ils soient maintenables, puis forme les équipes
pour qu'elles gagnent en autonomie. Sa règle d'or : une solution
que le client ne comprend pas est une solution ratée.
</p>
<div class="team-tags">
<span>Gitea</span><span>Mattermost</span><span>Matomo</span><span>Python</span><span>Formation</span>
</div>
</div>
</article>
</div>
</div>
</section>
<!-- Rejoindre -->
<section class="section section--cta">
<div class="container text-center">
<h2>Vous partagez nos valeurs ?</h2>
<p>Le collectif est ouvert. Si vous êtes passionné d'open-source et que vous voulez contribuer, parlons-en.</p>
<a href="{{ url_for('contact') }}" class="btn btn--primary btn--lg">Nous écrire</a>
</div>
</section>
{% endblock %}

129
templates/index.html Normal file
View File

@@ -0,0 +1,129 @@
{% extends "base.html" %}
{% block title %}Ospow — Cloud souverain, Vidéosurveillance IP & Open-Source en France{% endblock %}
{% block meta_description %}Ospow : collectif d'experts open-source basé en France. Nous déployons votre cloud personnel auto-hébergé, vos systèmes de vidéosurveillance IP et vos solutions logicielles libres. Premier échange gratuit.{% endblock %}
{% block json_ld %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Ospow",
"url": "{{ request.host_url.rstrip('/') }}",
"description": "Collectif open-source spécialisé en cloud souverain, vidéosurveillance IP et déploiement de logiciels libres en France."
}
</script>
{% endblock %}
{% block banner %}
<section class="hero">
<div class="container hero-inner">
<div class="hero-text">
<h1>Votre numérique,<br><span class="accent">sous votre contrôle.</span></h1>
<p class="hero-lead">
Ospow conçoit et déploie des solutions open-source souveraines pour les
particuliers et les entreprises qui refusent de confier leurs données aux
géants du cloud.
</p>
<div class="hero-cta">
<a href="{{ url_for('contact') }}" class="btn btn--primary">Demander un devis</a>
<a href="#services" class="btn btn--ghost">Nos services</a>
</div>
</div>
<div class="hero-visual" aria-hidden="true">
<div class="hero-hexgrid">
<div class="hex hex--1"></div>
<div class="hex hex--2"></div>
<div class="hex hex--3"></div>
</div>
</div>
</div>
</section>
{% endblock %}
{% block content %}
<!-- Services -->
<section id="services" class="section">
<div class="container">
<h2 class="section-title">Nos solutions</h2>
<p class="section-sub">Trois domaines d'expertise, une seule philosophie : vous gardez la main.</p>
<div class="cards">
<article class="card">
<div class="card-icon">📷</div>
<h3>Vidéosurveillance</h3>
<p>
Installation et gestion de systèmes de surveillance IP sur mesure,
avec stockage local ou hébergé chez vous. Pas de cloud tiers, pas
d'abonnement imposé.
</p>
<a href="{{ url_for('surveillance') }}" class="card-link">En savoir plus →</a>
</article>
<article class="card">
<div class="card-icon">☁️</div>
<h3>Cloud souverain personnel</h3>
<p>
Vos fichiers, contacts, agenda et mails hébergés sur votre propre
infrastructure. Reprenez le contrôle de vos données personnelles avec
des outils éprouvés.
</p>
<a href="{{ url_for('cloud') }}" class="card-link">En savoir plus →</a>
</article>
<article class="card">
<div class="card-icon">⚙️</div>
<h3>Déploiement Open-Source</h3>
<p>
Installation, configuration et maintenance sur mesure de solutions
open-source pour votre entreprise. On s'occupe de tout, vous profitez
du résultat.
</p>
<a href="{{ url_for('opensource') }}" class="card-link">En savoir plus →</a>
</article>
</div>
</div>
</section>
<!-- Pourquoi Ospow -->
<section class="section section--alt">
<div class="container">
<h2 class="section-title">Pourquoi Ospow ?</h2>
<div class="features">
<div class="feature">
<span class="feature-icon">🔒</span>
<h3>Souveraineté</h3>
<p>Vos données ne quittent jamais votre périmètre de confiance.</p>
</div>
<div class="feature">
<span class="feature-icon">🛠️</span>
<h3>Sur mesure</h3>
<p>Chaque déploiement est adapté à vos besoins réels, sans sur-ingénierie.</p>
</div>
<div class="feature">
<span class="feature-icon">🤝</span>
<h3>Accompagnement</h3>
<p>Formation, documentation et support : vous n'êtes jamais seul.</p>
</div>
<div class="feature">
<span class="feature-icon">📖</span>
<h3>Open-Source</h3>
<p>Pas de boîte noire. Tout ce qu'on déploie est auditable et pérenne.</p>
</div>
</div>
</div>
</section>
<!-- CTA final -->
<section class="section section--cta">
<div class="container text-center">
<h2>Un projet en tête ?</h2>
<p>Parlons-en. Un premier échange est toujours gratuit.</p>
<a href="{{ url_for('contact') }}" class="btn btn--primary btn--lg">Nous contacter</a>
</div>
</section>
{% endblock %}

121
templates/opensource.html Normal file
View File

@@ -0,0 +1,121 @@
{% extends "base.html" %}
{% block title %}Déploiement & Maintenance de Solutions Open-Source sur Mesure | Ospow{% endblock %}
{% block meta_description %}Ospow installe et maintient pour vous les meilleurs logiciels open-source : gestion de projet (Gitea, Plane), messagerie (Mattermost), analytics (Matomo), monitoring (Grafana). Formation incluse, sans jargon.{% endblock %}
{% block json_ld %}
<script type="application/ld+json">
[
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Accueil", "item": "{{ url_for('index', _external=True) }}" },
{ "@type": "ListItem", "position": 2, "name": "Déploiement Open-Source", "item": "{{ url_for('opensource', _external=True) }}" }
]
},
{
"@context": "https://schema.org",
"@type": "Service",
"name": "Déploiement et maintenance de logiciels open-source",
"provider": { "@type": "Organization", "name": "Ospow" },
"description": "Installation, configuration, migration et maintenance sur mesure de solutions open-source pour entreprises et associations : outils collaboratifs, analytics, monitoring, e-commerce.",
"areaServed": "France",
"serviceType": "Déploiement logiciel open-source",
"offers": { "@type": "Offer", "availability": "https://schema.org/InStock", "priceCurrency": "EUR" }
}
]
</script>
{% endblock %}
{% block banner %}
<div class="page-hero">
<div class="container">
<span class="page-hero-icon">⚙️</span>
<h1>Déploiement Open-Source</h1>
<p>Les meilleurs outils du monde libre, installés et maintenus pour vous.</p>
</div>
</div>
{% endblock %}
{% block content %}
<section class="section">
<div class="container prose">
<h2>L'open-source sans la complexité</h2>
<p>
Les logiciels open-source sont souvent les plus fiables, les plus sécurisés
et les plus évolutifs. Mais les installer et les maintenir demande du temps et
des compétences. <strong>Ospow s'en charge</strong>, de l'installation initiale
à la maintenance longue durée.
</p>
<h2>Exemples de solutions déployées</h2>
<div class="cards cards--3col">
<article class="card card--sm">
<h3>🗂️ Gestion de projet</h3>
<p>Plane, Taiga, Gitea — alternatives souveraines à Jira, GitHub, Trello.</p>
</article>
<article class="card card--sm">
<h3>💬 Communication</h3>
<p>Mattermost, Matrix/Element — messagerie d'équipe hébergée chez vous.</p>
</article>
<article class="card card--sm">
<h3>📊 Analytics</h3>
<p>Matomo, Plausible — statistiques web sans RGPD headaches.</p>
</article>
<article class="card card--sm">
<h3>🔍 Monitoring</h3>
<p>Grafana + Prometheus — supervision de vos serveurs et applications.</p>
</article>
<article class="card card--sm">
<h3>📝 Wiki & Docs</h3>
<p>BookStack, Wiki.js — base de connaissances interne pour votre équipe.</p>
</article>
<article class="card card--sm">
<h3>🛒 E-commerce</h3>
<p>PrestaShop, WooCommerce — boutique en ligne sans commission plateforme.</p>
</article>
</div>
<h2>Notre méthode</h2>
<ol class="steps">
<li>
<strong>Audit de vos besoins</strong>
<p>On commence par comprendre votre activité et vos contraintes avant de proposer quoi que ce soit.</p>
</li>
<li>
<strong>Choix de la solution</strong>
<p>On sélectionne ensemble l'outil le plus adapté, en évitant la sur-ingénierie.</p>
</li>
<li>
<strong>Déploiement</strong>
<p>Installation sur votre serveur, configuration sécurisée, migration des données existantes.</p>
</li>
<li>
<strong>Formation</strong>
<p>On vous montre comment utiliser et administrer votre nouvel outil.</p>
</li>
<li>
<strong>Maintenance</strong>
<p>Mises à jour, sauvegardes, supervision — on reste disponibles sur le long terme.</p>
</li>
</ol>
<div class="cta-block">
<p>Un outil en particulier vous intéresse ?</p>
<a href="{{ url_for('contact') }}" class="btn btn--primary">Parlons de votre projet</a>
</div>
</div>
</section>
{% endblock %}

104
templates/surveillance.html Normal file
View File

@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block title %}Vidéosurveillance IP sur mesure — Stockage local sans abonnement | Ospow{% endblock %}
{% block meta_description %}Installation de systèmes de vidéosurveillance IP avec stockage local (Frigate, ZoneMinder) : vos images restent sur votre réseau, sans cloud tiers ni abonnement mensuel. Accès à distance sécurisé via VPN.{% endblock %}
{% block json_ld %}
<script type="application/ld+json">
[
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Accueil", "item": "{{ url_for('index', _external=True) }}" },
{ "@type": "ListItem", "position": 2, "name": "Vidéosurveillance", "item": "{{ url_for('surveillance', _external=True) }}" }
]
},
{
"@context": "https://schema.org",
"@type": "Service",
"name": "Vidéosurveillance IP sur mesure",
"provider": { "@type": "Organization", "name": "Ospow" },
"description": "Installation et configuration de systèmes de vidéosurveillance IP open-source avec stockage local, accès distant chiffré et alertes intelligentes.",
"areaServed": "France",
"serviceType": "Vidéosurveillance",
"offers": { "@type": "Offer", "availability": "https://schema.org/InStock", "priceCurrency": "EUR" }
}
]
</script>
{% endblock %}
{% block banner %}
<div class="page-hero">
<div class="container">
<span class="page-hero-icon">📷</span>
<h1>Vidéosurveillance</h1>
<p>Des yeux sur votre bien, des données chez vous.</p>
</div>
</div>
{% endblock %}
{% block content %}
<section class="section">
<div class="container prose">
<h2>Surveillez sans dépendre</h2>
<p>
La plupart des systèmes de caméras du marché envoient vos flux vidéo vers
des serveurs aux États-Unis ou en Asie. Chez Ospow, nous déployons des
solutions où <strong>chaque image reste sur votre réseau</strong>.
</p>
<div class="cards cards--2col">
<article class="card">
<h3>📦 Installation clé en main</h3>
<p>
Choix du matériel, câblage, configuration du NVR (enregistreur réseau)
et des caméras IP. Nous intervenons chez vous ou à distance.
</p>
</article>
<article class="card">
<h3>🗄️ Stockage local</h3>
<p>
Enregistrement sur disque dur local ou NAS. Pas d'abonnement mensuel,
pas de données chez un tiers.
</p>
</article>
<article class="card">
<h3>📱 Accès distant sécurisé</h3>
<p>
Consultez vos caméras depuis votre téléphone via un tunnel chiffré
(WireGuard / VPN). Personne d'autre n'y accède.
</p>
</article>
<article class="card">
<h3>🔔 Alertes intelligentes</h3>
<p>
Détection de mouvement, notifications push, enregistrement à la
demande. Tout configuré selon vos habitudes.
</p>
</article>
</div>
<h2>Logiciels utilisés</h2>
<ul class="tech-list">
<li><strong>Frigate</strong> — NVR open-source avec détection d'objets par IA locale</li>
<li><strong>MotionEye / MotionEyeOS</strong> — Interface légère pour petites installations</li>
<li><strong>ZoneMinder</strong> — Solution complète pour les grandes installations</li>
</ul>
<div class="cta-block">
<p>Vous avez un projet de vidéosurveillance ?</p>
<a href="{{ url_for('contact') }}" class="btn btn--primary">Discutons-en</a>
</div>
</div>
</section>
{% endblock %}