From ef71d70b44b7da69eb1b55a05d1f506242bb7075 Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 5 Apr 2026 18:54:15 +0200 Subject: [PATCH] filling --- .../API_Specs/CONVENTIONS_OPENAPI.md | 500 ++++++ .../Rust_Engines/OVERVIEW_RUST_MODULES.md | 386 +++++ .../Auth_&_Core/SPEC_AUTH_CORE.md | 464 +++++ .../Formation/SPEC_TECHNIQUE_FORMATION.md | 469 +++++ .../Partage/SPEC_TECHNIQUE_PARTAGE.md | 447 +++++ .../Community/Troc/SPEC_TECHNIQUE_TROC.md | 427 +++++ .../Personal/Cloud_Perso/SPEC_CLOUD_PERSO.md | 437 +++++ .../Shop/Backend/SPEC_BACKEND_SHOP.md | 442 +++++ .../Shop/Frontend/SPEC_FRONTEND_SHOP.md | 367 ++++ .../Shop/Paiement/SPEC_PAIEMENT.md | 404 +++++ .../WORKBOOK_IMPRIMABLE/00_MODE_EMPLOI.md | 109 ++ .../01_MANIFESTE_PERSONNALITE.md | 127 ++ .../02_TERRITOIRE_MARQUE.md | 124 ++ .../03_MOODBOARD_AMBIANCE.md | 94 ++ .../04_MOODBOARD_MATIERES_GESTE.md | 133 ++ .../WORKBOOK_IMPRIMABLE/05_LOGO_BRAINSTORM.md | 137 ++ .../06_LOGO_CROQUIS_SYMBOLE.md | 187 ++ .../07_LOGO_SYNTHESE_DECLINAISONS.md | 209 +++ .../08_PALETTE_EXPLORATION.md | 131 ++ .../09_PALETTE_COMBINAISONS.md | 153 ++ .../10_TYPOGRAPHIE_TEST.md | 168 ++ .../WORKBOOK_IMPRIMABLE/11_ICONES_CROQUIS.md | 149 ++ .../WORKBOOK_IMPRIMABLE/12_DIRECTION_PHOTO.md | 190 +++ .../WORKBOOK_IMPRIMABLE/13_IDENTITE_SONORE.md | 165 ++ .../WORKBOOK_IMPRIMABLE/14_SYNTHESE_FINALE.md | 164 ++ .../SPEC_DEPOT_SAMPLES.md | 307 ++++ .../SPEC_TROC_COMMUNAUTAIRE.md | 276 +++ .../SPEC_EVENEMENTS.md | 304 ++++ talas-wiki/static/style.css | 1503 +++++++++-------- talas-wiki/templates/base.html | 20 +- 30 files changed, 8309 insertions(+), 684 deletions(-) create mode 100644 03_APPS_&_SERVICES/APIs_&_Rust_Modules/API_Specs/CONVENTIONS_OPENAPI.md create mode 100644 03_APPS_&_SERVICES/APIs_&_Rust_Modules/Rust_Engines/OVERVIEW_RUST_MODULES.md create mode 100644 03_APPS_&_SERVICES/Auth_&_Core/SPEC_AUTH_CORE.md create mode 100644 03_APPS_&_SERVICES/Community/Formation/SPEC_TECHNIQUE_FORMATION.md create mode 100644 03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE.md create mode 100644 03_APPS_&_SERVICES/Community/Troc/SPEC_TECHNIQUE_TROC.md create mode 100644 03_APPS_&_SERVICES/Personal/Cloud_Perso/SPEC_CLOUD_PERSO.md create mode 100644 03_APPS_&_SERVICES/Shop/Backend/SPEC_BACKEND_SHOP.md create mode 100644 03_APPS_&_SERVICES/Shop/Frontend/SPEC_FRONTEND_SHOP.md create mode 100644 03_APPS_&_SERVICES/Shop/Paiement/SPEC_PAIEMENT.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/00_MODE_EMPLOI.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/01_MANIFESTE_PERSONNALITE.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/02_TERRITOIRE_MARQUE.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/03_MOODBOARD_AMBIANCE.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/04_MOODBOARD_MATIERES_GESTE.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/05_LOGO_BRAINSTORM.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/06_LOGO_CROQUIS_SYMBOLE.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/07_LOGO_SYNTHESE_DECLINAISONS.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/08_PALETTE_EXPLORATION.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/09_PALETTE_COMBINAISONS.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/10_TYPOGRAPHIE_TEST.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/11_ICONES_CROQUIS.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/12_DIRECTION_PHOTO.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/13_IDENTITE_SONORE.md create mode 100644 05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/14_SYNTHESE_FINALE.md create mode 100644 06_COMMUNAUTE_ECOSYSTEME/Partage_Samples_Presets/SPEC_DEPOT_SAMPLES.md create mode 100644 06_COMMUNAUTE_ECOSYSTEME/Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE.md create mode 100644 06_COMMUNAUTE_ECOSYSTEME/Événements_Participatifs/SPEC_EVENEMENTS.md diff --git a/03_APPS_&_SERVICES/APIs_&_Rust_Modules/API_Specs/CONVENTIONS_OPENAPI.md b/03_APPS_&_SERVICES/APIs_&_Rust_Modules/API_Specs/CONVENTIONS_OPENAPI.md new file mode 100644 index 0000000..5321f79 --- /dev/null +++ b/03_APPS_&_SERVICES/APIs_&_Rust_Modules/API_Specs/CONVENTIONS_OPENAPI.md @@ -0,0 +1,500 @@ +# Conventions OpenAPI — Veza + +> Comment écrire, organiser et publier les spécifications OpenAPI des APIs Veza. +> Standard : **OpenAPI 3.1** +> Dernière mise à jour : avril 2026. + +--- + +## 1. Pourquoi OpenAPI + +- **Source de vérité** : un contrat API sans ambiguïté entre backend/frontend/mobile +- **Génération client** : SDKs TypeScript/Go/Rust auto-générés +- **Documentation vivante** : Swagger UI / Redoc auto-déployé +- **Tests contractuels** : vérification que l'implémentation respecte le contrat +- **Pas de dérive** : impossible de merger un code qui ne respecte pas le spec + +--- + +## 2. Organisation des fichiers + +### 2.1 Structure + +``` +API_Specs/ +├── CONVENTIONS_OPENAPI.md # ce document +├── openapi.yaml # spec root (références les domaines) +├── domains/ +│ ├── auth.yaml # endpoints /auth/* +│ ├── users.yaml # endpoints /users/* +│ ├── tracks.yaml # endpoints /tracks/* +│ ├── samples.yaml # endpoints /samples/* +│ ├── trades.yaml # endpoints /trades/* +│ ├── learn.yaml # endpoints /learn/* +│ ├── shop.yaml # endpoints /shop/* +│ ├── payments.yaml # endpoints /payments/* +│ ├── cloud.yaml # endpoints /cloud/* +│ ├── community.yaml # endpoints /community/* +│ └── stream.yaml # endpoints Rust stream server +├── components/ +│ ├── schemas.yaml # modèles partagés (User, Track, Error, ...) +│ ├── responses.yaml # réponses communes (401, 403, 429, 500) +│ ├── parameters.yaml # paramètres communs (pagination, filters) +│ └── security.yaml # schémas d'auth +└── examples/ + ├── auth_register.json + ├── sample_pack_create.json + └── ... +``` + +### 2.2 Fichier root `openapi.yaml` + +```yaml +openapi: 3.1.0 +info: + title: Veza API + version: 1.0.0 + description: | + API REST de la plateforme Veza. + Base URL : https://api.veza.talas.co/v1 + contact: + email: dev@talas.co + license: + name: Proprietary + +servers: + - url: https://api.veza.talas.co/v1 + description: Production + - url: http://localhost:18080/api/v1 + description: Dev local + +tags: + - name: Auth + description: Authentification et sessions + - name: Users + description: Gestion profils + - name: Tracks + description: Morceaux audio + # ... (un tag par domaine) + +paths: + $ref: './domains/index.yaml' # aggregation + +components: + schemas: + $ref: './components/schemas.yaml' + responses: + $ref: './components/responses.yaml' + parameters: + $ref: './components/parameters.yaml' + securitySchemes: + $ref: './components/security.yaml' + +security: + - BearerAuth: [] + - CookieAuth: [] +``` + +--- + +## 3. Conventions de nommage + +### 3.1 Paths + +- **kebab-case** dans les URLs : `/user-profiles`, `/sample-packs` +- **Pluriel** pour les collections : `/tracks`, pas `/track` +- **Singulier** pour les singletons : `/me`, `/auth` +- **Verbes standards** : GET, POST, PUT, PATCH, DELETE +- **Pas de verbes dans les paths** sauf actions métier (`POST /orders/:id/cancel`) + +### 3.2 Query parameters + +- **snake_case** : `?user_id=...&created_after=...` +- **Arrays** : `?tags=rock,jazz` (comma-separated) ou `?tags[]=rock&tags[]=jazz` + +### 3.3 Bodies JSON + +- **snake_case** partout : `{ "user_id": "...", "created_at": "..." }` +- **Pas de camelCase** dans le JSON (cohérent avec PostgreSQL) + +### 3.4 Opération IDs (pour codegen) + +Convention : `verbResource` ou `verbResourceBy...` + +```yaml +paths: + /tracks: + get: + operationId: listTracks + post: + operationId: createTrack + /tracks/{id}: + get: + operationId: getTrack + put: + operationId: updateTrack + delete: + operationId: deleteTrack +``` + +--- + +## 4. Pagination + +### 4.1 Convention standard (page+limit) + +Query params : +- `page` (default: 1, min: 1) +- `limit` (default: 20, max: 100) + +Response : +```json +{ + "data": [ ... ], + "pagination": { + "page": 1, + "limit": 20, + "total_items": 342, + "total_pages": 18 + } +} +``` + +### 4.2 Cursor-based (gros volumes) + +Pour les endpoints avec >10k résultats : + +``` +GET /tracks?cursor=eyJpZCI6...&limit=20 + +Response: +{ + "data": [ ... ], + "next_cursor": "eyJpZCI6..." +} +``` + +--- + +## 5. Format d'erreur + +### 5.1 Structure standard + +```json +{ + "error": { + "code": "validation_failed", + "message": "Un champ est invalide", + "details": [ + { + "field": "email", + "rule": "required", + "message": "L'email est requis" + } + ] + }, + "request_id": "req_abc123" +} +``` + +### 5.2 Codes d'erreur métier (snake_case) + +| Code | HTTP | Description | +|------|------|-------------| +| `validation_failed` | 400 | Payload invalide | +| `authentication_required` | 401 | Pas ou token invalide | +| `forbidden` | 403 | Permissions insuffisantes | +| `not_found` | 404 | Ressource absente | +| `conflict` | 409 | Conflit de version / duplicate | +| `rate_limited` | 429 | Trop de requêtes | +| `internal_error` | 500 | Erreur serveur | + +### 5.3 Erreurs spécifiques domaine + +Chaque domaine a ses codes (préfixés) : +- `auth_wrong_credentials` (401) +- `payment_declined` (402) +- `cart_item_out_of_stock` (409) +- `upload_file_too_large` (413) +- `upload_invalid_format` (415) + +--- + +## 6. Authentification + +### 6.1 Schémas sécurité + +```yaml +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + CookieAuth: + type: apiKey + in: cookie + name: access_token + CsrfToken: + type: apiKey + in: header + name: X-CSRF-Token +``` + +### 6.2 Application par endpoint + +```yaml +paths: + /auth/login: + post: + security: [] # pas d'auth requise + /users/me: + get: + security: + - CookieAuth: [] + # ou: + - BearerAuth: [] +``` + +--- + +## 7. Versioning + +### 7.1 Stratégie + +- **Version dans URL** : `/api/v1/...`, `/api/v2/...` +- **Majeure uniquement** : pas de minor/patch dans URL +- **Cohabitation** : v1 et v2 actives pendant 6 mois min lors d'upgrade + +### 7.2 Déprécation + +- Header `Deprecation: true` + `Sunset: Wed, 31 Dec 2026 23:59:59 GMT` +- Champs JSON dépréciés marqués `deprecated: true` dans le spec +- Annonce 3 mois avant sunset + +--- + +## 8. Exemples de spec + +### 8.1 Endpoint simple (GET) + +```yaml +/tracks/{id}: + get: + operationId: getTrack + tags: [Tracks] + summary: Récupère un track par ID + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + '200': + description: Track trouvé + content: + application/json: + schema: + $ref: '#/components/schemas/Track' + '404': + $ref: '#/components/responses/NotFound' +``` + +### 8.2 Endpoint avec body (POST) + +```yaml +/tracks: + post: + operationId: createTrack + tags: [Tracks] + summary: Crée un nouveau track + security: + - CookieAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTrackInput' + examples: + basic: + $ref: '#/components/examples/track_create_basic' + responses: + '201': + description: Track créé + content: + application/json: + schema: + $ref: '#/components/schemas/Track' + '400': + $ref: '#/components/responses/ValidationError' + '401': + $ref: '#/components/responses/Unauthorized' +``` + +### 8.3 Schema avec validation + +```yaml +CreateTrackInput: + type: object + required: [title, duration_sec] + properties: + title: + type: string + minLength: 1 + maxLength: 200 + example: "Morning Coffee" + duration_sec: + type: number + minimum: 1 + maximum: 7200 + tags: + type: array + items: + type: string + minLength: 2 + maxLength: 40 + maxItems: 10 + is_public: + type: boolean + default: true +``` + +--- + +## 9. Outils + +### 9.1 Édition + +- **Swagger Editor** (self-hosted) : `http://editor.veza.internal` +- **VS Code extension** : OpenAPI (Swagger) Editor by 42Crunch + +### 9.2 Documentation publique + +**Redoc** (self-hosted) : `https://api.veza.talas.co/docs` +- Build statique à partir de `openapi.yaml` +- Regeneration sur chaque merge main + +### 9.3 Génération de clients + +| Langage | Outil | Output | +|---------|-------|--------| +| TypeScript | `openapi-typescript` | types seulement | +| TypeScript client | `openapi-fetch` | types + fetch wrapper | +| Go | `oapi-codegen` | types + server/client | +| Rust | `openapi-generator-cli` | types + reqwest client | + +### 9.4 Tests contractuels + +- **Dredd** (CI) : vérifie que le backend respecte le spec +- **Schemathesis** : property-based testing basé sur le spec + +--- + +## 10. Workflow + +### 10.1 Ajout d'un endpoint + +1. **Design first** : écrire le spec OpenAPI d'abord +2. **Review** : validation en PR (au moins 1 reviewer) +3. **Generate** : regénérer types TypeScript et clients Go +4. **Implement** : backend + frontend utilisent les types générés +5. **Test** : Dredd en CI vérifie la conformité +6. **Deploy** : Redoc redéployé automatiquement + +### 10.2 Modification breaking change + +- **Ne pas modifier** un endpoint existant sauf bugfix +- **Ajouter v2** pour changement incompatible +- **Annoncer sunset** v1 avec délai + +### 10.3 Validation CI + +```yaml +# .github/workflows/openapi.yaml +- name: Validate OpenAPI + run: npx @stoplight/spectral-cli lint openapi.yaml +- name: Check contract + run: dredd openapi.yaml http://localhost:18080 +``` + +--- + +## 11. Conventions spécifiques Veza + +### 11.1 Timestamps + +Toujours **ISO 8601 UTC** : `2026-04-05T14:30:00Z` + +```yaml +created_at: + type: string + format: date-time +``` + +### 11.2 IDs + +Toujours **UUID v4** : + +```yaml +id: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" +``` + +### 11.3 Montants + +Toujours en **centimes (int)**, jamais en float : + +```yaml +amount_cents: + type: integer + minimum: 0 + example: 9900 # = 99.00 € +currency: + type: string + pattern: "^[A-Z]{3}$" + example: "EUR" +``` + +### 11.4 Enums + +Enum as `snake_case` strings : + +```yaml +status: + type: string + enum: [draft, published, archived] +``` + +--- + +## 12. Roadmap API_Specs + +### V1.0 (Q2 2026) +- [ ] Créer structure de dossiers +- [ ] Documenter auth, users, tracks (ceux qui existent déjà) +- [ ] Setup Redoc auto-deploy +- [ ] Setup spectral linting CI + +### V1.5 (Q3 2026) +- [ ] Domaines samples, trades, events +- [ ] Domaines learn, cloud +- [ ] Génération clients TS + Go en CI +- [ ] Dredd contract tests + +### V2.0 (Q4 2026) +- [ ] Domaines shop, payments +- [ ] Async API (WebSocket) documenté avec AsyncAPI +- [ ] Webhooks documentés (spec Mollie webhooks) + +--- + +## Voir aussi + +- [[ROUTES_API]] — liste exhaustive endpoints actuels (source de conversion vers OpenAPI) +- [[ARCHITECTURE_VEZA]] — architecture globale +- [[Rust_Engines/OVERVIEW_RUST_MODULES]] — APIs spécifiques Rust diff --git a/03_APPS_&_SERVICES/APIs_&_Rust_Modules/Rust_Engines/OVERVIEW_RUST_MODULES.md b/03_APPS_&_SERVICES/APIs_&_Rust_Modules/Rust_Engines/OVERVIEW_RUST_MODULES.md new file mode 100644 index 0000000..5632a61 --- /dev/null +++ b/03_APPS_&_SERVICES/APIs_&_Rust_Modules/Rust_Engines/OVERVIEW_RUST_MODULES.md @@ -0,0 +1,386 @@ +# Overview — Modules Rust Veza + +> Vue d'ensemble des moteurs Rust de Veza, patterns communs et roadmap. +> Détail du stream-server existant : voir [[SERVEUR_STREAMING_RUST]] +> Dernière mise à jour : avril 2026. + +--- + +## 1. Pourquoi Rust chez Veza + +Veza utilise Rust **uniquement** pour les composants où ça fait la différence : + +- **Performance CPU** : transcoding audio, DSP, traitement temps réel +- **Concurrence massive** : WebSocket streaming à N clients simultanés +- **Sûreté mémoire** : code proche du hardware sans GC pauses +- **Stabilité long-terme** : services qui tournent 24/7 sans leaks + +Le reste (CRUD, business logic, auth) reste en **Go** (plus rapide à itérer, équipe plus large). + +--- + +## 2. Modules existants + +### 2.1 `veza-stream-server` (actif) + +**Source** : `/home/senke/git/talas/veza/veza-stream-server/` + +Responsabilités : +- Streaming HLS adaptatif multi-bitrate +- WebSocket temps réel avec sync lecture +- Transcoding audio via FFmpeg +- Métriques Prometheus + +Stack : Axum, Tokio, FFmpeg bindings. + +Voir [[SERVEUR_STREAMING_RUST]] pour détails. + +--- + +## 3. Modules futurs (roadmap) + +### 3.1 `veza-audio-analyzer` (V1.5, Q3 2026) + +**Mission** : analyse audio asynchrone pour uploads. + +**Tâches** : +- Détection tempo (BPM) +- Détection tonalité (key signature) +- Analyse spectrale (RMS, LUFS, crête) +- Détection fingerprint (anti-doublon) +- Génération waveform peaks (pour visualisation) + +**Stack envisagée** : +- `aubio-rs` ou binding `essentia` +- `symphonia` (décodage audio pure-Rust) +- Workers RabbitMQ + +**Intégration** : +``` +[Backend Go] upload track + → publish RabbitMQ "track.analyze" + → [audio-analyzer Rust] consomme + → analyse + → écrit résultats en DB + → publish "track.analyzed" + → [Backend Go] met à jour metadata +``` + +### 3.2 `veza-dsp-engine` (V2.0, 2027) + +**Mission** : moteur DSP pour effets audio temps réel. + +**Use cases** : +- Preview de samples avec effets (EQ, reverb, compression) sans DAW +- Mastering automatique léger (LUFS target) +- Génération de previews "stylisées" (cassette, radio, vinyl) + +**Stack envisagée** : +- `fundsp` ou `cpal` + `rubato` +- WASM export pour preview client-side (V2.5) + +### 3.3 `veza-encoder-farm` (V2.0, 2027) + +**Mission** : cluster de transcoding distribué. + +**Problème actuel** : le stream-server fait le transcoding inline, ne scale pas au-delà de ~50 uploads/h. + +**Solution** : +- N workers Rust sur le cluster +- Queue RabbitMQ pour distribution +- Output sur MinIO +- Métriques par worker + +### 3.4 `veza-search-ranker` (V2+, optionnel) + +**Mission** : ré-ranking des résultats Elasticsearch avec signaux Veza (popularité, affinité). + +**Pertinence** : seulement si ES brut ne suffit pas. + +--- + +## 4. Patterns communs + +### 4.1 Structure projet Rust + +``` +veza-/ +├── Cargo.toml +├── src/ +│ ├── main.rs # entry point (si service) +│ ├── lib.rs # si library +│ ├── config/ # config depuis env +│ ├── handlers/ # HTTP/RMQ handlers +│ ├── services/ # business logic +│ ├── models/ # types métier +│ ├── errors/ # AppError enum +│ └── telemetry/ # metrics, logs, tracing +├── tests/ # tests d'intégration +└── benches/ # benchmarks (criterion) +``` + +### 4.2 Gestion d'erreurs + +**Pattern** : custom `AppError` enum + `thiserror` + +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + + #[error("Upstream service error: {0}")] + Upstream(String), + + #[error("Validation: {0}")] + Validation(String), + + #[error("Not found: {0}")] + NotFound(String), +} + +pub type AppResult = Result; +``` + +Conversion en HTTP : +```rust +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, code) = match &self { + AppError::Database(_) => (500, "internal_error"), + AppError::Upstream(_) => (502, "upstream_error"), + AppError::Validation(_) => (400, "validation_failed"), + AppError::NotFound(_) => (404, "not_found"), + }; + // ... construit JSON error standard + } +} +``` + +### 4.3 Runtime & async + +- **Tokio** multi-thread runtime (par défaut) +- **Pas de `block_on`** dans les handlers +- **`tokio::spawn`** pour les tâches détachées + +### 4.4 Configuration + +Pattern : struct `Config` hydratée depuis env via `serde` + `envy` ou `config-rs`. + +```rust +#[derive(Deserialize)] +pub struct Config { + pub database_url: String, + pub redis_url: String, + pub rabbitmq_url: String, + pub port: u16, + pub log_level: String, +} + +impl Config { + pub fn from_env() -> Result { + envy::prefixed("VEZA_").from_env() + } +} +``` + +### 4.5 Logging + +Stack : **`tracing` + `tracing-subscriber`** + +```rust +use tracing::{info, warn, error, instrument}; + +#[instrument(skip(db))] +pub async fn process_track(db: &Pool, track_id: Uuid) -> AppResult<()> { + info!(%track_id, "processing track"); + // ... +} +``` + +Format : **JSON structuré en prod**, texte coloré en dev. + +### 4.6 Metrics + +Stack : **`prometheus` + `axum-prometheus`** + +Chaque service expose `/metrics`. Dashboard Grafana centralisé. + +### 4.7 Health checks + +- `/healthz` → liveness (service alive, réponse 200) +- `/readyz` → readiness (dépendances OK : DB, RMQ, S3) + +--- + +## 5. Interfaces entre services + +### 5.1 Service-to-service + +| Méthode | Usage | +|---------|-------| +| **REST interne** | Callbacks synchrones (Rust ↔ Go) | +| **RabbitMQ** | Events asynchrones (Rust consume, Go produce) | +| **PostgreSQL** | État partagé (Rust en lecture directe parfois) | +| **MinIO S3** | Blob storage commun | + +### 5.2 Service → client + +- **HTTP REST** : endpoints exposés via Axum +- **WebSocket** : streaming audio temps réel +- **Server-Sent Events** : si push unidirectionnel suffit + +### 5.3 Pas de gRPC en V1 + +**Décision** : pas de gRPC tant que REST + RMQ suffisent. Complexité opérationnelle non justifiée. + +--- + +## 6. Dépendances communes (Cargo.toml) + +```toml +[dependencies] +# Runtime +tokio = { version = "1.42", features = ["full"] } + +# HTTP +axum = "0.7" +tower = "0.5" +tower-http = { version = "0.6", features = ["cors", "trace"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Errors +thiserror = "2" +anyhow = "1" + +# Database +sqlx = { version = "0.8", features = ["postgres", "uuid", "time", "runtime-tokio"] } + +# Storage S3 +aws-sdk-s3 = "1" + +# Messaging +lapin = "2" # RabbitMQ + +# Observability +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +prometheus = "0.13" + +# Config +envy = "0.4" + +# UUID / time +uuid = { version = "1", features = ["v4", "serde"] } +time = { version = "0.3", features = ["serde"] } + +[dev-dependencies] +criterion = "0.5" # benchmarks +mockall = "0.13" # mocking +``` + +--- + +## 7. Tests + +### 7.1 Niveaux + +| Niveau | Outil | Cible | +|--------|-------|-------| +| Unit | `cargo test` | Fonctions isolées | +| Integration | `cargo test --test ...` | Handlers avec DB test | +| Benchmarks | `criterion` | Hot paths (DSP, transcoding) | +| Load | `k6` ou `vegeta` | Endpoints HTTP/WS | + +### 7.2 Conventions + +- Tests unitaires dans le même fichier que le code (`#[cfg(test)] mod tests`) +- Tests d'intégration dans `tests/` avec base de données test (Docker) +- Benchmarks dans `benches/` + +--- + +## 8. Build & deploy + +### 8.1 Build + +```bash +# Dev (rapide) +cargo run + +# Release optimisé +cargo build --release + +# Docker multi-stage +docker build -t veza-stream-server . +``` + +### 8.2 Image Docker type + +```dockerfile +FROM rust:1.83 AS builder +WORKDIR /app +COPY . . +RUN cargo build --release + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y ffmpeg ca-certificates +COPY --from=builder /app/target/release/veza-stream-server /usr/local/bin/ +CMD ["veza-stream-server"] +``` + +### 8.3 Déploiement + +Ansible role : `04_INFRA_DEPLOIEMENT/Ansible/roles/veza-rust-service/` + +--- + +## 9. Benchmarks & objectifs + +### 9.1 Stream server + +| Métrique | Objectif | Mesure actuelle | +|----------|----------|-----------------| +| Connexions WS simultanées | 1000+ | À mesurer | +| Latence chunks audio | < 100ms p99 | À mesurer | +| Mémoire / 1000 WS | < 2 GB | À mesurer | + +### 9.2 Audio analyzer (futur) + +| Métrique | Objectif | +|----------|----------| +| Durée analyse / min audio | < 10 sec | +| Throughput workers | 100+ tracks/h / worker | + +--- + +## 10. Roadmap + +### V1.0 (actuel) +- [x] `veza-stream-server` production + +### V1.5 (Q3 2026) +- [ ] `veza-audio-analyzer` (BPM, key, waveform) +- [ ] Benchmarks automatisés en CI + +### V2.0 (2027) +- [ ] `veza-dsp-engine` (effets temps réel) +- [ ] `veza-encoder-farm` (scaling transcoding) + +### V2.5+ +- [ ] Export WASM pour DSP côté client +- [ ] Compatibilité WebCodecs (browser-native transcoding) + +--- + +## Voir aussi + +- [[SERVEUR_STREAMING_RUST]] — détail stream-server existant +- [[ARCHITECTURE_VEZA]] — architecture globale +- [[API_Specs/CONVENTIONS_OPENAPI]] — conventions API +- [[04_INFRA_DEPLOIEMENT]] — déploiement des services Rust diff --git a/03_APPS_&_SERVICES/Auth_&_Core/SPEC_AUTH_CORE.md b/03_APPS_&_SERVICES/Auth_&_Core/SPEC_AUTH_CORE.md new file mode 100644 index 0000000..66a1a56 --- /dev/null +++ b/03_APPS_&_SERVICES/Auth_&_Core/SPEC_AUTH_CORE.md @@ -0,0 +1,464 @@ +# Spec technique — Authentification & Core System + +> Architecture auth de Veza : décisions, modèle de sécurité, RBAC, conformité. +> Ce document complète [[ROUTES_API]] qui liste les endpoints — ici on documente le **pourquoi** et le **comment**. +> Stack : Go (Gin + GORM) · PostgreSQL 16 · Redis · JWT RS256 +> Dernière mise à jour : avril 2026. + +--- + +## 1. Vue d'ensemble + +Veza implémente une auth **multi-facteurs, multi-device, avec rotation de clés**. Défense en profondeur : chaque couche de sécurité est indépendante (JWT, CSRF, rate limit, verrouillage compte, audit). + +### Principes fondateurs + +1. **Auth centralisée** — un seul module pour toutes les apps (web, mobile, desktop) +2. **Tokens courts** — JWT access 15 min, refresh 30 jours +3. **Cookies HTTP-only** (prod) + header Authorization (mobile/API) +4. **Rotation de clés** RS256 automatique +5. **Audit complet** — toute action sensible est loggée + +--- + +## 2. Modèle de sécurité (couches) + +``` +┌─────────────────────────────────────────────────────┐ +│ 1. Rate limiting (global, par IP, par endpoint) │ +├─────────────────────────────────────────────────────┤ +│ 2. CSRF token (Redis-backed, obligatoire POST/PUT) │ +├─────────────────────────────────────────────────────┤ +│ 3. JWT validation (signature RS256 + expiration) │ +├─────────────────────────────────────────────────────┤ +│ 4. Permissions RBAC (role + resource check) │ +├─────────────────────────────────────────────────────┤ +│ 5. Audit logging (écriture DB async via worker) │ +└─────────────────────────────────────────────────────┘ +``` + +Chaque couche peut refuser la requête **indépendamment**. + +--- + +## 3. Authentification + +### 3.1 JWT — choix de design + +| Décision | Valeur | Justification | +|----------|--------|---------------| +| Algorithme | **RS256** | Asymétrique, permet de distribuer la clé publique au stream-server sans partager le secret | +| Access token TTL | **15 min** | Compromis sécurité/UX | +| Refresh token TTL | **30 jours** | Évite re-login trop fréquent | +| Rotation des clés | **Automatique, toutes les 90 jours** | Via cron, ancienne clé conservée 15 jours pour validation | +| Stockage | **Cookie HTTP-only + Secure + SameSite=Lax** (web) / Header Authorization (API clients) | Protection XSS | + +### 3.2 Cycle de vie des tokens + +``` +[Login] + POST /auth/login (email, password) + → Backend vérifie password (bcrypt) + → Génère access_token (15min) + refresh_token (30d) + → refresh_token stocké en DB (hashé) dans table refresh_tokens + → Cookies HTTP-only retournés + +[Appel authentifié] + GET /auth/me + Cookie: access_token=... + → Middleware valide JWT (signature + expiration) + → Injecte user_id dans contexte Gin + +[Refresh] + POST /auth/refresh + Cookie: refresh_token=... + → Backend vérifie hash en DB + → Génère nouveau access_token + → Tourne le refresh_token (nouveau généré, ancien révoqué) + → Response: nouveaux cookies + +[Logout] + POST /auth/logout + → Révoque le refresh_token en DB + → Clear cookies +``` + +### 3.3 Table `refresh_tokens` + +```sql +CREATE TABLE refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash VARCHAR(64) NOT NULL, -- SHA-256 du token + device_id VARCHAR(100), -- fingerprint léger + user_agent VARCHAR(500), + ip_address INET, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ, + last_used_at TIMESTAMPTZ +); + +CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id) WHERE revoked_at IS NULL; +CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash) WHERE revoked_at IS NULL; +``` + +### 3.4 Multi-device + +Un user peut être connecté sur N devices simultanément. Chaque device = un refresh_token distinct. + +- **Lister mes devices** : `GET /auth/sessions` +- **Déconnecter un device** : `DELETE /auth/sessions/:id` +- **Déconnecter tous les devices** : `DELETE /auth/sessions/all` + +--- + +## 4. Password policy + +### 4.1 Règles + +| Règle | Valeur | +|-------|--------| +| Longueur min | 12 caractères | +| Longueur max | 128 caractères | +| Complexité | 3 des 4 : minuscule, majuscule, chiffre, symbole | +| Hash | bcrypt cost 12 | +| Vérification pwned | Hash SHA-1 partiel check via **HaveIBeenPwned API** (self-hosted mirror si possible) | +| Expiration | **Pas de rotation forcée** (NIST 800-63B) | + +### 4.2 Verrouillage de compte + +- 5 tentatives échouées → verrouillage 15 minutes +- 10 tentatives échouées (tous IPs) → verrouillage 24h + email admin +- Reset : via email de déverrouillage + +### 4.3 Reset password + +``` +POST /auth/password/reset-request { email } + → Génère token UUID (expire 1h) + → Stocke hash en DB + → Envoie email avec lien + +GET /reset-password?token=xxx + → Frontend vérifie via POST /auth/password/reset/validate + → Affiche formulaire + +POST /auth/password/reset { token, new_password } + → Vérifie token (hash) + expiration + → Change password + → Révoque TOUS les refresh_tokens du user (force logout) + → Invalide token +``` + +--- + +## 5. 2FA / MFA + +### 5.1 Méthodes supportées + +| Méthode | Priorité | Use case | +|---------|----------|----------| +| **TOTP** (Google Authenticator, Aegis, 2FAS) | Primaire | Standard ouvert | +| **WebAuthn / Passkeys** | Primaire | Meilleure UX, biométrie, hardware keys | +| **Backup codes** | Secondaire | 10 codes à usage unique | +| **SMS** | **NON** | Vulnérable SIM swap, pas GDPR friendly | + +### 5.2 Flow TOTP + +``` +[Setup] +POST /auth/2fa/setup + → Génère secret TOTP (32 chars base32) + → Stocke temporairement en Redis (5 min) + → Response: { qr_code_url, secret } + +POST /auth/2fa/verify { code } + → Vérifie le code TOTP + → Persiste secret en DB (chiffré) + → Génère 10 backup codes + → Response: { backup_codes: [...] } + +[Login avec 2FA] +POST /auth/login { email, password } + → Si user.mfa_enabled : response 200 { requires_2fa: true, temp_token: ... } + +POST /auth/login/2fa { temp_token, code } + → Vérifie TOTP ou backup code + → Génère access + refresh tokens +``` + +### 5.3 Flow WebAuthn (Passkeys) + +Voir [[ROUTES_API]] §1 pour les endpoints. Utilise **bibliothèque go-webauthn** côté backend. + +### 5.4 Chiffrement des secrets 2FA en DB + +- Secrets TOTP chiffrés en AES-256-GCM +- Clé de chiffrement : **KMS léger** via variable d'env `TOTP_ENCRYPTION_KEY` +- Rotation de clé supportée (re-chiffrement batch) + +--- + +## 6. OAuth 2.0 (providers externes) + +### 6.1 Providers supportés + +| Provider | Usage | Priorité V1 | +|----------|-------|-------------| +| **Discord** | Communautés musicales | Haute | +| **GitHub** | Dev-friendly | Moyenne | +| **Google** | Vaste adoption | Basse (GAFAM, contraire aux valeurs) | +| **Apple** | iOS users | Basse | +| **Mastodon** | Fediverse-friendly | Moyenne | + +**Décision V1** : Discord + GitHub. Pas de Google/Apple en V1 (incohérent avec valeurs self-hosted). + +### 6.2 Flow OAuth + +Standard OAuth 2.0 Authorization Code + PKCE : + +``` +[Initiation] +GET /auth/oauth/discord + → Backend génère state + code_verifier + → Stocke en Redis (TTL 10min, keyed par session) + → Redirect vers Discord avec code_challenge + +[Callback] +GET /auth/oauth/discord/callback?code=xxx&state=yyy + → Vérifie state contre Redis + → POST Discord /token avec code_verifier + → Récupère access_token Discord + → GET Discord /users/@me + → SI user Veza existe avec cet oauth_id : login + SINON : création user (email vérifié auto via Discord) + → Génère JWT Veza +``` + +### 6.3 Chiffrement des tokens OAuth + +Les refresh_tokens OAuth sont stockés **chiffrés** en DB (AES-256-GCM) pour pouvoir rafraîchir l'access token du provider si besoin. + +### 6.4 Unlink + +Un user peut dissocier un provider OAuth : +- Si c'est le SEUL moyen de login → exige de définir un password d'abord +- Sinon : suppression simple du lien + +--- + +## 7. RBAC — Roles & Permissions + +### 7.1 Rôles V1 + +| Role | Permissions | +|------|-------------| +| `user` | Standard : lecture publique, écriture sur ses contenus | +| `content_creator` | + publier courses, être featured | +| `moderator` | + modérer comments, flagger contenus | +| `admin` | + gestion users, publication courses, modification catalogue shop | +| `superadmin` | + gestion infra, rotation clés, accès audit logs | + +### 7.2 Table `user_roles` + +```sql +CREATE TABLE user_roles ( + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + role VARCHAR(30) NOT NULL, + granted_by UUID REFERENCES users(id), + granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ, + PRIMARY KEY (user_id, role) +); + +CREATE INDEX idx_user_roles_role ON user_roles(role); +``` + +### 7.3 Permissions granulaires (V2+) + +En V1 : check par role (`user.HasRole("admin")`). +En V2 : table `permissions` et matrice `role_permissions` pour fine-grained control. + +### 7.4 Middleware Gin + +```go +router.POST("/admin/users/:id/ban", + middleware.RequireAuth(), + middleware.RequireRole("moderator", "admin"), + handlers.BanUser, +) +``` + +--- + +## 8. CSRF Protection + +### 8.1 Stratégie + +**Double-submit cookie + Redis backing** : + +1. GET `/csrf-token` → génère UUID, stocke en Redis (TTL 24h), retourne dans cookie `_csrf` ET header `X-CSRF-Token` +2. POST/PUT/DELETE : frontend envoie header `X-CSRF-Token` (depuis cookie accessible JS) +3. Backend vérifie : cookie == header == Redis value + +### 8.2 Exclusions + +- Endpoints d'auth (login, register, refresh) → exemptés (pas de session encore) +- API clients externes (mobile) → utilisent Bearer token, CSRF non-applicable + +### 8.3 Expiration + +Token renouvelé à chaque login. TTL 24h actif. + +--- + +## 9. Rate limiting + +### 9.1 Niveaux + +| Niveau | Limite | Backing | +|--------|--------|---------| +| Global | 1000 req/s | Redis | +| Par IP | 100 req/s | Redis + token bucket | +| Par endpoint sensible | Variable | Redis | + +### 9.2 Endpoints spécifiquement limités + +| Endpoint | Limite | +|----------|--------| +| `POST /auth/login` | 5 tentatives / 15 min / IP | +| `POST /auth/register` | 3 / heure / IP | +| `POST /auth/password/reset-request` | 3 / heure / email | +| `POST /auth/verify-email` | 10 / heure / user | +| Upload multipart | 5 / jour / user | + +### 9.3 Réponse 429 + +```json +{ + "error": "rate_limited", + "retry_after_seconds": 120, + "message": "Trop de tentatives. Réessaye dans 2 minutes." +} +``` + +Header `Retry-After: 120` systématique. + +--- + +## 10. Audit logging + +### 10.1 Actions loggées + +- Login (succès/échec) +- Logout +- Changement password +- Activation/désactivation 2FA +- Création/révocation session +- Changement de role +- Actions admin (ban, suppression) + +### 10.2 Table `audit_logs` + +```sql +CREATE TABLE audit_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + actor_id UUID REFERENCES users(id), + action VARCHAR(50) NOT NULL, + target_type VARCHAR(50), + target_id UUID, + ip_address INET, + user_agent VARCHAR(500), + metadata JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_audit_logs_actor ON audit_logs(actor_id, created_at DESC); +CREATE INDEX idx_audit_logs_action ON audit_logs(action, created_at DESC); +CREATE INDEX idx_audit_logs_target ON audit_logs(target_type, target_id); +``` + +### 10.3 Écriture asynchrone + +Via RabbitMQ : l'app push un événement, un worker l'écrit en DB. Évite de bloquer les requêtes API sur l'écriture d'audit. + +--- + +## 11. Conformité RGPD + +### 11.1 Droits implémentés + +| Droit | Endpoint | Délai | +|-------|----------|-------| +| Accès | `GET /auth/export-data` | Génération async, email avec lien (24h) | +| Rectification | `PUT /users/me` | Instant | +| Effacement | `DELETE /auth/account` | Soft delete immédiat, purge après 30 jours | +| Portabilité | `GET /auth/export-data` format JSON | 24h | +| Opposition | Opt-out analytics dans settings | Instant | + +### 11.2 Pseudonymisation + +Après suppression de compte : +- Email → `deleted_@deleted.veza` +- Username → `deleted_` +- Contenus : soft delete (conservables pour droits des tiers si morceau remixé etc.) + +### 11.3 Cookies + +| Cookie | Finalité | Durée | Opt-in ? | +|--------|----------|-------|----------| +| `access_token` | Auth | 15 min | Obligatoire (fonctionnel) | +| `refresh_token` | Auth | 30 jours | Obligatoire (fonctionnel) | +| `_csrf` | Sécurité | 24h | Obligatoire (fonctionnel) | +| `session_analytics` | Stats internes | 30j | **Opt-in** | + +--- + +## 12. Considérations spéciales + +### 12.1 Partage de clé publique JWT + +Le stream-server Rust valide les JWT. Il récupère la clé publique RS256 via : +- Endpoint interne `GET /internal/jwks` (liste clés publiques valides) +- Cache local (TTL 1h) +- Refresh automatique si clé inconnue rencontrée + +### 12.2 Propagation user sur services internes + +Le user_id est propagé via header custom `X-User-ID` entre services internes, signé par une clé partagée (HMAC) pour éviter spoofing. + +--- + +## 13. Roadmap + +### V1.0 (actuel, avril 2026) +- [x] JWT RS256 + refresh +- [x] Password bcrypt +- [x] 2FA TOTP +- [x] OAuth Discord + GitHub +- [x] RBAC 5 rôles +- [x] CSRF Redis +- [x] Rate limiting multi-niveaux +- [x] Audit logging + +### V1.5 (été 2026) +- [ ] WebAuthn/Passkeys +- [ ] Vérification password pwned (HIBP) +- [ ] Export données RGPD complet +- [ ] Rotation clés RS256 automatique + +### V2.0 (fin 2026) +- [ ] Permissions fine-grained (au-delà des rôles) +- [ ] SSO SAML pour entreprises (si demandé) +- [ ] Audit dashboard admin + +--- + +## Voir aussi + +- [[ROUTES_API]] — liste complète des endpoints `/auth/*` +- [[ARCHITECTURE_VEZA]] — architecture globale +- [[SCHEMA_BASE_DE_DONNEES]] — tables users, sessions +- [[08_CONFORMITE_JURIDIQUE/RGPD]] — politique RGPD +- [[04_INFRA_DEPLOIEMENT/Sécurité]] — sécurité infra diff --git a/03_APPS_&_SERVICES/Community/Formation/SPEC_TECHNIQUE_FORMATION.md b/03_APPS_&_SERVICES/Community/Formation/SPEC_TECHNIQUE_FORMATION.md new file mode 100644 index 0000000..6180389 --- /dev/null +++ b/03_APPS_&_SERVICES/Community/Formation/SPEC_TECHNIQUE_FORMATION.md @@ -0,0 +1,469 @@ +# Spec technique — Module Formation + +> Implémentation backend des parcours de formation Veza : tutoriels, masterclasses, bootcamps. +> Stack : Go (Gin + GORM) · PostgreSQL 16 · MinIO S3 · Rust stream-server · Redis +> Dernière mise à jour : avril 2026. + +Voir aussi le curriculum côté contenu : [[06_COMMUNAUTE_ECOSYSTEME/Formation_Creators/CURRICULUM_FORMATION_CREATORS]] + +--- + +## 1. Vue d'ensemble + +Le module **Formation** héberge des **parcours d'apprentissage** composés de leçons (vidéo + texte + exercices). Positionnement : un Coursera artisanal, sans gamification agressive, sans certification bidon. + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Frontend │─────▶│ Backend API │─────▶│ PostgreSQL │ +│ React │ │ Go / Gin │ │ (content + │ +│ + Player │ └──────┬───────┘ │ progress) │ +└──────┬───────┘ │ └──────────────┘ + │ │ + │ HLS stream ├────────────▶ MinIO S3 + │ │ (vidéos, PDFs) + ▼ │ +┌──────────────┐ └────────────▶ Stream Server +│ Stream Server│◀─────────────────────────── (Rust, HLS) +│ Rust/Axum │ +└──────────────┘ +``` + +--- + +## 2. Modèle de données + +### 2.1 Table `courses` (parcours complet) + +```sql +CREATE TABLE courses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug VARCHAR(100) NOT NULL UNIQUE, + title VARCHAR(150) NOT NULL, + subtitle VARCHAR(300), + description TEXT NOT NULL, + author_id UUID NOT NULL REFERENCES users(id), + + cover_image_s3_key VARCHAR(500), + difficulty VARCHAR(20) NOT NULL CHECK (difficulty IN ('debutant','intermediaire','avance')), + language VARCHAR(5) NOT NULL DEFAULT 'fr', + estimated_hours NUMERIC(4,1), + + category VARCHAR(50) NOT NULL, + -- 'production', 'mixing', 'mastering', 'sound_design', + -- 'recording', 'business', 'hardware', 'theory' + tags VARCHAR(40)[] DEFAULT '{}', + + status VARCHAR(20) NOT NULL DEFAULT 'draft', + -- 'draft', 'published', 'archived' + is_free BOOLEAN NOT NULL DEFAULT true, + + lessons_count INT NOT NULL DEFAULT 0, + enrolled_count INT NOT NULL DEFAULT 0, + completed_count INT NOT NULL DEFAULT 0, + + published_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_courses_published ON courses(published_at DESC) WHERE status = 'published' AND deleted_at IS NULL; +CREATE INDEX idx_courses_category ON courses(category) WHERE status = 'published' AND deleted_at IS NULL; +CREATE INDEX idx_courses_author ON courses(author_id); +``` + +### 2.2 Table `course_modules` (chapitres) + +```sql +CREATE TABLE course_modules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + order_index INT NOT NULL, + title VARCHAR(150) NOT NULL, + description TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (course_id, order_index) +); + +CREATE INDEX idx_course_modules_course ON course_modules(course_id, order_index); +``` + +### 2.3 Table `course_lessons` (leçons individuelles) + +```sql +CREATE TABLE course_lessons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + module_id UUID NOT NULL REFERENCES course_modules(id) ON DELETE CASCADE, + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + order_index INT NOT NULL, + + title VARCHAR(150) NOT NULL, + content_markdown TEXT, -- contenu textuel de la leçon + + lesson_type VARCHAR(20) NOT NULL, + -- 'video', 'article', 'exercise', 'quiz', 'download' + + -- si video + video_s3_key VARCHAR(500), + video_duration_sec NUMERIC(6,1), + hls_manifest_url VARCHAR(500), -- URL vers le manifest HLS généré + + -- si download (fichiers attachés) + attachments_s3_keys VARCHAR(500)[] DEFAULT '{}', + + -- si exercise / quiz + exercise_config JSONB, -- structure flexible selon type + + is_free_preview BOOLEAN NOT NULL DEFAULT false, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (module_id, order_index) +); + +CREATE INDEX idx_course_lessons_course ON course_lessons(course_id, order_index); +CREATE INDEX idx_course_lessons_module ON course_lessons(module_id, order_index); +``` + +### 2.4 Table `course_enrollments` + +```sql +CREATE TABLE course_enrollments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_accessed_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + progress_percent NUMERIC(5,2) NOT NULL DEFAULT 0, + UNIQUE (user_id, course_id) +); + +CREATE INDEX idx_course_enrollments_user ON course_enrollments(user_id, last_accessed_at DESC); +CREATE INDEX idx_course_enrollments_course ON course_enrollments(course_id); +``` + +### 2.5 Table `lesson_progress` + +```sql +CREATE TABLE lesson_progress ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + lesson_id UUID NOT NULL REFERENCES course_lessons(id) ON DELETE CASCADE, + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + + status VARCHAR(20) NOT NULL DEFAULT 'not_started', + -- 'not_started', 'in_progress', 'completed' + + -- si vidéo : timestamp de reprise + video_position_sec NUMERIC(8,1) DEFAULT 0, + + -- si exercise/quiz : résultat + exercise_result JSONB, + + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE (user_id, lesson_id) +); + +CREATE INDEX idx_lesson_progress_user_course ON lesson_progress(user_id, course_id); +``` + +### 2.6 Table `course_comments` (discussions par leçon) + +```sql +CREATE TABLE course_comments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + lesson_id UUID NOT NULL REFERENCES course_lessons(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + parent_id UUID REFERENCES course_comments(id) ON DELETE CASCADE, + body TEXT NOT NULL, + video_timestamp_sec NUMERIC(8,1), -- si commentaire ancré à un moment de la vidéo + edited BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_course_comments_lesson ON course_comments(lesson_id, created_at) WHERE deleted_at IS NULL; +``` + +### 2.7 Table `course_reviews` (évaluations) + +```sql +CREATE TABLE course_reviews ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5), + body TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, course_id) +); + +CREATE INDEX idx_course_reviews_course ON course_reviews(course_id); +``` + +--- + +## 3. API Endpoints + +Base : `/api/v1/learn` + +### 3.1 Parcours — consultation (public) + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/courses` | 🔓 | Liste courses publiés (filtres) | +| GET | `/courses/:slug` | 🔓 | Détail d'un course (syllabus) | +| GET | `/courses/:slug/modules` | 🔓 | Arborescence modules + leçons | +| GET | `/lessons/:id/preview` | 🔓 | Preview (si `is_free_preview=true`) | + +### 3.2 Inscription et progression + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/courses/:id/enroll` | 🔒 | S'inscrire à un course | +| DELETE | `/courses/:id/enroll` | 🔒 | Se désinscrire | +| GET | `/me/courses` | 🔒 | Mes inscriptions | +| GET | `/me/courses/:course_id/progress` | 🔒 | Progression détaillée | +| GET | `/lessons/:id` | 🔒 | Contenu leçon (inscrit au course) | +| POST | `/lessons/:id/start` | 🔒 | Marquer commencée | +| POST | `/lessons/:id/complete` | 🔒 | Marquer terminée | +| PUT | `/lessons/:id/progress` | 🔒 | Sauvegarder position (video_position_sec) | +| POST | `/lessons/:id/exercise` | 🔒 | Soumettre résultat d'exercice | + +### 3.3 Interactions + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/lessons/:id/comments` | 🔓 | Liste commentaires | +| POST | `/lessons/:id/comments` | 🔒 | Poster commentaire | +| PUT | `/comments/:id` | 🔒 | Modifier (own only) | +| DELETE | `/comments/:id` | 🔒 | Soft delete (own only) | +| GET | `/courses/:id/reviews` | 🔓 | Reviews | +| POST | `/courses/:id/review` | 🔒 | Poster review (une seule par user) | + +### 3.4 Author / Admin + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/courses` | 🔒🎨 | Créer course (status=draft) | +| PUT | `/courses/:id` | 🔒🎨 | Éditer course (own only) | +| POST | `/courses/:id/modules` | 🔒🎨 | Ajouter module | +| POST | `/modules/:id/lessons` | 🔒🎨 | Ajouter leçon | +| POST | `/lessons/:id/video-upload` | 🔒🎨 | Upload vidéo (génère URL signée) | +| POST | `/courses/:id/publish` | 🔒🎨 | Publier course | + +--- + +## 4. Flux techniques + +### 4.1 Création d'un course (auteur) + +``` +[1] POST /courses + Body: { title, subtitle, description, difficulty, language, category, tags } + → Backend: INSERT courses (status='draft', author=current_user) + → response: { course } + +[2] POST /courses/:id/modules (plusieurs fois) + Body: { title, description, order_index } + → Backend: INSERT course_modules + +[3] POST /modules/:id/lessons (plusieurs fois) + Body: { title, lesson_type, content_markdown, order_index, is_free_preview } + → Backend: INSERT course_lessons + → response: { lesson, upload_url } (si video) + +[4] PUT (binary vidéo) + → Upload MinIO direct + +[5] Worker "process_video" (RabbitMQ): + - Transcode FFmpeg multi-bitrate + - Génère manifest HLS (via stream-server Rust) + - UPDATE course_lessons SET video_duration_sec, hls_manifest_url + +[6] POST /courses/:id/publish + → Backend: + - Validation (min 1 module, min 1 lesson, cover image, description>100car) + - UPDATE courses SET status='published', published_at=NOW() + - Notif aux followers de l'auteur +``` + +### 4.2 Progression (vue étudiant) + +``` +[1] POST /courses/:id/enroll + → INSERT course_enrollments + → UPDATE courses SET enrolled_count += 1 + +[2] GET /lessons/:id + → Check enrollment existe + → Retourne contenu + HLS URL signé (expire 2h) + +[3] Pendant la lecture vidéo (toutes les 10 sec): + PUT /lessons/:id/progress + Body: { video_position_sec } + → UPSERT lesson_progress + → Update course_enrollments.last_accessed_at + +[4] À la fin de la vidéo (ou clic "Terminé"): + POST /lessons/:id/complete + → UPDATE lesson_progress SET status='completed', completed_at=NOW() + → Recalcul course_enrollments.progress_percent + → Si 100% : SET completed_at, UPDATE courses.completed_count += 1 +``` + +### 4.3 Streaming vidéo + +- Les vidéos sont servies en **HLS adaptatif** via le stream-server Rust (réutilise l'infra existante pour l'audio) +- URL signée générée au moment de `GET /lessons/:id` (expire 2h) +- Protection : URL liée à l'user_id (JWT check au niveau stream-server) + +--- + +## 5. Exercices et quiz + +### 5.1 Structure `exercise_config` (JSONB) + +Selon le `lesson_type`, le champ `exercise_config` varie : + +**Type `quiz`** : +```json +{ + "questions": [ + { + "id": "q1", + "text": "Quelle est la fréquence de Nyquist pour un sample rate de 44.1kHz ?", + "type": "single_choice", + "options": ["22.05 kHz", "44.1 kHz", "88.2 kHz"], + "correct": 0, + "explanation": "La fréquence de Nyquist est la moitié..." + } + ] +} +``` + +**Type `exercise`** (exercice pratique à soumettre) : +```json +{ + "instructions": "Crée un beat de 16 mesures avec uniquement kick + snare + hi-hat. Upload le WAV final.", + "submission_type": "audio_upload", + "criteria": ["Structure 16 mesures", "3 instruments seulement", "Export WAV 44.1kHz/16bit"], + "auto_grade": false +} +``` + +### 5.2 Résultats dans `lesson_progress.exercise_result` (JSONB) + +```json +{ + "score": 80, + "max_score": 100, + "answers": { "q1": 0, "q2": 2 }, + "submitted_at": "2026-06-01T10:30:00Z", + "submission_s3_key": "...", // si upload audio + "reviewer_feedback": null // si corrigé manuellement plus tard +} +``` + +--- + +## 6. Roles et permissions + +| Action | Role requis | +|--------|------------| +| Voir courses publiés | 🔓 Public | +| S'inscrire | 🔒 User connecté | +| Créer/éditer course | 🔒🎨 content_creator | +| Modérer commentaires | 🔒 moderator | +| Publier course (validation) | 🔒 content_creator (+ approbation admin en V1) | + +**V1** : tout course créé passe par une **validation manuelle** avant publication (qualité, légalité, alignement Talas). + +**V2+** : content_creators vérifiés peuvent publier sans validation. + +--- + +## 7. Règles de qualité minimum (pour publication) + +Checks automatiques avant `POST /courses/:id/publish` : + +- [ ] Cover image uploadée +- [ ] Description ≥ 200 caractères +- [ ] Au moins 1 module +- [ ] Au moins 3 leçons +- [ ] Au moins 1 leçon en `is_free_preview=true` +- [ ] Durée estimée renseignée +- [ ] Toutes les leçons vidéo ont un `hls_manifest_url` + +--- + +## 8. Cache Redis + +| Clé | TTL | Contenu | +|-----|-----|---------| +| `learn:courses:published:p` | 5min | Feed courses | +| `learn:course:` | 10min | Détail course | +| `learn:course::modules` | 10min | Arborescence | +| `learn:user::enrollments` | 1min | Mes inscriptions | + +--- + +## 9. Roadmap implémentation + +### V0.1 (MVP, juin 2026) +- [ ] Tables DB + migrations +- [ ] CRUD courses/modules/lessons (auteur) +- [ ] Upload vidéo + HLS (via stream-server) +- [ ] Inscription + progression (étudiant) +- [ ] Feed public + +### V0.5 (beta, juillet 2026) +- [ ] Commentaires par leçon +- [ ] Reviews +- [ ] Quiz (auto-graded) +- [ ] Tracking position vidéo + +### V1.0 (publique, sept 2026) +- [ ] Exercices (upload audio + review manuelle) +- [ ] Validation admin avant publication +- [ ] Notifications (nouvelle leçon, feedback exercise) +- [ ] Statistiques auteur + +### V1.5+ +- [ ] Certificats (PDF téléchargeable après 100%) +- [ ] Parcours multi-courses (tracks) +- [ ] Transcriptions / sous-titres auto +- [ ] Export SCORM pour intégrations externes +- [ ] Monétisation optionnelle (V2 ou jamais) + +--- + +## 10. Tests + +### 10.1 Tests unitaires +- Validation schémas course/module/lesson +- Calcul progress_percent +- Validation publication + +### 10.2 Tests d'intégration +- Flow auteur : création → upload vidéo → publication +- Flow étudiant : enroll → progress → complete +- Permissions (content_creator vs user normal) + +### 10.3 Tests de charge +- 50 étudiants en stream HLS simultané +- 500 lectures `GET /courses` + +--- + +## Voir aussi + +- [[06_COMMUNAUTE_ECOSYSTEME/Formation_Creators/CURRICULUM_FORMATION_CREATORS]] — curriculum produit +- [[03_APPS_&_SERVICES/ARCHITECTURE_VEZA]] — stream-server Rust réutilisé pour HLS +- [[03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE]] — upload pattern similaire +- [[REGLES_MODERATION]] — règles applicables aux commentaires diff --git a/03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE.md b/03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE.md new file mode 100644 index 0000000..d17f923 --- /dev/null +++ b/03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE.md @@ -0,0 +1,447 @@ +# Spec technique — Module Partage (Samples & Presets) + +> Implémentation backend du dépôt de samples/presets/loops Veza. +> Stack : Go (Gin + GORM) · PostgreSQL 16 · MinIO S3 · Redis · RabbitMQ +> Dernière mise à jour : avril 2026. + +Voir la spec produit : [[06_COMMUNAUTE_ECOSYSTEME/Partage_Samples_Presets/SPEC_DEPOT_SAMPLES]] + +--- + +## 1. Vue d'ensemble + +Le module **Partage** gère l'upload, le stockage, la curation et la découverte de packs de samples/presets sur Veza. + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Frontend │─────▶│ Backend API │─────▶│ PostgreSQL │ +│ React │ │ Go / Gin │ │ (metadata) │ +└──────────────┘ └──────┬───────┘ └──────────────┘ + │ + ├──────────────▶ MinIO S3 + │ (blobs) + │ + ├──────────────▶ RabbitMQ + │ (jobs post-upload) + │ + └──────────────▶ Redis + (cache + rate-limit) +``` + +--- + +## 2. Modèle de données + +### 2.1 Table `sample_packs` + +```sql +CREATE TABLE sample_packs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(80) NOT NULL, + description TEXT NOT NULL CHECK (char_length(description) <= 1000), + pack_type VARCHAR(30) NOT NULL, -- 'sample_pack', 'one_shot', 'loop', 'preset_synth', etc. + license VARCHAR(30) NOT NULL DEFAULT 'CC-BY-SA-4.0', -- enum licences + bpm NUMERIC(6,2), -- optionnel + key_signature VARCHAR(10), -- ex: 'C#m', 'F' + genre VARCHAR(50), + tools_used TEXT, + remix_allowed BOOLEAN NOT NULL DEFAULT true, + parent_pack_id UUID REFERENCES sample_packs(id), -- si c'est un remix + + -- storage + s3_bucket VARCHAR(63) NOT NULL, + s3_object_key VARCHAR(500) NOT NULL, -- path du zip dans MinIO + file_size_bytes BIGINT NOT NULL, + file_count INT NOT NULL, + checksum_sha256 VARCHAR(64) NOT NULL, + + -- preview + preview_s3_key VARCHAR(500), -- preview MP3 généré + preview_duration_sec NUMERIC(4,2), + + -- modération + status VARCHAR(20) NOT NULL DEFAULT 'published', + -- 'draft', 'published', 'flagged', 'removed' + flagged_reason TEXT, + + -- metrics (dénormalisées pour perf) + likes_count INT NOT NULL DEFAULT 0, + downloads_count INT NOT NULL DEFAULT 0, + remix_count INT NOT NULL DEFAULT 0, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ -- soft delete +); + +CREATE INDEX idx_sample_packs_creator ON sample_packs(creator_id) WHERE deleted_at IS NULL; +CREATE INDEX idx_sample_packs_status ON sample_packs(status) WHERE deleted_at IS NULL; +CREATE INDEX idx_sample_packs_created ON sample_packs(created_at DESC) WHERE deleted_at IS NULL AND status = 'published'; +CREATE INDEX idx_sample_packs_downloads ON sample_packs(downloads_count DESC) WHERE deleted_at IS NULL AND status = 'published'; +CREATE INDEX idx_sample_packs_parent ON sample_packs(parent_pack_id) WHERE parent_pack_id IS NOT NULL; +CREATE INDEX idx_sample_packs_genre ON sample_packs(genre) WHERE deleted_at IS NULL AND status = 'published'; +``` + +### 2.2 Table `sample_pack_tags` (N-N) + +```sql +CREATE TABLE sample_pack_tags ( + pack_id UUID REFERENCES sample_packs(id) ON DELETE CASCADE, + tag VARCHAR(40) NOT NULL, + PRIMARY KEY (pack_id, tag) +); + +CREATE INDEX idx_sample_pack_tags_tag ON sample_pack_tags(tag); +``` + +### 2.3 Table `sample_pack_files` (listing interne d'un pack zip) + +```sql +CREATE TABLE sample_pack_files ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + pack_id UUID NOT NULL REFERENCES sample_packs(id) ON DELETE CASCADE, + filename VARCHAR(300) NOT NULL, + relative_path VARCHAR(500) NOT NULL, + file_size_bytes BIGINT NOT NULL, + duration_sec NUMERIC(8,3), -- pour les audios + mime_type VARCHAR(50) +); + +CREATE INDEX idx_sample_pack_files_pack ON sample_pack_files(pack_id); +``` + +### 2.4 Table `sample_pack_likes` + +```sql +CREATE TABLE sample_pack_likes ( + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + pack_id UUID REFERENCES sample_packs(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, pack_id) +); +``` + +### 2.5 Table `sample_pack_downloads` (log + analytics) + +```sql +CREATE TABLE sample_pack_downloads ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + pack_id UUID NOT NULL REFERENCES sample_packs(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + ip_hash VARCHAR(64), -- SHA-256 de l'IP (GDPR friendly) + user_agent VARCHAR(500), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_sample_pack_downloads_pack ON sample_pack_downloads(pack_id, created_at); +CREATE INDEX idx_sample_pack_downloads_user ON sample_pack_downloads(user_id, created_at); +``` + +### 2.6 Table `sample_pack_flags` (signalements) + +```sql +CREATE TABLE sample_pack_flags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + pack_id UUID NOT NULL REFERENCES sample_packs(id) ON DELETE CASCADE, + reporter_id UUID NOT NULL REFERENCES users(id), + reason VARCHAR(50) NOT NULL, + -- 'stolen', 'copyright', 'inappropriate', 'low_quality', 'wrong_license' + details TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + -- 'pending', 'resolved', 'rejected' + resolved_by UUID REFERENCES users(id), + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +--- + +## 3. API Endpoints + +Base : `/api/v1/samples` + +### 3.1 Packs — CRUD + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/packs` | 🔓 | Liste paginée (filtres, tri) | +| GET | `/packs/:id` | 🔓 | Détail d'un pack | +| GET | `/packs/:id/files` | 🔓 | Liste des fichiers dans le pack | +| GET | `/packs/:id/remixes` | 🔓 | Arbre des remix (enfants) | +| POST | `/packs` | 🔒 | Créer un pack (metadata, status=draft) | +| POST | `/packs/:id/upload` | 🔒 | Upload binaire (multipart, max 500MB) | +| POST | `/packs/:id/publish` | 🔒 | Publier (status=draft → published) | +| PUT | `/packs/:id` | 🔒 | Modifier metadata (own only) | +| DELETE | `/packs/:id` | 🔒 | Soft delete (own only) | + +### 3.2 Interactions + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/packs/:id/like` | 🔒 | Like un pack | +| DELETE | `/packs/:id/like` | 🔒 | Retirer like | +| GET | `/packs/:id/download` | 🔒 | Télécharger (URL signée MinIO, expire 5 min) | +| POST | `/packs/:id/flag` | 🔒 | Signaler | +| POST | `/packs/:id/remix` | 🔒 | Créer un pack enfant (remix) | + +### 3.3 Découverte + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/feed` | 🔓 | Feed principal (récents + populaires) | +| GET | `/feed/featured` | 🔓 | Pack de la semaine + curation | +| GET | `/tags/popular` | 🔓 | Top 50 tags | +| GET | `/search` | 🔓 | Recherche full-text (title + tags + desc) | + +### 3.4 Modération + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/flags` | 🔒👑 | Liste des signalements pending | +| POST | `/flags/:id/resolve` | 🔒👑 | Résoudre signalement | +| POST | `/packs/:id/feature` | 🔒👑 | Mettre en "pack de la semaine" | + +### 3.5 Filtres de `/packs` + +``` +?type=sample_pack,one_shot,preset_synth # multi-valeurs +?license=CC-BY-SA-4.0 +?genre=hip-hop,techno +?bpm_min=80&bpm_max=100 +?key=Cm,C +?tags=lofi,organic # AND +?creator= +?remix_allowed=true +?sort=recent|popular|downloads|likes +?page=1&limit=20 # max 50 +``` + +--- + +## 4. Flux techniques + +### 4.1 Upload d'un pack (du draft à la publication) + +``` +[1] POST /packs (metadata) + ← Backend crée sample_packs{status:draft, s3_key:} + → response: {id, upload_url} (URL présignée MinIO) + +[2] PUT (binary) + → Frontend upload direct vers MinIO + +[3] POST /packs/:id/publish + → Backend: + - vérifie checksum + - scan antivirus (ClamAV) + - publie (status=published) + - enqueue job RabbitMQ "post_upload" + → response: {pack} + +[4] Worker "post_upload" (async): + - unzip temporaire en mémoire stream + - extraction metadata audio (FFprobe) + - insert sample_pack_files + - génération preview MP3 (60 sec max, FFmpeg) + - upload preview vers MinIO + - UPDATE sample_packs SET preview_s3_key, preview_duration_sec +``` + +**Limites** : +- Taille max : 500 MB +- Formats autorisés : .zip uniquement (on n'accepte pas .rar, .7z en V1) +- Timeout upload : 30 minutes +- Rate limit : 5 packs / jour / user + +### 4.2 Téléchargement + +``` +GET /packs/:id/download + → Backend: + - check status == 'published' AND deleted_at IS NULL + - generate presigned URL (MinIO, expire 5 min) + - INSERT sample_pack_downloads (log) + - UPDATE sample_packs SET downloads_count = downloads_count + 1 + → response: {download_url} +``` + +**Note** : on n'incrémente PAS via trigger PG pour éviter les contentions. On bump la colonne dénormalisée depuis l'app. + +### 4.3 Remix + +``` +POST /packs/:id/remix (body: {title, description, ...}) + → Backend: + - check remix_allowed sur parent + - check licence parent compatible + - INSERT sample_packs{parent_pack_id: :id, status:draft} + - UPDATE parent SET remix_count = remix_count + 1 + → response: {new_pack_id, upload_url} +``` + +--- + +## 5. Storage MinIO + +### 5.1 Buckets + +| Bucket | Contenu | Visibilité | +|--------|---------|-----------| +| `veza-samples-packs` | ZIP des packs | Privé (accès via URL signée) | +| `veza-samples-previews` | MP3 previews | Public (CDN possible) | + +### 5.2 Naming convention + +``` +veza-samples-packs/ +├── / +│ └── / +│ └── original.zip +│ +veza-samples-previews/ +├── / +│ └── preview.mp3 +``` + +### 5.3 Lifecycle + +- Packs soft-deleted : objets MinIO supprimés après 30 jours (cron) +- Previews : jamais supprimées (tant que le pack existe) + +--- + +## 6. Sécurité et validation + +### 6.1 Validation metadata (backend) + +Validations Zod-like côté backend (Go struct tags + validator) : + +```go +type CreatePackRequest struct { + Title string `json:"title" validate:"required,min=3,max=80"` + Description string `json:"description" validate:"required,min=10,max=1000"` + PackType string `json:"pack_type" validate:"required,oneof=sample_pack one_shot loop preset_synth preset_fx preset_daw ir field_recording drum_kit sound_bank"` + License string `json:"license" validate:"required,oneof=CC0 CC-BY-4.0 CC-BY-SA-4.0 veza_only"` + BPM *float64 `json:"bpm" validate:"omitempty,min=20,max=300"` + KeySignature string `json:"key_signature" validate:"omitempty,max=10"` + Genre string `json:"genre" validate:"omitempty,max=50"` + Tags []string `json:"tags" validate:"required,min=3,max=10,dive,min=2,max=40"` + RemixAllowed bool `json:"remix_allowed"` + ParentPackID *string `json:"parent_pack_id" validate:"omitempty,uuid"` +} +``` + +### 6.2 Antivirus (ClamAV) + +Tout zip uploadé passe par **ClamAV** avant publication. Si détection → status=`removed` + notif admin. + +### 6.3 Anti-duplication + +Checksum SHA-256 du zip comparé avant insertion. Si checksum existe déjà → refus avec message "un pack identique existe". + +### 6.4 Rate limiting + +- 5 uploads / jour / user +- 50 téléchargements / heure / user (anti-scraping) +- 20 likes / minute / user + +### 6.5 RGPD + +- Logs downloads anonymisés (IP → hash) +- Right to deletion : soft delete cascade les fichiers MinIO après délai + +--- + +## 7. Cache Redis + +| Clé | TTL | Contenu | +|-----|-----|---------| +| `samples:feed:recent:p` | 60s | Page N du feed récent | +| `samples:feed:featured` | 5min | Pack de la semaine | +| `samples:pack:` | 5min | Détail pack | +| `samples:tags:popular` | 1h | Top 50 tags | +| `samples:user::likes` | 1h | Set des packs likés par user | + +Invalidation : sur write (publish/delete/like) on invalide les clés concernées. + +--- + +## 8. Considérations de performance + +### 8.1 Feed + +- Pas de `ORDER BY random()` — utiliser un cron qui pré-calcule "featured" +- Pagination par curseur (`created_at + id`) plutôt que offset pour les gros jeux + +### 8.2 Comptages dénormalisés + +- `likes_count`, `downloads_count`, `remix_count` mis à jour depuis l'app (pas trigger PG) +- Job nocturne de reconciliation (anti-drift) : recompte les 3 colonnes + +### 8.3 Recherche full-text + +- Phase 1 : PostgreSQL full-text (`tsvector` sur title + description) +- Phase 2 (quand > 1000 packs) : Elasticsearch déjà dans l'infra + +--- + +## 9. Roadmap implémentation + +### V0.1 (MVP interne, avril 2026) +- [ ] Tables DB + migrations +- [ ] CRUD packs (endpoints 3.1) +- [ ] Upload vers MinIO avec URL signée +- [ ] ClamAV scan +- [ ] Feed simple (récents) + +### V0.5 (beta fondateurs, juin 2026) +- [ ] Preview MP3 auto (FFmpeg) +- [ ] Likes, downloads log +- [ ] Search PG full-text +- [ ] Filtres feed +- [ ] Modération (flags + résolution) + +### V1.0 (ouverture publique, juillet 2026) +- [ ] Remix (parent_pack_id) +- [ ] Curation "pack de la semaine" +- [ ] Tags populaires +- [ ] Analytics basiques pour créateurs + +### V1.5 (post-lancement) +- [ ] Elasticsearch pour search +- [ ] Extraction BPM/key automatique (aubio / essentia) +- [ ] Recommandations "packs similaires" (tags match) + +--- + +## 10. Tests + +### 10.1 Tests unitaires (Go) + +- Validation metadata (validator tags) +- Calcul checksum +- Signature URL MinIO + +### 10.2 Tests d'intégration + +- Flow complet upload → publish → download +- Rate limiting +- Anti-duplication checksum +- RBAC (seul le créateur modifie son pack) + +### 10.3 Tests de charge + +- 100 uploads concurrents +- 1000 téléchargements / seconde +- Feed avec 10 000 packs + +--- + +## Voir aussi + +- [[06_COMMUNAUTE_ECOSYSTEME/Partage_Samples_Presets/SPEC_DEPOT_SAMPLES]] — spec produit +- [[03_APPS_&_SERVICES/ARCHITECTURE_VEZA]] — architecture globale +- [[03_APPS_&_SERVICES/APIs_&_Rust_Modules/ROUTES_API]] — toutes les routes Veza +- [[04_INFRA_DEPLOIEMENT]] — infra MinIO, Redis, RabbitMQ diff --git a/03_APPS_&_SERVICES/Community/Troc/SPEC_TECHNIQUE_TROC.md b/03_APPS_&_SERVICES/Community/Troc/SPEC_TECHNIQUE_TROC.md new file mode 100644 index 0000000..d53cccc --- /dev/null +++ b/03_APPS_&_SERVICES/Community/Troc/SPEC_TECHNIQUE_TROC.md @@ -0,0 +1,427 @@ +# Spec technique — Module Troc + +> Implémentation backend du troc communautaire Veza. +> Stack : Go (Gin + GORM) · PostgreSQL 16 · Redis · WebSocket +> Dernière mise à jour : avril 2026. + +Voir la spec produit : [[06_COMMUNAUTE_ECOSYSTEME/Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE]] + +--- + +## 1. Vue d'ensemble + +Le module **Troc** est un tableau d'annonces avec messagerie intégrée. Pas de marketplace, pas de transactions financières — juste un feed d'annonces et des DMs entre artistes. + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Frontend │─────▶│ Backend API │─────▶│ PostgreSQL │ +└──────────────┘ │ Go / Gin │ │ │ + └──────┬───────┘ └──────────────┘ + │ + ├──────────────▶ Chat existant (DMs) + │ (module Groupes_Chat) + │ + └──────────────▶ Redis + (feed cache, rate-limit) +``` + +--- + +## 2. Modèle de données + +### 2.1 Table `trade_listings` + +```sql +CREATE TABLE trade_listings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(80) NOT NULL, + offering TEXT NOT NULL CHECK (char_length(offering) <= 500), + seeking TEXT NOT NULL CHECK (char_length(seeking) <= 500), + category_offer VARCHAR(30) NOT NULL, + category_seek VARCHAR(30) NOT NULL, + -- 'audio_service', 'visual_creation', 'music_production', + -- 'training_coaching', 'hardware', 'samples_presets', 'other' + delivery_days INT NOT NULL CHECK (delivery_days BETWEEN 1 AND 30), + + constraints TEXT, -- optionnel + location VARCHAR(100), -- optionnel (ville) + + status VARCHAR(20) NOT NULL DEFAULT 'open', + -- 'open', 'in_discussion', 'concluded', 'cancelled', 'removed' + + -- si conclu, info sur le closing + concluded_with_id UUID REFERENCES users(id), + concluded_at TIMESTAMPTZ, + + -- metrics + views_count INT NOT NULL DEFAULT 0, + interested_count INT NOT NULL DEFAULT 0, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ, -- auto-archive après 60 jours + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_trade_listings_creator ON trade_listings(creator_id) WHERE deleted_at IS NULL; +CREATE INDEX idx_trade_listings_status ON trade_listings(status) WHERE deleted_at IS NULL; +CREATE INDEX idx_trade_listings_category ON trade_listings(category_offer, category_seek) WHERE deleted_at IS NULL; +CREATE INDEX idx_trade_listings_open ON trade_listings(created_at DESC) WHERE status = 'open' AND deleted_at IS NULL; +``` + +### 2.2 Table `trade_listing_examples` (liens vers morceaux Veza) + +```sql +CREATE TABLE trade_listing_examples ( + listing_id UUID REFERENCES trade_listings(id) ON DELETE CASCADE, + track_id UUID REFERENCES tracks(id) ON DELETE CASCADE, + PRIMARY KEY (listing_id, track_id) +); +``` + +### 2.3 Table `trade_interests` (qui s'intéresse à quoi) + +```sql +CREATE TABLE trade_interests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + listing_id UUID NOT NULL REFERENCES trade_listings(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + conversation_id UUID, -- référence vers DM créée + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (listing_id, user_id) +); + +CREATE INDEX idx_trade_interests_listing ON trade_interests(listing_id); +CREATE INDEX idx_trade_interests_user ON trade_interests(user_id); +``` + +### 2.4 Table `trade_recognitions` (badges post-closing) + +```sql +CREATE TABLE trade_recognitions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + listing_id UUID NOT NULL REFERENCES trade_listings(id) ON DELETE CASCADE, + from_user_id UUID NOT NULL REFERENCES users(id), + to_user_id UUID NOT NULL REFERENCES users(id), + badges VARCHAR(20)[] NOT NULL, + -- array : ['reliable', 'punctual', 'inventive', 'patient', 'precise', 'warm'] + note TEXT, -- optionnel, privé + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (listing_id, from_user_id, to_user_id) +); + +CREATE INDEX idx_trade_recognitions_to_user ON trade_recognitions(to_user_id); +``` + +### 2.5 Vue matérialisée `user_trade_reputation` + +```sql +CREATE MATERIALIZED VIEW user_trade_reputation AS +SELECT + u.id AS user_id, + COUNT(DISTINCT tl.id) FILTER (WHERE tl.status = 'concluded') AS concluded_count, + COUNT(DISTINCT tl.id) FILTER (WHERE tl.status = 'cancelled') AS cancelled_count, + COUNT(*) FILTER (WHERE 'reliable' = ANY(tr.badges)) AS badge_reliable, + COUNT(*) FILTER (WHERE 'punctual' = ANY(tr.badges)) AS badge_punctual, + COUNT(*) FILTER (WHERE 'inventive' = ANY(tr.badges)) AS badge_inventive, + COUNT(*) FILTER (WHERE 'patient' = ANY(tr.badges)) AS badge_patient, + COUNT(*) FILTER (WHERE 'precise' = ANY(tr.badges)) AS badge_precise, + COUNT(*) FILTER (WHERE 'warm' = ANY(tr.badges)) AS badge_warm +FROM users u +LEFT JOIN trade_listings tl ON tl.creator_id = u.id OR tl.concluded_with_id = u.id +LEFT JOIN trade_recognitions tr ON tr.to_user_id = u.id +GROUP BY u.id; + +CREATE UNIQUE INDEX idx_user_trade_reputation ON user_trade_reputation(user_id); +-- Refresh nocturne via cron +``` + +### 2.6 Table `trade_flags` (signalements) + +```sql +CREATE TABLE trade_flags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + listing_id UUID NOT NULL REFERENCES trade_listings(id), + reporter_id UUID NOT NULL REFERENCES users(id), + reason VARCHAR(50) NOT NULL, + -- 'spam', 'inappropriate', 'money_involved', 'stolen_content', 'multi_account' + details TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + resolved_by UUID REFERENCES users(id), + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +--- + +## 3. API Endpoints + +Base : `/api/v1/trades` + +### 3.1 Annonces — CRUD + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/listings` | 🔓 | Liste paginée (filtres) | +| GET | `/listings/:id` | 🔓 | Détail d'une annonce | +| POST | `/listings` | 🔒 | Créer annonce | +| PUT | `/listings/:id` | 🔒 | Modifier (own only, tant que status='open') | +| POST | `/listings/:id/cancel` | 🔒 | Annuler (own only) | +| POST | `/listings/:id/conclude` | 🔒 | Clôturer avec un user | +| DELETE | `/listings/:id` | 🔒 | Soft delete (own only) | + +### 3.2 Interactions + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/listings/:id/interest` | 🔒 | "Je suis intéressé" → crée DM | +| POST | `/listings/:id/flag` | 🔒 | Signaler annonce | +| POST | `/listings/:id/recognition` | 🔒 | Donner reconnaissances après closing | + +### 3.3 Découverte + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/feed` | 🔓 | Feed général (filtres, tri) | +| GET | `/categories` | 🔓 | Liste des catégories | +| GET | `/users/:id/reputation` | 🔓 | Réputation troc d'un user | +| GET | `/users/:id/listings` | 🔓 | Annonces d'un user (actives + historique) | +| GET | `/me/listings` | 🔒 | Mes annonces | +| GET | `/me/interests` | 🔒 | Annonces auxquelles je suis intéressé | + +### 3.4 Modération + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/flags` | 🔒👑 | Liste des signalements | +| POST | `/flags/:id/resolve` | 🔒👑 | Résoudre signalement | +| POST | `/listings/:id/remove` | 🔒👑 | Retirer annonce (admin) | + +### 3.5 Filtres de `/feed` + +``` +?category_offer=audio_service,music_production +?category_seek=music_production +?status=open # default +?delivery_days_max=7 +?creator= +?location=Paris +?sort=recent|popular # popular = interested_count DESC +?page=1&limit=20 +``` + +--- + +## 4. Flux techniques + +### 4.1 Créer une annonce + +``` +POST /listings + Body: { title, offering, seeking, category_offer, category_seek, + delivery_days, constraints?, location?, example_track_ids? } + +Backend: + - Validation metadata + - Check rate limit (max 5 annonces actives simultanées par user) + - INSERT trade_listings (status='open', expires_at=NOW()+60days) + - INSERT trade_listing_examples si track_ids fournis + - Notification broadcast : "nouvelle annonce dans catégorie X" + +Response: { listing } +``` + +### 4.2 Marquer son intérêt (le pivot central) + +``` +POST /listings/:id/interest + +Backend: + - Check listing.status == 'open' + - Check creator_id != current_user_id (pas d'auto-intérêt) + - Check unique (listing_id, user_id) + - INSERT trade_interests + - UPDATE trade_listings SET interested_count += 1 + - Créer conversation DM via module Chat : + POST internal /chat/conversations { + participants: [listing.creator_id, current_user_id], + context: { type: 'trade', listing_id: :id } + } + - Premier message auto-injecté : + "🤝 [Username] s'intéresse à ton annonce : '' + Il/elle offre : <offering> + Il/elle cherche : <seeking>" + - Notification au créateur de l'annonce + +Response: { conversation_id } +``` + +### 4.3 Clôturer un troc + +``` +POST /listings/:id/conclude + Body: { concluded_with_user_id } + +Backend: + - Check current_user == listing.creator_id + - Check concluded_with_user_id existe dans trade_interests(listing_id) + - UPDATE trade_listings SET status='concluded', concluded_with_id=..., concluded_at=NOW() + - Notification aux deux : "Le troc est clôturé. Tu peux laisser des reconnaissances." + - Créer 2 tasks backend (lien d'action dans notif) pour demander les reconnaissances + +Response: { listing } +``` + +### 4.4 Donner des reconnaissances + +``` +POST /listings/:id/recognition + Body: { to_user_id, badges: ['reliable', 'warm'], note? } + +Backend: + - Check listing.status == 'concluded' + - Check current_user IN (listing.creator_id, listing.concluded_with_id) + - Check to_user_id is the other party + - Check unique (listing_id, from_user_id, to_user_id) + - INSERT trade_recognitions + - Incremental update user_trade_reputation (ou attendre refresh nocturne) + +Response: { recognition } +``` + +### 4.5 Auto-archivage + +**Cron nocturne** : listings avec `expires_at < NOW()` et `status = 'open'` → status `cancelled` (auto). + +--- + +## 5. Intégration avec module Chat existant + +Le troc **réutilise** le système de DMs du module `Groupes_Chat` (pas de chat dédié). Intégration : + +- Chaque "intérêt" crée une conversation DM +- Les conversations issues de troc ont un **context** `{type: 'trade', listing_id: ...}` +- Affichage spécial dans le chat : bandeau "Discussion autour de l'annonce : <title>" +- Bouton rapide dans le chat : "Clôturer ce troc" (visible au créateur de l'annonce uniquement) + +--- + +## 6. Règles de validation + +### 6.1 Validation metadata + +```go +type CreateListingRequest struct { + Title string `validate:"required,min=5,max=80"` + Offering string `validate:"required,min=20,max=500"` + Seeking string `validate:"required,min=20,max=500"` + CategoryOffer string `validate:"required,oneof=audio_service visual_creation music_production training_coaching hardware samples_presets other"` + CategorySeek string `validate:"required,oneof=audio_service visual_creation music_production training_coaching hardware samples_presets other"` + DeliveryDays int `validate:"required,min=1,max=30"` + Constraints string `validate:"omitempty,max=300"` + Location string `validate:"omitempty,max=100"` + ExampleTracks []string `validate:"omitempty,max=5,dive,uuid"` +} +``` + +### 6.2 Détection de mots interdits + +Check automatique dans `offering` et `seeking` : +- Si mention de montants en euros/dollars/etc. → **refus** avec message "Le troc n'implique pas d'argent" +- Mots : `€`, `$`, `EUR`, `USD`, `euros`, `dollars`, `payer`, `vendre`, `acheter` +- Liste maintenue dans config, facile à étendre + +### 6.3 Rate limiting + +- Max 5 annonces actives simultanées par user +- Max 2 nouvelles annonces / jour +- Max 10 "intérêts" / jour (anti-spam DMs) +- 1 seul intérêt par listing (unique constraint) + +--- + +## 7. Cache Redis + +| Clé | TTL | Contenu | +|-----|-----|---------| +| `trades:feed:open:p<N>:<filter_hash>` | 30s | Page feed filtrée | +| `trades:listing:<id>` | 5min | Détail listing | +| `trades:user:<uid>:reputation` | 1h | Réputation user (vue matérialisée) | + +Invalidation : sur write (create/update/cancel/conclude), invalider `trades:feed:open:*` + la clé listing concernée. + +--- + +## 8. Sécurité + +### 8.1 Abus anti-argent + +- Détection regex sur montants (§6.2) +- Signalement user si mention argent confirmée +- Escalade : 2 signalements → review manuelle + +### 8.2 Multi-compte + +- Fingerprint léger : IP + user-agent + creation date +- Vérification manuelle si suspicion (pas auto-action) + +### 8.3 Harcèlement via DMs + +- Bloquer un user ferme la conversation troc +- Un user bloqué ne peut plus "s'intéresser" aux annonces du bloqueur + +--- + +## 9. Roadmap implémentation + +### V0.1 (MVP interne, mai 2026) +- [ ] Tables DB + migrations +- [ ] CRUD annonces (endpoints 3.1) +- [ ] Création DM au "intérêt" +- [ ] Feed simple (récent, filtres basiques) + +### V0.5 (beta fondateurs, juin 2026) +- [ ] Filtres avancés +- [ ] Reconnaissances post-closing +- [ ] Vue matérialisée user_trade_reputation +- [ ] Modération (flags) +- [ ] Détection anti-argent + +### V1.0 (ouverture publique, juillet 2026) +- [ ] Notifications (email + push in-app) +- [ ] Profil public "réputation troc" +- [ ] Export historique de ses trocs +- [ ] Feed personnalisé (catégories préférées) + +### V1.5+ +- [ ] Suggestions automatiques d'annonces complémentaires +- [ ] Statistiques publiques (nb trocs Veza, top catégories) +- [ ] Calendrier des trocs (pour les services à date) + +--- + +## 10. Tests + +### 10.1 Tests unitaires +- Validation metadata (validator tags + regex anti-argent) +- Règles métier (own listing, status transitions) + +### 10.2 Tests d'intégration +- Flow complet : création → intérêt → closing → reconnaissance +- Cascade deletes +- Cron auto-archive + +### 10.3 Tests de charge +- 100 créations d'annonces / minute +- Feed avec 5 000 annonces actives + +--- + +## Voir aussi + +- [[06_COMMUNAUTE_ECOSYSTEME/Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE]] — spec produit +- [[03_APPS_&_SERVICES/Community/Groupes_Chat/README]] — module Chat (DMs réutilisés) +- [[03_APPS_&_SERVICES/ARCHITECTURE_VEZA]] — architecture globale +- [[REGLES_MODERATION]] — règles communauté diff --git a/03_APPS_&_SERVICES/Personal/Cloud_Perso/SPEC_CLOUD_PERSO.md b/03_APPS_&_SERVICES/Personal/Cloud_Perso/SPEC_CLOUD_PERSO.md new file mode 100644 index 0000000..b70d85f --- /dev/null +++ b/03_APPS_&_SERVICES/Personal/Cloud_Perso/SPEC_CLOUD_PERSO.md @@ -0,0 +1,437 @@ +# Spec technique — Cloud Perso Veza + +> Espace de stockage personnel pour chaque artiste Veza : fichiers audio, projets DAW, presets, snapshots. +> Stack : Go (Gin + GORM) · PostgreSQL 16 · MinIO S3 · Redis +> Dernière mise à jour : avril 2026. + +--- + +## 1. Vue d'ensemble + +### 1.1 Positionnement + +**Cloud Perso ≠ Partage Samples (public)** + +| | Cloud Perso | Partage Samples | +|---|-------------|-----------------| +| Audience | Privé (le user + ses invités) | Public (tout Veza) | +| Finalité | Travail en cours, backup, projets | Partager avec la communauté | +| Structure | Arborescence fichiers/dossiers | Packs catalogués | +| Quota | Limité par plan | Quota public séparé | + +### 1.2 Concurrents de référence + +- Dropbox, Google Drive : UX simple, mais GAFAM +- Nextcloud, Seafile : self-hosted sérieux mais UX lourde +- Splice Studio : audio-spécifique, SaaS + +**Positionnement Veza** : un espace perso **audio-centré**, intégré à la plateforme, simple. + +### 1.3 Cas d'usage principaux + +1. Sauvegarder ses projets DAW en cours +2. Transférer des fichiers entre son PC studio et son laptop de route +3. Partager un WIP avec un collaborateur via lien privé temporaire +4. Garder un backup des morceaux finalisés +5. Stocker ses presets perso organisés + +--- + +## 2. Architecture + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Frontend Web │───────▶│ Backend API │───────▶│ PostgreSQL │ +│ (file browser│ │ Go / Gin │ │ (metadata) │ +└──────────────┘ └──────┬───────┘ └──────────────┘ + │ +┌──────────────┐ │ +│ Desktop Sync │ └───────────────▶ MinIO S3 +│ (V2, future) │ (blobs) +└──────────────┘ │ + │ lifecycle + ▼ + Cold storage (30j+) +``` + +--- + +## 3. Modèle de données + +### 3.1 Table `user_cloud_quotas` + +```sql +CREATE TABLE user_cloud_quotas ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + plan VARCHAR(30) NOT NULL DEFAULT 'free', + -- 'free', 'plus', 'pro', 'custom' + quota_bytes BIGINT NOT NULL, + used_bytes BIGINT NOT NULL DEFAULT 0, + file_count INT NOT NULL DEFAULT 0, + max_file_size_bytes BIGINT NOT NULL DEFAULT 2147483648, -- 2 GB + max_files INT NOT NULL DEFAULT 10000, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +``` + +### 3.2 Table `cloud_folders` + +```sql +CREATE TABLE cloud_folders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + parent_id UUID REFERENCES cloud_folders(id) ON DELETE CASCADE, + name VARCHAR(200) NOT NULL, + path VARCHAR(2000) NOT NULL, -- path complet denormalise + is_trash BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + UNIQUE (user_id, parent_id, name) DEFERRABLE INITIALLY DEFERRED +); + +CREATE INDEX idx_cloud_folders_user_parent ON cloud_folders(user_id, parent_id) WHERE deleted_at IS NULL; +CREATE INDEX idx_cloud_folders_path ON cloud_folders(user_id, path) WHERE deleted_at IS NULL; +``` + +### 3.3 Table `cloud_files` + +```sql +CREATE TABLE cloud_files ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + folder_id UUID REFERENCES cloud_folders(id) ON DELETE CASCADE, + + name VARCHAR(300) NOT NULL, + extension VARCHAR(20), + mime_type VARCHAR(100), + + file_size_bytes BIGINT NOT NULL, + checksum_sha256 VARCHAR(64) NOT NULL, + + -- storage + s3_bucket VARCHAR(63) NOT NULL, + s3_object_key VARCHAR(500) NOT NULL, + + -- metadata audio (si applicable, extraite à l'upload) + audio_metadata JSONB, + -- ex: { duration_sec, sample_rate, bit_depth, channels, format } + + -- versioning (pointeur vers version précédente) + previous_version_id UUID REFERENCES cloud_files(id), + version_number INT NOT NULL DEFAULT 1, + + -- trash + is_trash BOOLEAN NOT NULL DEFAULT false, + trashed_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_cloud_files_user_folder ON cloud_files(user_id, folder_id) WHERE deleted_at IS NULL AND is_trash = false; +CREATE INDEX idx_cloud_files_trash ON cloud_files(user_id, trashed_at) WHERE is_trash = true; +CREATE INDEX idx_cloud_files_checksum ON cloud_files(user_id, checksum_sha256); +``` + +### 3.4 Table `cloud_share_links` + +```sql +CREATE TABLE cloud_share_links ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + file_id UUID REFERENCES cloud_files(id) ON DELETE CASCADE, + folder_id UUID REFERENCES cloud_folders(id) ON DELETE CASCADE, + + token VARCHAR(40) NOT NULL UNIQUE, -- random URL-safe + password_hash VARCHAR(100), -- optionnel + + max_downloads INT, -- null = illimité + downloads_count INT NOT NULL DEFAULT 0, + + expires_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + revoked_at TIMESTAMPTZ, + + CHECK ((file_id IS NOT NULL) OR (folder_id IS NOT NULL)) +); + +CREATE INDEX idx_cloud_share_links_token ON cloud_share_links(token) WHERE revoked_at IS NULL; +``` + +--- + +## 4. Plans et quotas + +### 4.1 V1 — un seul plan gratuit + +| Plan | Quota | Max file | Max files | +|------|-------|----------|-----------| +| `free` | 10 GB | 2 GB | 10 000 | + +### 4.2 V2 — plans additionnels (si demande) + +| Plan | Quota | Max file | Max files | Prix (HT) | +|------|-------|----------|-----------|-----------| +| `free` | 10 GB | 2 GB | 10 000 | 0 € | +| `plus` | 100 GB | 5 GB | 50 000 | 4 €/mois | +| `pro` | 500 GB | 10 GB | 200 000 | 12 €/mois | +| `custom` | Sur-mesure | - | - | Devis | + +**Note** : les plans payants n'arriveront que si la demande émerge. Philosophie : gratuit solide par défaut. + +--- + +## 5. API Endpoints + +Base : `/api/v1/cloud` + +### 5.1 Navigation + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/folders/:id` | 🔒 | Contenu d'un dossier (sous-dossiers + fichiers) | +| GET | `/folders/root` | 🔒 | Dossier racine | +| GET | `/files/:id` | 🔒 | Metadata fichier | +| GET | `/search?q=...` | 🔒 | Recherche dans tout le cloud | +| GET | `/quota` | 🔒 | Mon quota | + +### 5.2 CRUD fichiers + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/files` | 🔒 | Créer file entry (metadata) + génère upload URL | +| POST | `/files/:id/complete-upload` | 🔒 | Finaliser upload (vérif checksum) | +| PUT | `/files/:id` | 🔒 | Renommer / déplacer | +| DELETE | `/files/:id` | 🔒 | Soft delete → trash | +| POST | `/files/:id/restore` | 🔒 | Restaurer depuis trash | +| GET | `/files/:id/download` | 🔒 | URL signée téléchargement | + +### 5.3 CRUD dossiers + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/folders` | 🔒 | Créer dossier | +| PUT | `/folders/:id` | 🔒 | Renommer / déplacer | +| DELETE | `/folders/:id` | 🔒 | Soft delete → trash | + +### 5.4 Partage + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/share` | 🔒 | Créer lien partage (file ou folder) | +| GET | `/share/:token` | 🔓 | Accéder à un partage (password si défini) | +| POST | `/share/:token/download` | 🔓 | Télécharger | +| DELETE | `/share/:id` | 🔒 | Révoquer lien | +| GET | `/me/shares` | 🔒 | Mes partages actifs | + +### 5.5 Trash + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/trash` | 🔒 | Contenu de la corbeille | +| POST | `/trash/empty` | 🔒 | Vider corbeille | +| POST | `/trash/purge-item/:id` | 🔒 | Supprimer définitivement un item | + +--- + +## 6. Flux techniques + +### 6.1 Upload fichier + +``` +[1] POST /cloud/files + Body: { name, size_bytes, mime_type, folder_id, checksum_sha256 } + → Backend: + - Check quota disponible + - Check nom unique dans folder + - INSERT cloud_files (s3_object_key pre-généré) + - Génère upload URL MinIO (PUT, expire 1h) + → response: { file_id, upload_url } + +[2] PUT <upload_url> (binary) + → Upload direct MinIO + +[3] POST /cloud/files/:id/complete-upload + → Backend: + - GET object MinIO, vérifie checksum + - Extraction metadata audio (si audio, via FFprobe async) + - UPDATE user_cloud_quotas (used_bytes += size) + - UPDATE cloud_files (mettre audio_metadata) + → response: { file } +``` + +### 6.2 Download fichier + +``` +GET /cloud/files/:id/download + → Backend: + - Check ownership + - Generate presigned URL (expire 5 min) + → response: { download_url } +``` + +### 6.3 Partage via lien + +``` +POST /cloud/share + Body: { file_id, password?, max_downloads?, expires_in_hours? } + → Backend: + - Génère token (40 chars random URL-safe) + - Hash password si fourni (bcrypt) + - INSERT cloud_share_links + → response: { share_url: "/s/<token>" } + +GET /cloud/share/<token> (public) + → Backend: + - Load link (check not revoked, not expired) + - Si password requis : demande password + → response: { file_info, needs_password } + +POST /cloud/share/<token>/download (public) + Body: { password? } + → Backend: + - Check password si défini + - Check max_downloads non atteint + - UPDATE downloads_count + - Generate presigned URL MinIO + → response: { download_url } +``` + +--- + +## 7. Versioning + +### 7.1 Règle + +**V1** : pas de versioning automatique. Upload d'un fichier de même nom au même endroit **remplace** l'ancien. + +**V2+** : versioning automatique, max 10 versions retenues, historique visible dans UI. + +### 7.2 Comment ça marchera (V2) + +- Chaque "remplacement" crée un nouveau `cloud_files` row +- Le précédent garde `previous_version_id` +- L'UI expose un bouton "Historique" par fichier +- Rotation : au-delà de 10, les plus anciennes sont purgées + +--- + +## 8. Storage MinIO + +### 8.1 Bucket + +`veza-cloud-perso` — privé, accès via URLs signées uniquement. + +### 8.2 Naming convention + +``` +veza-cloud-perso/ +├── <user_uuid>/ +│ └── <file_uuid>/ +│ └── blob +``` + +**Note** : on utilise l'UUID et non le nom pour éviter les problèmes de charset / renommage. + +### 8.3 Lifecycle + +- **Fichiers trash** : purge automatique après 30 jours +- **Fichiers supprimés définitivement** : purge immédiate +- **Comptes supprimés (user)** : purge après 30 jours (cascade soft delete → purge) + +--- + +## 9. Sécurité + +### 9.1 Contrôle d'accès + +- Chaque endpoint vérifie `user_id == current_user.id` +- Pas d'ACL partagée entre users (V1). Sharing = lien public. + +### 9.2 Antivirus + +**V1** : pas de scan antivirus (les fichiers ne sont jamais téléchargés par d'autres users sauf via liens de partage). + +**V2+** : scan ClamAV sur les fichiers partagés publiquement. + +### 9.3 Rate limiting + +- Upload : 50 fichiers / heure / user +- Download : 200 / heure / user +- Partage : 20 liens actifs simultanés / user + +### 9.4 Chiffrement + +- **At rest** : chiffrement MinIO côté serveur (AES-256-SSE) +- **In transit** : HTTPS obligatoire, URLs signées avec signature SHA-256 + +--- + +## 10. Desktop sync (V2, futur) + +### 10.1 Vision + +Un client desktop (Electron ou natif) qui synchronise un dossier local avec le cloud perso. + +### 10.2 Architecture envisagée + +- Protocol : basé sur WebSocket pour notifications + REST pour upload +- Algorithm : delta sync (checksum des blocks) +- Conflict resolution : "latest wins" avec backup du précédent + +### 10.3 Plateforms V2 + +- Linux (AppImage) +- macOS (.dmg) +- Windows (.exe MSI) + +--- + +## 11. Monitoring + +| Métrique | Cible | +|----------|-------| +| Upload réussi ratio | > 98% | +| Temps upload 100MB | < 30s | +| Temps génération URL signée | < 50ms | +| Quota usage moyen user | Monitorer | +| Sharing link usage | Monitorer | + +--- + +## 12. Roadmap + +### V1.0 (Q3 2026, post-lancement public) +- [ ] Tables DB + migrations +- [ ] CRUD fichiers/dossiers +- [ ] Upload/download via URL signée +- [ ] Quotas +- [ ] Partage via lien (password + expiration) +- [ ] Trash + purge auto 30j +- [ ] File browser web + +### V1.5 (Q4 2026) +- [ ] Recherche full-text (noms) +- [ ] Extraction métadonnées audio auto +- [ ] Prévisualisation audio (player intégré) +- [ ] Drag & drop multi-fichiers +- [ ] Upload en arrière-plan + +### V2.0 (2027) +- [ ] Versioning +- [ ] Desktop sync client +- [ ] Plans payants (si demande) +- [ ] Scan ClamAV sur fichiers partagés +- [ ] Compression à la volée (option) + +--- + +## Voir aussi + +- [[Personal/README]] — vue d'ensemble module Personal +- [[Personal/AudioGridder_Client/SPEC_AUDIOGRIDDER]] — autre feature Personal +- [[03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE]] — stockage public (vs perso) +- [[04_INFRA_DEPLOIEMENT]] — MinIO, storage diff --git a/03_APPS_&_SERVICES/Shop/Backend/SPEC_BACKEND_SHOP.md b/03_APPS_&_SERVICES/Shop/Backend/SPEC_BACKEND_SHOP.md new file mode 100644 index 0000000..77ed3cf --- /dev/null +++ b/03_APPS_&_SERVICES/Shop/Backend/SPEC_BACKEND_SHOP.md @@ -0,0 +1,442 @@ +# Spec technique — Backend Shop + +> Backend boutique Veza : catalogue produits, stock, commandes, expédition. +> Stack : Go (Gin + GORM) · PostgreSQL 16 · Redis · RabbitMQ +> Dernière mise à jour : avril 2026. + +--- + +## 1. Vue d'ensemble + +Le backend Shop gère le cycle de vie complet d'une commande : du catalogue produits à la livraison, en passant par le panier, le checkout et l'expédition. + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Frontend │─────▶│ Backend API │─────▶│ PostgreSQL │ +│ Shop │ │ Go / Gin │ │ │ +└──────────────┘ └──────┬───────┘ └──────────────┘ + │ + ├──▶ Paiement (Mollie) + │ + ├──▶ RabbitMQ + │ (order.paid, shipment.ready) + │ + └──▶ Email (listmonk self-hosted) +``` + +--- + +## 2. Catalogue produits + +### 2.1 Structure — micro modulaire + +Un micro Talas = **produit configurable**. Le client choisit : +- Le modèle (Talas Lite electret OU Talas One condenser) +- La finition (brute, noire, anodisée couleur) +- Les options (pochette tissu, suspension, bonnette) + +### 2.2 Table `products` + +```sql +CREATE TABLE products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug VARCHAR(100) NOT NULL UNIQUE, + title VARCHAR(150) NOT NULL, + subtitle VARCHAR(200), + description_md TEXT NOT NULL, + + product_type VARCHAR(30) NOT NULL, + -- 'microphone', 'accessory', 'bundle', 'spare_part' + + base_price_cents INT NOT NULL, -- prix de référence HT + + cover_image_s3_key VARCHAR(500), + gallery_s3_keys VARCHAR(500)[] DEFAULT '{}', + + weight_grams INT, -- pour calcul shipping + dimensions_mm JSONB, -- { l, w, h } + + is_active BOOLEAN NOT NULL DEFAULT false, + is_preorder BOOLEAN NOT NULL DEFAULT false, + available_from TIMESTAMPTZ, -- pour précommandes + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_products_active ON products(is_active) WHERE deleted_at IS NULL; +CREATE INDEX idx_products_type ON products(product_type) WHERE is_active = true; +``` + +### 2.3 Table `product_variants` + +```sql +CREATE TABLE product_variants ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, + sku VARCHAR(50) NOT NULL UNIQUE, + + variant_name VARCHAR(100) NOT NULL, -- ex: "Finition noire" + attributes JSONB NOT NULL, -- { finition: "noir", couleur: null } + + price_modifier_cents INT NOT NULL DEFAULT 0, -- différence vs base + + stock_quantity INT NOT NULL DEFAULT 0, + reserved_quantity INT NOT NULL DEFAULT 0, -- stock en cours d'achat + low_stock_threshold INT NOT NULL DEFAULT 3, + + is_active BOOLEAN NOT NULL DEFAULT true, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_product_variants_product ON product_variants(product_id) WHERE is_active = true; +CREATE INDEX idx_product_variants_sku ON product_variants(sku); +``` + +### 2.4 Table `product_options` (options ajoutables) + +```sql +CREATE TABLE product_options ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, + option_name VARCHAR(100) NOT NULL, -- "Pochette tissu" + option_type VARCHAR(20) NOT NULL, -- 'addon', 'required_choice' + price_cents INT NOT NULL DEFAULT 0, + sort_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true +); +``` + +--- + +## 3. Gestion du stock + +### 3.1 Principe "réservation temporaire" + +Quand un user ajoute un variant au panier → **pas de réservation**. +Quand il passe au checkout → **réservation 15 minutes**. +Si paiement OK → réservation devient consommation. +Si paiement échoue / timeout → réservation libérée. + +### 3.2 Table `stock_reservations` + +```sql +CREATE TABLE stock_reservations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + variant_id UUID NOT NULL REFERENCES product_variants(id), + order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + quantity INT NOT NULL, + reserved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL, + released_at TIMESTAMPTZ, + consumed_at TIMESTAMPTZ +); + +CREATE INDEX idx_stock_reservations_variant ON stock_reservations(variant_id) WHERE released_at IS NULL AND consumed_at IS NULL; +CREATE INDEX idx_stock_reservations_expires ON stock_reservations(expires_at) WHERE released_at IS NULL AND consumed_at IS NULL; +``` + +### 3.3 Cron de libération + +Cron toutes les 5 min : `UPDATE stock_reservations SET released_at = NOW() WHERE expires_at < NOW() AND released_at IS NULL AND consumed_at IS NULL`. + +Puis `UPDATE product_variants SET reserved_quantity = recalcul`. + +### 3.4 Alertes stock bas + +Quand `stock_quantity - reserved_quantity <= low_stock_threshold` → email admin. + +--- + +## 4. Panier (Cart) + +### 4.1 Deux modes + +**Anonyme** : panier stocké en cookie/localStorage frontend (TTL 7 jours). +**Connecté** : panier persisté en DB, sync entre devices. + +### 4.2 Table `carts` (user connecté) + +```sql +CREATE TABLE carts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id) +); + +CREATE TABLE cart_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + cart_id UUID NOT NULL REFERENCES carts(id) ON DELETE CASCADE, + variant_id UUID NOT NULL REFERENCES product_variants(id), + quantity INT NOT NULL DEFAULT 1 CHECK (quantity > 0), + selected_options UUID[] DEFAULT '{}', -- IDs product_options + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (cart_id, variant_id, selected_options) +); +``` + +### 4.3 Fusion panier anonyme → connecté + +À la connexion : merge des items du localStorage dans le cart DB. Stratégie : si conflit sur variant, garder qté max. + +--- + +## 5. Commandes (Orders) + +### 5.1 Table `orders` + +```sql +CREATE TABLE orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_number VARCHAR(20) NOT NULL UNIQUE, -- 'TALAS-2026-00123' + user_id UUID NOT NULL REFERENCES users(id), + + status VARCHAR(30) NOT NULL DEFAULT 'draft', + -- 'draft', 'awaiting_payment', 'paid', 'preparing', + -- 'shipped', 'delivered', 'cancelled', 'refunded' + + -- montants en centimes + subtotal_cents INT NOT NULL, + shipping_cents INT NOT NULL, + discount_cents INT NOT NULL DEFAULT 0, + vat_rate NUMERIC(4,2) NOT NULL DEFAULT 0, + vat_cents INT NOT NULL DEFAULT 0, + total_cents INT NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'EUR', + + -- contact + contact_email VARCHAR(200) NOT NULL, + contact_phone VARCHAR(30), + + -- adresses (snapshot au moment de la commande) + shipping_address JSONB NOT NULL, + billing_address JSONB NOT NULL, + + -- expédition + shipping_method VARCHAR(30), + tracking_number VARCHAR(100), + tracking_url VARCHAR(500), + shipped_at TIMESTAMPTZ, + delivered_at TIMESTAMPTZ, + + -- notes + customer_note TEXT, + internal_note TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + cancelled_at TIMESTAMPTZ, + cancellation_reason VARCHAR(100) +); + +CREATE INDEX idx_orders_user ON orders(user_id, created_at DESC); +CREATE INDEX idx_orders_status ON orders(status) WHERE status NOT IN ('cancelled', 'delivered'); +CREATE INDEX idx_orders_number ON orders(order_number); +``` + +### 5.2 Table `order_items` + +```sql +CREATE TABLE order_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, + variant_id UUID NOT NULL REFERENCES product_variants(id), + + -- snapshot au moment de la commande + product_title VARCHAR(200) NOT NULL, + variant_name VARCHAR(100) NOT NULL, + sku VARCHAR(50) NOT NULL, + + unit_price_cents INT NOT NULL, + quantity INT NOT NULL CHECK (quantity > 0), + options_description TEXT, + options_price_cents INT NOT NULL DEFAULT 0, + + line_total_cents INT NOT NULL -- (unit+options)*qty +); +``` + +### 5.3 Numérotation commande + +Format : `TALAS-YYYY-NNNNN` (séquence PG annuelle, rotation 1er janvier). + +--- + +## 6. Lifecycle commande + +``` +[draft] + │ (user construit sa commande) + ▼ +[awaiting_payment] + │ (checkout validé, paiement créé) + │ + ├── timeout 15min sans paiement → [cancelled] + │ + ▼ (webhook payment.paid) +[paid] + │ (stock consommé, email envoyé) + ▼ +[preparing] + │ (manuel : Nikola prépare le colis) + ▼ +[shipped] + │ (tracking_number renseigné, email envoyé) + ▼ +[delivered] + │ (manuel ou auto via webhook transporteur) + ▼ +[fin] + +Transitions alternatives : +- [paid/preparing] → [cancelled] (annulation, trigger refund) +- [shipped] → [refunded] (retour après livraison) +``` + +--- + +## 7. Expédition + +### 7.1 Transporteurs supportés V1 + +| Transporteur | Zone | API | +|-------------|------|-----| +| **La Poste Colissimo** | FR + intl | API Colissimo | +| **Mondial Relay** | FR + Benelux | API relais | +| **Chronopost** | FR + intl | API Chronopost | +| **SEUR** | ES + EU | API SEUR (phase 2) | + +### 7.2 Calcul des frais de port + +**V1** : grille tarifaire statique par zone + poids. + +```sql +CREATE TABLE shipping_rates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + carrier VARCHAR(30) NOT NULL, + method_name VARCHAR(100) NOT NULL, + zone_code VARCHAR(10) NOT NULL, -- 'FR', 'EU1', 'EU2', 'INTL' + weight_min_g INT NOT NULL, + weight_max_g INT NOT NULL, + price_cents INT NOT NULL, + estimated_days_min INT, + estimated_days_max INT, + is_active BOOLEAN NOT NULL DEFAULT true +); +``` + +Table `shipping_zones` définit les zones → pays. + +**V2** : appels API live pour tarifs temps réel. + +### 7.3 Étiquettes d'expédition + +**V1 manuel** : Nikola génère l'étiquette sur le site transporteur, saisit le `tracking_number` dans l'admin Veza. + +**V2 automatisé** : API d'étiquette, PDF généré auto, déposé dans MinIO. + +--- + +## 8. API Endpoints + +Base : `/api/v1/shop` + +### 8.1 Catalogue (public) + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/products` | 🔓 | Liste produits actifs | +| GET | `/products/:slug` | 🔓 | Détail produit + variants + options | +| GET | `/categories` | 🔓 | Types de produits | + +### 8.2 Panier + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/cart` | 🔒 | Mon panier | +| POST | `/cart/items` | 🔒 | Ajouter item | +| PUT | `/cart/items/:id` | 🔒 | Modifier qté | +| DELETE | `/cart/items/:id` | 🔒 | Retirer item | +| DELETE | `/cart` | 🔒 | Vider panier | +| POST | `/cart/merge` | 🔒 | Merge depuis localStorage | + +### 8.3 Checkout & commande + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/checkout/shipping-rates` | 🔒 | Calcul frais (poids, destination) | +| POST | `/checkout/validate` | 🔒 | Valider addresses + panier | +| POST | `/orders` | 🔒 | Créer order depuis cart | +| GET | `/orders` | 🔒 | Mes commandes | +| GET | `/orders/:id` | 🔒 | Détail commande | +| POST | `/orders/:id/cancel` | 🔒 | Annuler (si encore possible) | + +### 8.4 Admin + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| GET | `/admin/orders` | 🔒👑 | Toutes commandes (filtres) | +| PUT | `/admin/orders/:id/status` | 🔒👑 | Changer status | +| POST | `/admin/orders/:id/ship` | 🔒👑 | Marquer expédié (tracking_number) | +| POST | `/admin/products` | 🔒👑 | Créer produit | +| PUT | `/admin/products/:id` | 🔒👑 | Modifier | +| POST | `/admin/variants/:id/stock` | 🔒👑 | Ajuster stock | + +--- + +## 9. Events RabbitMQ + +| Event | Publisher | Consumers | +|-------|-----------|-----------| +| `order.created` | Shop | Email, Analytics | +| `order.paid` | Payment webhook | Shop (update status), Email | +| `order.cancelled` | Shop | Payment (refund), Email, Stock | +| `order.shipped` | Admin action | Email (tracking), Analytics | +| `stock.low` | Shop cron | Email admin | + +--- + +## 10. Sécurité + +- **Verrouillage stock** : `SELECT FOR UPDATE` pendant checkout +- **Idempotence** création commande : header `Idempotency-Key` côté client +- **Rate limit** : 10 orders créées / jour / user +- **Validation adresse** : service externe (Addressify self-hosted ou regex FR/EU) + +--- + +## 11. Roadmap + +### V1.0 (lancement, août 2026) +- [ ] Catalogue produits + variants +- [ ] Panier (anon + connecté) +- [ ] Checkout + calcul shipping +- [ ] Lifecycle commande 8 statuts +- [ ] Réservation stock 15min +- [ ] Admin basique + +### V1.5 (Q4 2026) +- [ ] Codes promo / remises +- [ ] Alertes stock bas +- [ ] Reviews produit +- [ ] Wishlist + +### V2.0 (2027) +- [ ] API étiquettes expédition +- [ ] Multi-devise +- [ ] Précommandes / crowdfunding +- [ ] Bundles produits + +--- + +## Voir aussi + +- [[Shop/Paiement/SPEC_PAIEMENT]] — intégration Mollie, TVA, factures +- [[Shop/Frontend/SPEC_FRONTEND_SHOP]] — UX frontend +- [[02_PRODUITS_PHYSIQUES/Microphone/FICHE_PRODUIT]] — détails produit micro +- [[09_MODELE_ECONOMIQUE/Modèle_Marge]] — calcul des prix +- [[08_CONFORMITE_JURIDIQUE/Politique_Garantie_Retour]] — politique retours diff --git a/03_APPS_&_SERVICES/Shop/Frontend/SPEC_FRONTEND_SHOP.md b/03_APPS_&_SERVICES/Shop/Frontend/SPEC_FRONTEND_SHOP.md new file mode 100644 index 0000000..04475b2 --- /dev/null +++ b/03_APPS_&_SERVICES/Shop/Frontend/SPEC_FRONTEND_SHOP.md @@ -0,0 +1,367 @@ +# Spec technique — Frontend Shop + +> Interface boutique Veza : parcours d'achat, panier, checkout, espace client. +> Stack : React 18 · TypeScript · Tailwind v4 · Zustand · TanStack Query · React Router v6 +> Design : aligné SUMI V3 (lavis japonais, cyan accent) +> Dernière mise à jour : avril 2026. + +--- + +## 1. Vue d'ensemble + +Le frontend shop est une **section intégrée** de l'app Veza (`/shop/*`), pas une app séparée. Elle partage l'auth, le design system SUMI V3, et les composants de base. + +### Principes UX + +1. **Mobile-first** — 60% du trafic probable sur mobile +2. **Parcours court** — max 3 clics entre produit et paiement +3. **Pas de dark patterns** — pas d'urgence fake, pas de stock mensonger +4. **Transparence radicale** — prix HT/TTC, délais, réparabilité affichés +5. **Cohérence visuelle** — même "lavis japonais" que le reste de Veza + +--- + +## 2. Architecture composants + +``` +apps/web/src/ +├── pages/shop/ +│ ├── ShopHome.tsx # /shop +│ ├── ProductListPage.tsx # /shop/products +│ ├── ProductDetailPage.tsx # /shop/products/:slug +│ ├── CartPage.tsx # /shop/cart +│ ├── CheckoutPage.tsx # /shop/checkout +│ ├── CheckoutSuccessPage.tsx # /shop/checkout/success +│ ├── OrderListPage.tsx # /shop/orders +│ └── OrderDetailPage.tsx # /shop/orders/:id +│ +├── components/shop/ +│ ├── ProductCard.tsx +│ ├── ProductGallery.tsx +│ ├── ProductConfigurator.tsx # variants + options +│ ├── PriceDisplay.tsx # HT/TTC/devise +│ ├── CartDrawer.tsx # panier en overlay +│ ├── CartItemRow.tsx +│ ├── CheckoutStepIndicator.tsx +│ ├── AddressForm.tsx +│ ├── ShippingMethodSelector.tsx +│ ├── PaymentMethodSelector.tsx +│ ├── OrderSummary.tsx +│ ├── OrderStatusBadge.tsx +│ └── OrderTimeline.tsx +│ +├── stores/shop/ +│ ├── cartStore.ts # Zustand + persist localStorage +│ └── checkoutStore.ts +│ +├── services/api/shop.ts # TanStack Query hooks +│ +└── hooks/shop/ + ├── useCart.ts + ├── useProducts.ts + └── useOrders.ts +``` + +--- + +## 3. Parcours d'achat (5 écrans clés) + +### 3.1 `/shop` — Accueil boutique + +**Contenu** : +- Hero statement (1 phrase forte + photo atelier) +- Grid des produits actifs (max 6) +- Section "Pourquoi Talas" (3 arguments clés, pas 10) +- Section réassurance (garanties, open-hardware, réparabilité) +- CTA vers page produit + +**Interactions** : +- Scroll fluide +- Hover sur ProductCard : preview autres vues + +### 3.2 `/shop/products/:slug` — Fiche produit + +**Zones principales** : + +``` +┌─────────────────────────────────────────────────────┐ +│ [Galerie photos × 5-8] │ Titre + sous-titre │ +│ │ Prix HT/TTC │ +│ [Player audio demo] │ │ +│ │ [Configurateur] │ +│ │ • Variant radio │ +│ │ • Options checkbox │ +│ │ │ +│ │ Prix final mis à jour │ +│ │ [Ajouter au panier] │ +│ │ │ +├──────────────────────────┴──────────────────────────┤ +│ Onglets : Description | Specs | Réparabilité | │ +│ Dans la boîte | Garantie | FAQ │ +├─────────────────────────────────────────────────────┤ +│ Avis utilisateurs │ +├─────────────────────────────────────────────────────┤ +│ Produits associés │ +└─────────────────────────────────────────────────────┘ +``` + +**Configurateur** : +- Sélection variant = obligatoire +- Options = optionnelles, cochables +- Prix recalculé instantanément +- Stock affiché honnêtement ("En stock" / "Précommande" / "Plus que N") + +### 3.3 `/shop/cart` — Panier + +- Liste des items avec vignette, titre, variant, options, prix unitaire, quantité +- Calcul sous-total +- Champ code promo (V1.5) +- Boutons : modifier qté, supprimer, vider panier +- CTA "Passer au checkout" +- Liens retour boutique + +**Drawer alternatif** : `CartDrawer` overlay latéral déclenché depuis header (ajout rapide sans quitter la page produit). + +### 3.4 `/shop/checkout` — Tunnel de commande + +**3 étapes visibles** (`CheckoutStepIndicator`) : + +``` + [1. Coordonnées] → [2. Livraison] → [3. Paiement] +``` + +**Étape 1 — Coordonnées** +- Email (pré-rempli si connecté) +- Adresse de livraison (form avec autocomplete OSM nominatim) +- Checkbox "Adresse de facturation identique" +- Adresse de facturation (si différente) +- Téléphone (optionnel) + +**Étape 2 — Livraison** +- Méthodes disponibles avec prix + délai estimé +- Radio sélection +- Note : "Colis expédié en matériaux recyclés" + +**Étape 3 — Paiement** +- Récap commande (items + totaux + adresse) +- Sélection méthode : CB (Mollie), iDEAL, SEPA, virement direct +- Checkbox CGV obligatoire +- CTA "Payer XX,XX €" +- Redirection vers Mollie + +### 3.5 `/shop/checkout/success` — Confirmation + +- ✓ Message succès +- Numéro commande +- Résumé commande +- Délai estimé de préparation +- Lien "Suivre ma commande" +- Lien télécharger facture (quand disponible) + +--- + +## 4. State management + +### 4.1 `cartStore.ts` (Zustand + persist) + +```typescript +interface CartState { + items: CartItem[] + addItem: (variant: Variant, options: string[], qty: number) => void + removeItem: (itemId: string) => void + updateQuantity: (itemId: string, qty: number) => void + clear: () => void + subtotal: () => number // computed + itemCount: () => number // computed +} +``` + +Persisté en `localStorage` → panier survit à la fermeture onglet. +Sur login, fusion avec panier serveur via `POST /api/v1/shop/cart/merge`. + +### 4.2 `checkoutStore.ts` + +Stocke les données du tunnel (adresses, shipping method, payment method) pour ne pas perdre si back/forward. +Nettoyé après commande validée ou abandon > 1h. + +--- + +## 5. Data fetching (TanStack Query) + +### 5.1 Hooks principaux + +| Hook | Query key | Endpoint | +|------|-----------|----------| +| `useProducts(filters)` | `['shop', 'products', filters]` | `GET /shop/products` | +| `useProduct(slug)` | `['shop', 'product', slug]` | `GET /shop/products/:slug` | +| `useCart()` | `['shop', 'cart']` | `GET /shop/cart` | +| `useShippingRates(address, weight)` | `['shop', 'shipping', address]` | `POST /shop/checkout/shipping-rates` | +| `useOrders()` | `['shop', 'orders']` | `GET /shop/orders` | +| `useOrder(id)` | `['shop', 'order', id]` | `GET /shop/orders/:id` | + +### 5.2 Stratégies de cache + +- **Products** : staleTime 5min, refetch on focus +- **Cart** : staleTime 0 (toujours frais) +- **Orders** : staleTime 30s, refetch on mount + +--- + +## 6. Design system — cohérence SUMI V3 + +### 6.1 Composants qui héritent + +- `Button`, `Input`, `Select`, `Checkbox`, `Radio` → réutilisés de SUMI V3 +- `Card`, `Drawer`, `Modal`, `Toast` → idem + +### 6.2 Spécificités shop + +- `PriceDisplay` : typographie tabulaire, cyan pour prix principal +- `ProductCard` : hover = léger scale + ombre encre +- `OrderTimeline` : trait d'encre qui se rempliuit selon statuts + +### 6.3 Couleurs de statut + +| Statut commande | Couleur badge | +|-----------------|---------------| +| Draft | gris #9A958D | +| Awaiting payment | ambre dilué | +| Paid | cyan #0098B5 | +| Preparing | cyan muted | +| Shipped | cyan + trait plein | +| Delivered | encre noire | +| Cancelled | gris barré | +| Refunded | ambre barré | + +--- + +## 7. Espace client + +### 7.1 `/shop/orders` — Mes commandes + +- Liste chronologique +- Filtres : statut, période +- Chaque ligne : numéro, date, total, statut (badge), lien détail + +### 7.2 `/shop/orders/:id` — Détail commande + +**Contenu** : +- Numéro commande + statut (badge grand format) +- `OrderTimeline` (visual status) +- Items commandés (vignettes + descriptions) +- Adresses (livraison + facturation) +- Totaux détaillés +- Si expédiée : tracking_number + lien transporteur +- Si facture disponible : bouton télécharger PDF +- Bouton "Annuler" si statut le permet +- Bouton "Contacter le support" + +--- + +## 8. Accessibilité + +### 8.1 Minimum WCAG AA + +- Contraste texte ≥ 4.5:1 (vérifié avec palette SUMI) +- Focus visible partout (outline cyan 2px) +- Navigation clavier complète (Tab, Enter, Escape) +- ARIA labels sur tous boutons icon-only +- Skip to content +- Formulaires avec `<label for>` et messages d'erreur explicites + +### 8.2 Lecteurs d'écran + +- `role="status"` sur notifications +- `aria-live="polite"` sur cart count, total updates +- Alt texts descriptifs sur produits + +--- + +## 9. Performance + +### 9.1 Objectifs + +| Métrique | Cible | +|----------|-------| +| LCP (Largest Contentful Paint) | < 2.5s | +| CLS (Cumulative Layout Shift) | < 0.1 | +| TTI (Time to Interactive) | < 3.5s | +| Bundle shop (gzipped) | < 80 KB | + +### 9.2 Stratégies + +- **Lazy loading** des images produits (`loading="lazy"`) +- **Route splitting** : pages shop chargées à la demande +- **Image optim** : WebP/AVIF, srcset responsive +- **Prefetch** au hover sur ProductCard + +--- + +## 10. Internationalisation (i18n) + +- Utilise i18next (existant dans Veza) +- Fichiers : `locales/fr/shop.json`, `locales/en/shop.json`, `locales/es/shop.json` +- Devise affichée selon locale (€, $, £) +- Formats date/heure locaux + +V1 : FR complet, EN partiel. +V1.5 : EN complet, ES basique. + +--- + +## 11. Tests + +### 11.1 Unit (Vitest) + +- Reducers cartStore +- Calculs totaux, TVA +- Composants isolés (PriceDisplay, ProductCard) + +### 11.2 Integration (Vitest + Testing Library) + +- Flow complet : ajout panier → checkout → validation +- Gestion erreurs API (rupture stock, paiement refusé) + +### 11.3 E2E (Playwright) + +- Parcours d'achat complet +- Connexion → commande → espace client +- Annulation commande + +### 11.4 Visual regression (Storybook + Chromatic-like) + +- Snapshots des composants shop dans le design system + +--- + +## 12. Roadmap + +### V1.0 (lancement, août 2026) +- [ ] Pages shop home, produit, panier, checkout +- [ ] Cart store + sync serveur +- [ ] Checkout 3 étapes +- [ ] Redirection Mollie +- [ ] Espace client (commandes + facture) + +### V1.5 (Q4 2026) +- [ ] CartDrawer latéral +- [ ] Codes promo +- [ ] Reviews produit (avec photos) +- [ ] Wishlist +- [ ] Améliorations a11y + +### V2.0 (2027) +- [ ] Multi-devise +- [ ] Checkout guest (sans compte) +- [ ] Paiement en plusieurs fois +- [ ] Récap PDF de commande + +--- + +## Voir aussi + +- [[Shop/Backend/SPEC_BACKEND_SHOP]] — API consommée +- [[Shop/Paiement/SPEC_PAIEMENT]] — flow paiement Mollie +- [[05_EXPERIENCE_UTILISATEUR/SUMI_V3_SPECIFICATION]] — design system +- [[05_EXPERIENCE_UTILISATEUR/DIRECTION_ARTISTIQUE_TALAS]] — direction visuelle +- [[FRONTEND_REACT]] — architecture frontend globale diff --git a/03_APPS_&_SERVICES/Shop/Paiement/SPEC_PAIEMENT.md b/03_APPS_&_SERVICES/Shop/Paiement/SPEC_PAIEMENT.md new file mode 100644 index 0000000..fd25a4f --- /dev/null +++ b/03_APPS_&_SERVICES/Shop/Paiement/SPEC_PAIEMENT.md @@ -0,0 +1,404 @@ +# Spec technique — Module Paiement + +> Stratégie de paiement Veza Shop pour micro-entreprise française, self-hosted first. +> Contrainte : pas de dépendance à un gros SaaS US, privilège aux acteurs européens. +> Stack : Go backend · webhooks HTTPS · PDF generation · PostgreSQL +> Dernière mise à jour : avril 2026. + +--- + +## 1. Contraintes + +### 1.1 Contraintes projet + +- **Structure juridique** : micro-entreprise (Nikola, solo) +- **Philosophie** : self-hosted first, pas de cloud SaaS si évitable +- **Localisation** : siège France, vente EU + international +- **Valeurs** : éthique, éviter GAFAM-adjacent + +### 1.2 Contraintes fiscales (micro-entreprise FR) + +- **Franchise en base de TVA** : < 37 500 € HT / an (plafond 2026, vérifier) +- **Si dépassé** : bascule TVA classique +- **Conservation factures** : 10 ans (DGFiP) +- **Numérotation obligatoire** : séquentielle, continue, sans trou +- **Mentions légales obligatoires** sur facture + +--- + +## 2. Choix du processor + +### 2.1 Options étudiées + +| Processor | Origine | Pour | Contre | +|-----------|---------|------|--------| +| **Mollie** | 🇳🇱 NL | API clean, fees raisonnables (1.8%+0.25€ CB EU), european | Dépendance | +| **SumUp** | 🇩🇪 DE/FR | Présence FR forte, fees 2.5% CB, pas mal de méthodes | API moins fluide que Mollie | +| **Lemonway** | 🇫🇷 FR | Agréé ACPR, escrow | Complexe, cher | +| **PayPlug** | 🇫🇷 FR | Filiale Natixis, FR | Moins d'intégrations | +| **HelloAsso** | 🇫🇷 FR | Associatif, sans frais | Pas adapté à e-commerce commercial | +| Stripe | 🇺🇸 US | Meilleur API, ultra répandu | US SaaS, contre les valeurs | +| PayPal | 🇺🇸 US | Universel | GAFAM-adjacent, fees élevés | + +### 2.2 Décision V1 : Mollie + SEPA direct + +**Primary** : **Mollie** +- API moderne, docs bien faites +- Fees compétitifs en Europe +- Couvre CB, Bancontact, iDEAL, SEPA, Apple/Google Pay +- Webhooks fiables +- Société européenne + +**Secondary** : **SEPA virement bancaire direct** +- Pour commandes > 300 € +- Gratuit, mais délai 1-3 jours +- IBAN Talas communiqué au client, vérification manuelle + +**Explicitement exclus en V1** : Stripe, PayPal (valeurs). + +### 2.3 Pourquoi pas 100% SEPA direct + +- UX catastrophique pour commandes < 200 € +- Délai de vérification tue la conversion +- Risque d'erreurs (montant, référence) + +→ Compromis : Mollie pour fluidité, SEPA pour gros montants / utilisateurs exigeants. + +--- + +## 3. Architecture + +``` +┌──────────────────┐ ┌──────────────────┐ +│ Frontend Shop │──────▶│ Backend API │ +│ (checkout page) │ │ (Go / Gin) │ +└──────────────────┘ └────────┬─────────┘ + │ + ├──▶ Mollie API + │ (creation paiement) + │ + ├──▶ PostgreSQL + │ (payments, invoices) + │ + └──▶ PDF generator + (go-pdf ou gotenberg) + + ┌──────────────────┐ + │ Mollie │ + │ webhook POST │───▶ /webhooks/mollie + └──────────────────┘ │ + │ signature verify + │ update payment status + │ send email client + │ trigger shipment prep +``` + +--- + +## 4. Modèle de données + +### 4.1 Table `payments` + +```sql +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID NOT NULL REFERENCES orders(id), + user_id UUID NOT NULL REFERENCES users(id), + + amount_cents INT NOT NULL, -- en centimes + currency VARCHAR(3) NOT NULL DEFAULT 'EUR', + + processor VARCHAR(20) NOT NULL, -- 'mollie', 'sepa_direct' + processor_payment_id VARCHAR(100), -- ID chez le processor + method VARCHAR(30), -- 'ideal', 'card', 'sepa', ... + + status VARCHAR(20) NOT NULL DEFAULT 'pending', + -- 'pending', 'authorized', 'paid', 'failed', + -- 'expired', 'refunded', 'partially_refunded' + + paid_at TIMESTAMPTZ, + failed_at TIMESTAMPTZ, + failure_reason VARCHAR(200), + + refunded_amount_cents INT NOT NULL DEFAULT 0, + + webhook_payload JSONB, -- dernier payload reçu + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_payments_order ON payments(order_id); +CREATE INDEX idx_payments_user ON payments(user_id, created_at DESC); +CREATE INDEX idx_payments_status ON payments(status) WHERE status IN ('pending', 'authorized'); +CREATE INDEX idx_payments_processor ON payments(processor, processor_payment_id); +``` + +### 4.2 Table `invoices` + +```sql +CREATE TABLE invoices ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + order_id UUID NOT NULL REFERENCES orders(id), + user_id UUID NOT NULL REFERENCES users(id), + payment_id UUID REFERENCES payments(id), + + invoice_number VARCHAR(30) NOT NULL UNIQUE, -- ex: 'TALAS-2026-00042' + invoice_type VARCHAR(20) NOT NULL, -- 'invoice', 'credit_note' + + billing_name VARCHAR(150) NOT NULL, + billing_address JSONB NOT NULL, -- address complète + billing_country VARCHAR(2) NOT NULL, + + subtotal_cents INT NOT NULL, + vat_rate NUMERIC(4,2) NOT NULL, -- 0.00 ou 20.00 + vat_cents INT NOT NULL, + total_cents INT NOT NULL, + + pdf_s3_key VARCHAR(500), -- chemin MinIO + + issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + sent_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX idx_invoices_number ON invoices(invoice_number); +CREATE INDEX idx_invoices_user ON invoices(user_id, issued_at DESC); +``` + +### 4.3 Numérotation des factures + +Format : `TALAS-YYYY-NNNNN` (ex: `TALAS-2026-00042`) + +- Séquence PostgreSQL par année : `CREATE SEQUENCE invoice_seq_2026;` +- Génération atomique en transaction +- Rotation au 1er janvier + +--- + +## 5. Flux paiement + +### 5.1 Paiement Mollie (standard) + +``` +[1] Frontend POST /api/v1/payments/create + Body: { order_id, method: 'ideal'|'card'|'sepa' } + +[2] Backend: + - Lock order (SELECT FOR UPDATE) + - Check order.status == 'awaiting_payment' + - Calcul montant TTC + - INSERT payments (status='pending') + - Appel Mollie API POST /payments + Body: { amount, description, redirectUrl, webhookUrl, metadata } + - Stocke processor_payment_id + - Response: { checkout_url } + +[3] Frontend redirige vers checkout_url Mollie + User paie sur UI Mollie + +[4] Mollie → webhook POST /webhooks/mollie + Body: { id: 'tr_...' } + +[5] Backend webhook handler: + - Vérifie signature + - GET Mollie /payments/:id + - Match avec payments.processor_payment_id + - UPDATE payments SET status=... paid_at=... + - Si 'paid' : UPDATE orders SET status='paid' + - Émet événement order.paid sur RabbitMQ + - Génère facture (async via worker) + - Envoie email de confirmation + +[6] Frontend sur redirectUrl Mollie + GET /shop/checkout/success?id=... + - Backend vérifie status payment + - Affiche succès / message d'attente +``` + +### 5.2 Paiement SEPA direct (manuel) + +``` +[1] User choisit "SEPA virement direct" + +[2] Backend: + - INSERT payments (status='pending', method='sepa_direct') + - Génère référence unique: 'TALAS-<order_id_short>' + - Envoie email avec IBAN + référence + montant + +[3] User fait le virement + +[4] Talas vérifie manuellement (interface admin) + - Matche virement entrant avec payment + - POST /admin/payments/:id/confirm-sepa + - UPDATE payments SET status='paid', paid_at=NOW() + - Reste du flow identique (facture, email, commande) +``` + +### 5.3 Remboursement + +``` +POST /admin/payments/:id/refund { amount_cents, reason } + +Backend: + - Vérifie status == 'paid' + - Appel Mollie POST /payments/:id/refunds (si Mollie) + - OU marque à rembourser manuellement (si SEPA) + - INSERT refund_request (statut pending) + - Sur webhook Mollie refund.paid : + - UPDATE payments SET refunded_amount_cents += amount + - Si total refund : status='refunded' + - Si partiel : status='partially_refunded' + - Génère facture d'avoir (credit_note) + - Email client +``` + +--- + +## 6. TVA + +### 6.1 Calcul selon statut + +**Phase 1 : franchise en base (V1, par défaut)** +- `vat_rate = 0.00` +- Mention obligatoire sur facture : _"TVA non applicable, art. 293 B du CGI"_ + +**Phase 2 : TVA assujettie (si > 37 500 € CA)** +- France / UE B2C : 20% (TVA FR) +- UE B2B (avec n° TVA valide) : 0% (autoliquidation, reverse charge) +- Hors UE : 0% (export) + +### 6.2 Vérification n° TVA intracommunautaire (B2B UE) + +Service VIES de la Commission européenne : endpoint SOAP public, self-hostable aussi. + +### 6.3 Règles OSS (One-Stop Shop) + +Si > 10 000 € de ventes B2C intra-UE hors France : +- Inscription OSS +- TVA au taux du pays destinataire +- Déclaration trimestrielle unique + +Implémentation V2 : table `vat_rates_eu` mise à jour manuellement. + +--- + +## 7. Génération de factures PDF + +### 7.1 Outil + +**Décision** : **gotenberg** (self-hosted, open source, HTML→PDF via Chromium) +- Conteneur Docker sur l'infra R720 +- Template HTML stylé Tailwind +- Génération async via RabbitMQ + +Alternative : **go-pdf** direct (plus simple, moins joli) + +### 7.2 Template facture + +Champs obligatoires (DGFiP) : +- Numéro unique + date d'émission +- Identité vendeur (Talas, SIREN, adresse) +- Identité client (nom, adresse) +- Désignation produits/services (qté, PU HT, total HT) +- Taux TVA appliqué + mention légale +- Total HT, TVA, TTC +- Date échéance / conditions de règlement +- Mention pénalités de retard +- Mention escompte + +### 7.3 Stockage + +- PDF généré → uploadé dans MinIO bucket `veza-invoices` +- Accès : URL signée (expire 24h), lien envoyé par email + disponible dans espace client +- Conservation : **10 ans minimum** (backup offsite) + +--- + +## 8. Sécurité + +### 8.1 PCI-DSS + +**Scope réduit** : Veza ne stocke **AUCUNE** donnée carte. +- Tokenization par Mollie +- Frontend ne transite jamais par Veza pour les données carte (iframe Mollie ou redirect) +- Scope SAQ-A (le plus bas) + +### 8.2 Webhooks — sécurité + +- **Vérification signature** : Mollie n'envoie pas de signature, on vérifie l'authenticité via re-call API (GET /payments/:id) +- **Idempotence** : webhook peut arriver plusieurs fois → check status avant update +- **IP allowlist** : optionnel, Mollie liste ses IPs + +### 8.3 Anti-fraude basique V1 + +- Rate limit : 10 tentatives de paiement / heure / user +- Check IP vs billing country (warning si mismatch) +- Blocage pays haut-risque (configurable) +- Verrouillage user après 3 paiements échoués / jour + +--- + +## 9. API Endpoints + +Base : `/api/v1/payments` + +| Méthode | Route | Auth | Description | +|---------|-------|------|-------------| +| POST | `/create` | 🔒 | Créer paiement pour un order | +| GET | `/:id` | 🔒 | Status paiement | +| GET | `/me` | 🔒 | Mes paiements | +| POST | `/:id/retry` | 🔒 | Relancer paiement échoué | +| GET | `/invoices/:id/download` | 🔒 | Télécharger PDF facture | +| POST | `/webhooks/mollie` | 🔓 | Webhook Mollie (public) | +| POST | `/admin/:id/confirm-sepa` | 🔒👑 | Confirmer SEPA manuel | +| POST | `/admin/:id/refund` | 🔒👑 | Rembourser | + +--- + +## 10. Monitoring & alertes + +| Alerte | Seuil | Action | +|--------|-------|--------| +| Webhook Mollie échec signature | > 3/h | Alerte Sentry | +| Taux échec paiement | > 10% | Check Mollie status | +| Paiements pending > 24h | > 5 | Review manuelle | +| Latence Mollie API | > 3s | Alerte monitoring | +| Facture non générée | > 1h après paid | Retry worker | + +--- + +## 11. Roadmap + +### V1.0 (lancement shop, août 2026) +- [ ] Intégration Mollie (CB + iDEAL + SEPA) +- [ ] SEPA direct manuel +- [ ] Génération facture PDF via gotenberg +- [ ] Remboursement complet +- [ ] Franchise en base TVA + +### V1.5 (Q4 2026) +- [ ] Remboursement partiel +- [ ] Email transactionnels avancés +- [ ] Retry automatique paiements échoués +- [ ] Dashboard admin payments + +### V2.0 (si CA > 37 500 €) +- [ ] TVA assujettie +- [ ] OSS multi-pays EU +- [ ] Vérification VIES +- [ ] Export comptable (CSV, FEC) + +### V2.5+ +- [ ] Crypto (BTC Lightning) si demandé +- [ ] Paiement en plusieurs fois (partnership) +- [ ] Abonnements (si produits récurrents) + +--- + +## Voir aussi + +- [[Shop/Backend/SPEC_BACKEND_SHOP]] — lifecycle commandes, triggers paiement +- [[Shop/Frontend/SPEC_FRONTEND_SHOP]] — UX checkout +- [[08_CONFORMITE_JURIDIQUE/CGU_CGV]] — conditions de vente +- [[09_MODELE_ECONOMIQUE/OPTIMISATION_FISCALE_ETHIQUE]] — stratégie fiscale +- [[01_PILOTAGE/CHECKLIST_IMMATRICULATION]] — micro-entreprise setup diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/00_MODE_EMPLOI.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/00_MODE_EMPLOI.md new file mode 100644 index 0000000..db4d648 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/00_MODE_EMPLOI.md @@ -0,0 +1,109 @@ +# Workbook d'identité visuelle — Talas / Veza + +> **À imprimer, à dessiner, à remplir à la main.** +> Ce workbook n'est pas un document numérique à compléter au clavier. +> Il est conçu pour être **sorti sur papier**, annoté au crayon, au feutre, avec de la gouache, des collages, du scotch et du café renversé. + +--- + +## À qui s'adresse ce workbook + +À toi, Nikola, seul face à une table à dessin. +Tu as déjà validé une direction artistique (**lavis japonais**) mais tu n'as pas encore : + +- dessiné le **symbole** du logo +- testé physiquement les **nuances** de cyan +- dessiné les **icônes** en style geste +- défini l'**identité sonore** +- synthétisé la **charte** sur une seule feuille de référence + +Ce workbook te guide sur chacun de ces points, exercice par exercice. + +--- + +## Comment l'utiliser + +### Préparation + +1. **Imprime les fiches** en A4, une par une ou d'un coup +2. Prépare un **classeur** ou une **pochette cartonnée** pour les regrouper +3. Matériel minimum : + - crayon HB + gomme + - feutre noir fin (0.3) + feutre noir large (1.0) + - un pinceau + encre de chine (ou feutre brush-pen) + - crayons de couleur ou feutres (bleu cyan, noirs, gris) + - papier calque (facultatif mais utile) + - ciseaux + colle pour les moodboards + +### Ordre recommandé + +Les fiches sont numérotées dans un ordre logique (fondations → exploration → synthèse) mais tu peux les faire dans le désordre si une te parle plus qu'une autre. + +**Parcours "minimum viable" (4 fiches, ~3h)** : +1. Fiche 01 — Manifeste et personnalité +2. Fiche 06 — Croquis du symbole +3. Fiche 08 — Exploration palette +4. Fiche 14 — Synthèse finale + +**Parcours "complet" (15 fiches, ~2 weekends)** : +Dans l'ordre de 00 à 14. + +### Règles du jeu + +- **Pas de clavier.** Si tu tapes une phrase au lieu de l'écrire, relis cette ligne. +- **Pas de pression.** Une fiche ratée = tu reprends sur une nouvelle feuille. +- **Pas de "c'est laid".** Le but est d'explorer, pas de produire du fini. +- **Dater** chaque fiche en haut à droite pour suivre ton évolution. + +--- + +## Liste complète des fiches + +### Fondations (01 — 02) +- [ ] **01** — Manifeste et personnalité +- [ ] **02** — Territoire de marque (ce qu'on est / ce qu'on n'est pas) + +### Moodboards (03 — 04) +- [ ] **03** — Moodboard ambiance générale +- [ ] **04** — Moodboard matières & geste + +### Logo (05 — 07) +- [ ] **05** — Brainstorm logo (mots, formes, associations) +- [ ] **06** — Croquis du symbole (grille de 48 tentatives) +- [ ] **07** — Synthèse et déclinaisons logo + +### Couleur (08 — 09) +- [ ] **08** — Exploration palette (nuanciers vides à remplir) +- [ ] **09** — Combinaisons et mises en situation + +### Typographie & icônes (10 — 11) +- [ ] **10** — Tests typographiques (hiérarchie, espacements) +- [ ] **11** — Croquis d'icônes (grille de 24) + +### Imagerie & son (12 — 13) +- [ ] **12** — Direction photographique +- [ ] **13** — Identité sonore + +### Synthèse (14) +- [ ] **14** — Synthèse finale sur une page + +--- + +## Après le workbook + +Une fois les fiches remplies : + +1. **Photographie** ou scanne tes meilleures pages +2. Classe-les dans `Identite_Visuelle/logo/`, `palette/`, `icones/` +3. **Numérise** le symbole final dans Inkscape +4. Mets à jour `CHARTE_GRAPHIQUE_TALAS.md` avec les décisions prises +5. Le workbook papier reste ton archive de travail — ne le jette pas + +--- + +## Voir aussi + +- [[DIRECTION_ARTISTIQUE_TALAS]] — le "pourquoi" de la direction +- [[CHARTE_GRAPHIQUE_TALAS]] — les règles actuelles +- [[GUIDE_CREATION_LOGO]] — pistes pour le symbole +- [[SUMI_V3_SPECIFICATION]] — implémentation technique web diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/01_MANIFESTE_PERSONNALITE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/01_MANIFESTE_PERSONNALITE.md new file mode 100644 index 0000000..2ef3d99 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/01_MANIFESTE_PERSONNALITE.md @@ -0,0 +1,127 @@ +# Fiche 01 — Manifeste & personnalité + +> **Temps estimé** : 45 min +> **Matériel** : stylo noir, crayon +> **Objectif** : écrire en 15 phrases qui est Talas, avec tes propres mots. + +Date : ____ / ____ / ________ + +--- + +## Exercice 1 — Les trois phrases fondatrices + +Écris à la main, sans te corriger, trois phrases qui terminent ces débuts. +Ne cherche pas l'éloquence. Cherche la vérité. + +**Talas existe parce que...** + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +**Ce que je refuse de faire dans ce projet, c'est...** + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +**Si Talas disparaissait demain, ce qui manquerait c'est...** + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Exercice 2 — Cinq adjectifs + +Écris 5 adjectifs qui décrivent Talas **en tant que personne**. +Puis entoure les 3 que tu garderais même si on te forçait à en enlever 2. + +1. _______________________ + +2. _______________________ + +3. _______________________ + +4. _______________________ + +5. _______________________ + + +--- + +## Exercice 3 — L'archétype + +Coche **un seul** archétype dominant (celui qui te gêne le moins) : + +- [ ] **L'Artisan** — précision, patience, savoir-faire, outils +- [ ] **Le Créateur** — innovation, imagination, expression, liberté +- [ ] **Le Sage** — transmission, vérité, compréhension, pédagogie +- [ ] **L'Explorateur** — découverte, indépendance, liberté, authenticité +- [ ] **Le Rebelle** — rupture, contre-culture, refus du système +- [ ] **Le Magicien** — transformation, vision, rêve réalisé + +Archétype secondaire (s'il t'en manque) : _________________________ + + +**Pourquoi ce choix ?** (3 lignes max) + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Exercice 4 — La personne incarnée + +**Si Talas était une personne que tu croisais dans un café, décris-la** : + +Âge approximatif : ______________________ + +Ce qu'elle porte : ______________________________________________ + +Ce qu'elle boit : _______________________________________________ + +Ce qu'elle lit : ________________________________________________ + +Sa voix : ______________________________________________________ + +Son défaut : ___________________________________________________ + +Ce qu'elle répète souvent : ____________________________________ + + +--- + +## Synthèse — à remplir en dernier + +**Les 3 mots du projet, classés** : + +1. _______________________ + +2. _______________________ + +3. _______________________ + + +**La phrase qui résume tout** (une seule phrase, courte) : + +___________________________________________________________________ + +___________________________________________________________________ + + +**Prochaine fiche** : → `02_TERRITOIRE_MARQUE.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/02_TERRITOIRE_MARQUE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/02_TERRITOIRE_MARQUE.md new file mode 100644 index 0000000..2d778ed --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/02_TERRITOIRE_MARQUE.md @@ -0,0 +1,124 @@ +# Fiche 02 — Territoire de marque + +> **Temps estimé** : 30 min +> **Matériel** : stylo noir, stylo rouge +> **Objectif** : cartographier ce que Talas EST et ce qu'il N'EST PAS. + +Date : ____ / ____ / ________ + +--- + +## Exercice 1 — Le plan croisé + +Place chaque concurrent/référence sur l'axe. Mets aussi **Talas** quelque part. + +``` + PREMIUM / LUXE + │ + │ + │ + │ + │ + CORPORATE ───────────────┼─────────────── ARTISANAL + │ + │ + │ + │ + │ + ACCESSIBLE / DIY +``` + +Positionne (écris les noms sur le plan) : +- Rode, Neumann, Shure, Audio-Technica +- Shelldock, BeyerDynamic, Earthworks +- Un fabricant que tu respectes +- Ton positionnement idéal = **TALAS** (entoure-le) + +--- + +## Exercice 2 — Liste noire + +**Talas ne doit JAMAIS ressembler à** : + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + + +**Pourquoi ça me hérisse ?** (écris en rouge) + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Exercice 3 — Références admirées (hors audio) + +Liste **5 marques/projets/personnes** que tu admires visuellement, **qui ne sont pas du tout du domaine audio**. + +| # | Référence | Ce que j'aime chez eux | +|---|-----------|------------------------| +| 1 | | | +| 2 | | | +| 3 | | | +| 4 | | | +| 5 | | | + +**Ce que ça dit de Talas** : + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Exercice 4 — Le test des contraires + +Coche le mot de chaque paire qui colle à Talas : + +- [ ] Chaud ↔ [ ] Froid +- [ ] Lisse ↔ [ ] Texturé +- [ ] Silencieux ↔ [ ] Expressif +- [ ] Strict ↔ [ ] Libre +- [ ] Précis ↔ [ ] Imprécis volontairement +- [ ] Nouveau ↔ [ ] Ancien +- [ ] Dense ↔ [ ] Aéré +- [ ] Rapide ↔ [ ] Lent +- [ ] Brillant ↔ [ ] Mat +- [ ] Solide ↔ [ ] Fragile +- [ ] Individuel ↔ [ ] Collectif +- [ ] Sobre ↔ [ ] Riche +- [ ] Sérieux ↔ [ ] Joueur +- [ ] Méthodique ↔ [ ] Intuitif +- [ ] Bavard ↔ [ ] Laconique + +**Le mot surprenant** (celui que tu n'attendais pas) : + +___________________________________________________________________ + + +--- + +## Synthèse — le territoire en une phrase + +**"Talas se situe à l'intersection entre _______________________ et _______________________, pour des gens qui _______________________."** + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +**Prochaine fiche** : → `03_MOODBOARD_AMBIANCE.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/03_MOODBOARD_AMBIANCE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/03_MOODBOARD_AMBIANCE.md new file mode 100644 index 0000000..c64fe34 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/03_MOODBOARD_AMBIANCE.md @@ -0,0 +1,94 @@ +# Fiche 03 — Moodboard ambiance générale + +> **Temps estimé** : 1h30 +> **Matériel** : cette feuille (A4), ciseaux, colle ou scotch, magazines / imprimés, imprimante (optionnel) +> **Objectif** : assembler 12 images qui, ensemble, donnent l'ambiance de Talas. + +Date : ____ / ____ / ________ + +--- + +## Consigne + +Colle ou dessine **12 images** dans la grille ci-dessous. +Pas de logos, pas de textes. Des **ambiances, des matières, des lumières, des gestes**. + +**Tu peux sourcer** : magazines, livres d'art, photos de ton atelier, captures d'écran imprimées, dessins à main levée, morceaux de tissu, écorces, photos de famille. + +**Tu ne dois PAS** : chercher "moodboard minimaliste japonais" sur Pinterest. Trop facile, trop générique. + +--- + +## Grille 4×3 (A4 paysage conseillé) + +``` +┌──────────────┬──────────────┬──────────────┬──────────────┐ +│ │ │ │ │ +│ 1 │ 2 │ 3 │ 4 │ +│ │ │ │ │ +│ │ │ │ │ +├──────────────┼──────────────┼──────────────┼──────────────┤ +│ │ │ │ │ +│ 5 │ 6 │ 7 │ 8 │ +│ │ │ │ │ +│ │ │ │ │ +├──────────────┼──────────────┼──────────────┼──────────────┤ +│ │ │ │ │ +│ 9 │ 10 │ 11 │ 12 │ +│ │ │ │ │ +│ │ │ │ │ +└──────────────┴──────────────┴──────────────┴──────────────┘ +``` + +*(Si la grille est trop petite imprimée, prends une feuille A3 et redessine-la.)* + +--- + +## Après collage — annotation + +Pour chaque image, écris **un seul mot** qui la résume : + +| # | Mot-clé | # | Mot-clé | +|---|---------|---|---------| +| 1 | ______________ | 7 | ______________ | +| 2 | ______________ | 8 | ______________ | +| 3 | ______________ | 9 | ______________ | +| 4 | ______________ | 10 | ______________ | +| 5 | ______________ | 11 | ______________ | +| 6 | ______________ | 12 | ______________ | + +--- + +## Le test à distance + +Accroche la feuille au mur. Recule de 3 mètres. Regarde. + +**Ce que je ressens en regardant ça de loin** : + +___________________________________________________________________ + +___________________________________________________________________ + +**L'image qui détonne** (celle qui ne colle pas) : #______ + +**Si je devais retirer 3 images** : #____, #____, #____ + +**Si je devais garder que 3 images** : #____, #____, #____ + + +--- + +## Synthèse + +**L'ambiance générale en 3 mots** : + +1. _______________________ +2. _______________________ +3. _______________________ + +**Ce qui revient le plus souvent dans mon moodboard** (matière, couleur, sujet) : + +___________________________________________________________________ + + +**Prochaine fiche** : → `04_MOODBOARD_MATIERES_GESTE.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/04_MOODBOARD_MATIERES_GESTE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/04_MOODBOARD_MATIERES_GESTE.md new file mode 100644 index 0000000..5e646ee --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/04_MOODBOARD_MATIERES_GESTE.md @@ -0,0 +1,133 @@ +# Fiche 04 — Moodboard matières & geste + +> **Temps estimé** : 1h +> **Matériel** : papier, colle, pinceau, encre de chine (ou feutre brush-pen), vrais échantillons de matière +> **Objectif** : sentir physiquement les matières et les gestes de la marque. + +Date : ____ / ____ / ________ + +--- + +## Pourquoi cette fiche + +Talas est une marque **hybride** : numérique (Veza) et physique (micros). +Pour que l'identité tienne, elle doit être **tactile** avant d'être visuelle. +Cette fiche t'oblige à penser avec les doigts, pas juste les yeux. + +--- + +## Partie 1 — Échantillonneur de matières + +Colle un **vrai morceau** de chaque matière dans la case prévue. +Ou trace une **empreinte à la mine de crayon** (frottis) si tu ne peux pas coller. + +### Matières "papier" + +``` +┌─────────────────────┬─────────────────────┐ +│ │ │ +│ Papier kraft │ Papier washi │ +│ (non blanchi) │ (ou calque) │ +│ │ │ +│ │ │ +└─────────────────────┴─────────────────────┘ + +┌─────────────────────┬─────────────────────┐ +│ │ │ +│ Papier recyclé │ Carton brut │ +│ (bureau) │ (colis) │ +│ │ │ +│ │ │ +└─────────────────────┴─────────────────────┘ +``` + +### Matières "produit" (métal, tissu, bois) + +``` +┌─────────────────────┬─────────────────────┐ +│ │ │ +│ Métal brossé │ Métal mat │ +│ (frottis mine) │ (frottis mine) │ +│ │ │ +│ │ │ +└─────────────────────┴─────────────────────┘ + +┌─────────────────────┬─────────────────────┐ +│ │ │ +│ Tissu noir │ Mousse/grille │ +│ (coton/lin) │ (bonnette) │ +│ │ │ +│ │ │ +└─────────────────────┴─────────────────────┘ +``` + +--- + +## Partie 2 — Geste d'encre + +**Trace 8 gestes différents** au pinceau (ou brush-pen) dans cette grille. +Ne cherche pas à faire joli. Cherche à **varier** : rapide/lent, plein/vide, courbe/droit. + +``` +┌───────────┬───────────┬───────────┬───────────┐ +│ │ │ │ │ +│ 1 │ 2 │ 3 │ 4 │ +│ │ │ │ │ +│ │ │ │ │ +├───────────┼───────────┼───────────┼───────────┤ +│ │ │ │ │ +│ 5 │ 6 │ 7 │ 8 │ +│ │ │ │ │ +│ │ │ │ │ +└───────────┴───────────┴───────────┴───────────┘ +``` + +**Sous chaque geste, écris sa durée** (ex : "2 sec", "1 min"), la **pression** (légère/forte), et **un verbe** qui le décrit (glisser, frapper, effleurer, écraser...). + +| # | Durée | Pression | Verbe | +|---|-------|----------|-------| +| 1 | | | | +| 2 | | | | +| 3 | | | | +| 4 | | | | +| 5 | | | | +| 6 | | | | +| 7 | | | | +| 8 | | | | + +--- + +## Partie 3 — Le geste qui est Talas + +Parmi tes 8 gestes, **entoure celui** qui ressemble le plus à l'identité de Talas. + +**Pourquoi celui-là ?** + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +--- + +## Synthèse + +**Les matières retenues pour Talas** (coche max 3) : +- [ ] Kraft non blanchi +- [ ] Washi / calque +- [ ] Papier recyclé +- [ ] Métal brossé +- [ ] Métal mat anodisé +- [ ] Tissu noir +- [ ] Mousse grille +- [ ] Autre : _______________ + +**Le geste signature** (1 phrase qui le décrit) : + +___________________________________________________________________ + +___________________________________________________________________ + + +**Prochaine fiche** : → `05_LOGO_BRAINSTORM.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/05_LOGO_BRAINSTORM.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/05_LOGO_BRAINSTORM.md new file mode 100644 index 0000000..6865d58 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/05_LOGO_BRAINSTORM.md @@ -0,0 +1,137 @@ +# Fiche 05 — Brainstorm logo + +> **Temps estimé** : 45 min +> **Matériel** : stylo, crayon, timer +> **Objectif** : vider ta tête sur le symbole avant de dessiner. + +Date : ____ / ____ / ________ + +--- + +## Règle du jeu + +**Ne dessine pas encore.** Cette fiche sert à **trier les idées**. +Les croquis viennent en fiche 06. + +--- + +## Exercice 1 — Les mots de "Talas" + +Le nom Talas évoque une **vague** (en bulgare, en russe). Écris tout ce qui te passe par la tête, sans filtrer, pendant **5 minutes** (timer obligatoire). + +**Ce que "Talas" évoque visuellement** : + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Exercice 2 — Les formes possibles + +Écris dans chaque case **une forme géométrique** qui pourrait représenter Talas. +Puis entoure les 3 qui t'attirent le plus. + +``` +┌────────────────┬────────────────┬────────────────┐ +│ │ │ │ +│ │ │ │ +│ │ │ │ +├────────────────┼────────────────┼────────────────┤ +│ │ │ │ +│ │ │ │ +│ │ │ │ +├────────────────┼────────────────┼────────────────┤ +│ │ │ │ +│ │ │ │ +│ │ │ │ +└────────────────┴────────────────┴────────────────┘ +``` + +*(Exemples pour t'aider : onde, cercle, trait, éclair, montagne, sceau, point, croissant, spirale... écris, ne dessine pas encore.)* + +--- + +## Exercice 3 — Ce que le logo doit dire + +Le symbole Talas doit évoquer **en 0.5 seconde** : + +☑ _____________________________________________________ (obligatoire) + +☑ _____________________________________________________ (obligatoire) + +☐ _____________________________________________________ (si possible) + +☐ _____________________________________________________ (si possible) + +☒ _____________________________________________________ (à éviter) + +☒ _____________________________________________________ (à éviter) + +--- + +## Exercice 4 — Les logos que j'admire + +Liste **5 logos de marques** (de n'importe quel secteur) qui te touchent visuellement. + +| # | Marque | Ce qui fonctionne | En 1 mot | +|---|--------|-------------------|----------| +| 1 | | | | +| 2 | | | | +| 3 | | | | +| 4 | | | | +| 5 | | | | + +**Le dénominateur commun entre ces 5 logos** : + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Exercice 5 — Le pire logo possible pour Talas + +**Décris en 3 phrases le logo que Talas ne doit JAMAIS devenir** : + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Synthèse + +**Les 3 mots-clés visuels du symbole** : + +1. _______________________ +2. _______________________ +3. _______________________ + +**Le symbole fonctionnera si** (1 critère que tu te donnes) : + +___________________________________________________________________ + + +**Prochaine fiche** : → `06_LOGO_CROQUIS_SYMBOLE.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/06_LOGO_CROQUIS_SYMBOLE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/06_LOGO_CROQUIS_SYMBOLE.md new file mode 100644 index 0000000..c238104 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/06_LOGO_CROQUIS_SYMBOLE.md @@ -0,0 +1,187 @@ +# Fiche 06 — Croquis du symbole (48 tentatives) + +> **Temps estimé** : 2h (en 2-3 sessions) +> **Matériel** : crayon HB, feutre noir fin, feutre noir large, pinceau + encre +> **Objectif** : 48 tentatives de symbole. Oui, 48. On ne valide rien en-dessous. + +Date début : ____ / ____ / ________ + +--- + +## Les règles + +1. **Chaque case = 1 tentative** (pas plus, pas moins) +2. **Tu n'as pas le droit de gommer** — si une case est ratée, passe à la suivante +3. **Pas deux fois la même idée** — varie épaisseur, geste, complexité +4. **Alterne les outils** : 12 au crayon, 12 au feutre fin, 12 au feutre large, 12 au pinceau +5. **Respire entre les sessions** — fais-en 16 par jour sur 3 jours + +--- + +## Grille A : Crayon HB (12 cases) + +``` +┌────────┬────────┬────────┬────────┐ +│ │ │ │ │ +│ 1 │ 2 │ 3 │ 4 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 5 │ 6 │ 7 │ 8 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 9 │ 10 │ 11 │ 12 │ +│ │ │ │ │ +│ │ │ │ │ +└────────┴────────┴────────┴────────┘ +``` + +--- + +## Grille B : Feutre noir fin (12 cases) + +``` +┌────────┬────────┬────────┬────────┐ +│ │ │ │ │ +│ 13 │ 14 │ 15 │ 16 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 17 │ 18 │ 19 │ 20 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 21 │ 22 │ 23 │ 24 │ +│ │ │ │ │ +│ │ │ │ │ +└────────┴────────┴────────┴────────┘ +``` + +--- + +## Grille C : Feutre noir large (12 cases) + +``` +┌────────┬────────┬────────┬────────┐ +│ │ │ │ │ +│ 25 │ 26 │ 27 │ 28 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 29 │ 30 │ 31 │ 32 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 33 │ 34 │ 35 │ 36 │ +│ │ │ │ │ +│ │ │ │ │ +└────────┴────────┴────────┴────────┘ +``` + +--- + +## Grille D : Pinceau + encre (12 cases) + +``` +┌────────┬────────┬────────┬────────┐ +│ │ │ │ │ +│ 37 │ 38 │ 39 │ 40 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 41 │ 42 │ 43 │ 44 │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ │ │ │ │ +│ 45 │ 46 │ 47 │ 48 │ +│ │ │ │ │ +│ │ │ │ │ +└────────┴────────┴────────┴────────┘ +``` + +--- + +## Tri + +**Session 1** — entoure tes **8 préférés** (tous outils confondus). + +Numéros : ____ , ____ , ____ , ____ , ____ , ____ , ____ , ____ + +**Session 2 (le lendemain)** — entoure tes **3 préférés** parmi les 8. + +Numéros : ____ , ____ , ____ + +**Session 3** — choisis **UN** symbole. Celui-là. + +Numéro : ____ + +--- + +## Test de réduction + +Recopie ton symbole final **à 3 tailles** : + +``` +┌──────────────────────┐ +│ │ 64mm (taille normale) +│ │ +│ │ +│ │ +│ │ +└──────────────────────┘ + +┌──────────┐ +│ │ 24mm (moyen) +│ │ +└──────────┘ + +┌──┐ +│ │ 8mm (favicon) +└──┘ +``` + +**Est-ce qu'il tient à 8mm ?** Oui / Non + +**Si non** — qu'est-ce qu'il faut simplifier ? + +___________________________________________________________________ + + +--- + +## Test de contraste + +Recopie-le **en blanc sur fond noir** (ou sur papier noir) : + +``` +████████████████████████████████ +█ █ +█ █ +█ █ +█ █ +█ █ +█ █ +█ █ +█ █ +████████████████████████████████ +``` + +**Fonctionne-t-il en inverse ?** Oui / Non + + +--- + +## Synthèse + +**Mon symbole** = tentative numéro ____ / 48 + +**Prochaine fiche** : → `07_LOGO_SYNTHESE_DECLINAISONS.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/07_LOGO_SYNTHESE_DECLINAISONS.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/07_LOGO_SYNTHESE_DECLINAISONS.md new file mode 100644 index 0000000..f9c7def --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/07_LOGO_SYNTHESE_DECLINAISONS.md @@ -0,0 +1,209 @@ +# Fiche 07 — Synthèse & déclinaisons logo + +> **Temps estimé** : 1h +> **Matériel** : crayon, feutre noir, règle, calque +> **Objectif** : verrouiller le logo final et ses 8 déclinaisons nécessaires. + +Date : ____ / ____ / ________ + +--- + +## Partie 1 — Le logo définitif + +### Symbole seul + +Dessine ton symbole **propre** (la version définitive issue de la fiche 06) : + +``` +┌────────────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### Logotype seul + +Dessine "TALAS" en MAJUSCULES, Space Grotesk Bold, letter-spacing large. +Trace d'abord des repères au crayon, puis repasse au feutre. + +``` +┌────────────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────┘ +``` + +--- + +## Partie 2 — Les 8 déclinaisons + +### Version 1 — Horizontal (symbole + logotype côte à côte) + +``` +┌────────────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### Version 2 — Vertical (symbole au-dessus, logotype dessous) + +``` +┌──────────────────────┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────┘ +``` + +### Version 3 — Symbole seul (favicon) — 32×32 et 16×16 + +``` +┌────────┐ ┌──┐ +│ │ │ │ +│ │ └──┘ +│ │ +└────────┘ +``` + +### Version 4 — Monochrome noir sur blanc + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ │ +└────────────────────────────┘ +``` + +### Version 5 — Monochrome blanc sur noir + +``` +████████████████████████████████ +█ █ +█ █ +█ █ +████████████████████████████████ +``` + +### Version 6 — Gravure laser (contours uniquement, pas de remplissage) + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ │ +└────────────────────────────┘ +``` + +### Version 7 — Broderie sur tissu (simplifié, traits épais) + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ │ +└────────────────────────────┘ +``` + +### Version 8 — Sceau (hanko) — symbole dans un carré + +``` +┌────────────────────────────┐ +│ ╔════════════════════════╗ │ +│ ║ ║ │ +│ ║ ║ │ +│ ║ ║ │ +│ ║ ║ │ +│ ╚════════════════════════╝ │ +└────────────────────────────┘ +``` + +--- + +## Partie 3 — Zone de protection + +Dessine le symbole entouré de sa zone de protection (espace vide minimal autour). + +**Règle** : la zone de protection = hauteur du symbole / X. X = ____ + +``` +┌──────────────────────────────────────────────┐ +│ ← ─── ─── ─── ─── ─── ─── ─── ─── ─── ───→ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ [ SYMBOLE ] │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ ← ─── ─── ─── ─── ─── ─── ─── ─── ─── ───→ │ +└──────────────────────────────────────────────┘ +``` + +--- + +## Partie 4 — Test d'application + +Dessine rapidement comment ton logo apparaît sur : + +### Carte de visite (85×55mm) + +``` +┌──────────────────────┐ +│ │ +│ │ +│ │ +│ │ +└──────────────────────┘ +``` + +### Bouton web + +``` +┌──────────────────────┐ +│ │ +│ │ +└──────────────────────┘ +``` + +### Sticker rond 50mm + +``` + ╭────────╮ + ╱ ╲ + │ │ + │ │ + ╲ ╱ + ╰────────╯ +``` + +--- + +## Checklist finale + +- [ ] Le symbole fonctionne en 8mm +- [ ] Le logo tient sur fond noir ET blanc +- [ ] Le symbole est reconnaissable **sans** le logotype +- [ ] Il n'y a pas de petits détails qui disparaissent en gravure laser +- [ ] Il évoque bien les mots-clés de la fiche 05 +- [ ] Je suis fier de le montrer +- [ ] Il ne ressemble à **aucun autre logo** que je connais + + +**Prochaine fiche** : → `08_PALETTE_EXPLORATION.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/08_PALETTE_EXPLORATION.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/08_PALETTE_EXPLORATION.md new file mode 100644 index 0000000..e2c7666 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/08_PALETTE_EXPLORATION.md @@ -0,0 +1,131 @@ +# Fiche 08 — Exploration palette + +> **Temps estimé** : 1h30 +> **Matériel** : gouache / aquarelle / crayons de couleur / feutres, nuancier Pantone (facultatif), papier washi ou kraft +> **Objectif** : peindre physiquement les couleurs pour les confronter au vrai monde. + +Date : ____ / ____ / ________ + +--- + +## Rappel de la direction + +La palette Talas est **monochrome + cyan**. Objectif de cette fiche : trouver **le bon cyan** et **la bonne nuance de papier**. + +--- + +## Partie 1 — Les neutres (papiers & encres) + +Peins / colorie chaque case **pleine**. Puis note ton impression à côté. + +### Papiers (fond) + +| Échantillon | Nom | Code HEX | Impression (1 mot) | +|:---:|:---|:---|:---| +| ▓▓▓▓▓▓▓▓▓▓ | Blanc pur | #FFFFFF | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Blanc crème | #F7F3EC | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Washi doux | #F2EDE6 | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Kraft clair | #E5DCC8 | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Ivoire vieilli | #EFE6D2 | ______________ | + +**Papier retenu** : ______________ + +### Encres (texte/élément) + +| Échantillon | Nom | Code HEX | Impression | +|:---:|:---|:---|:---| +| ▓▓▓▓▓▓▓▓▓▓ | Noir pur | #000000 | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Noir sumi | #1A1A1E | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Noir chaud | #201C17 | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Noir bleuté | #151820 | ______________ | + +**Encre retenue** : ______________ + +### Gris (encre diluée) + +| Échantillon | Nom | Code HEX | Impression | +|:---:|:---|:---|:---| +| ▓▓▓▓▓▓▓▓▓▓ | Gris chaud | #9A958D | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Gris froid | #8D9298 | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Gris moyen | #7D7A74 | ______________ | +| ▓▓▓▓▓▓▓▓▓▓ | Gris clair | #BFB9AF | ______________ | + +**Gris retenu** : ______________ + +--- + +## Partie 2 — Le cyan (la seule couleur) + +**C'est LA décision la plus importante de cette fiche.** + +Peins ces 8 cyans différents. Regarde-les **sur washi** ET **sur noir**. + +### Les 8 candidats + +| # | Échantillon | Nom | Code HEX | Sur papier | Sur noir | +|:---:|:---:|:---|:---|:---|:---| +| 1 | ▓▓▓▓▓▓ | Cyan classique | #00B4D8 | ______ | ______ | +| 2 | ▓▓▓▓▓▓ | Cyan profond | #0098B5 | ______ | ______ | +| 3 | ▓▓▓▓▓▓ | Teal sombre | #006B7F | ______ | ______ | +| 4 | ▓▓▓▓▓▓ | Cyan électrique | #00D4FF | ______ | ______ | +| 5 | ▓▓▓▓▓▓ | Cyan doux | #5BB6C7 | ______ | ______ | +| 6 | ▓▓▓▓▓▓ | Cyan pétrole | #1A6E82 | ______ | ______ | +| 7 | ▓▓▓▓▓▓ | Cyan poudré | #8EC5D0 | ______ | ______ | +| 8 | ▓▓▓▓▓▓ | Cyan Klein | #002FA7 | ______ | ______ | + +### Le test du pigment rare + +**Règle** : le cyan doit ressembler à **un pigment rare** posé sur un lavis monochrome. Pas à un bouton web Bootstrap. + +**Mon classement top 3** : +1. #____ — ____________________________________________________ +2. #____ — ____________________________________________________ +3. #____ — ____________________________________________________ + +**Mon cyan définitif** : #____ Code HEX : ________________ + +--- + +## Partie 3 — Test sur le produit physique + +Si tu as un micro en main (même un proto), pose un petit collage/post-it de ton cyan **contre le métal**. + +**Est-ce que le cyan "tient" contre le métal ?** Oui / Non + +**Est-ce qu'il est trop fort ? trop faible ?** ____________________ + +**Note pour la broderie de la pochette** : + +___________________________________________________________________ + + +--- + +## Partie 4 — Couleurs fonctionnelles (erreurs, succès, warnings) + +Règle : **jamais en aplat**, **jamais plus fort que le cyan**. + +| Rôle | Couleur proposée | Code | Trouvé OK ? | +|------|-----------------|------|-------------| +| Succès | Vert sauge dilué | #5A8C64 40% | _______ | +| Erreur | Rouge brique dilué | #B45046 40% | _______ | +| Warning | Ambre dilué | #BE963C 40% | _______ | +| Info | = Cyan principal | (voir ci-dessus) | _______ | + +--- + +## Synthèse — la palette Talas + +``` +┌────────────────────────────────────────────────┐ +│ │ +│ PAPIER ENCRE GRIS CYAN │ +│ │ +│ ▓▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓▓▓ │ +│ │ +│ #______ #______ #______ #______ │ +│ │ +└────────────────────────────────────────────────┘ +``` + +**Prochaine fiche** : → `09_PALETTE_COMBINAISONS.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/09_PALETTE_COMBINAISONS.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/09_PALETTE_COMBINAISONS.md new file mode 100644 index 0000000..5a2c66d --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/09_PALETTE_COMBINAISONS.md @@ -0,0 +1,153 @@ +# Fiche 09 — Combinaisons & mises en situation + +> **Temps estimé** : 1h +> **Matériel** : les couleurs validées de la fiche 08, règle, crayon +> **Objectif** : tester les combinaisons dans des contextes réels avant de les verrouiller. + +Date : ____ / ____ / ________ + +--- + +## Partie 1 — Hiérarchie visuelle + +**Question** : dans une page web, dans quel ordre dois-je voir les éléments ? + +Classe les couleurs de **1 (vu en premier)** à **4 (vu en dernier)** : + +- [ ] _____ Cyan (accent) +- [ ] _____ Encre / noir sumi (textes) +- [ ] _____ Gris chaud (textes secondaires) +- [ ] _____ Papier washi (fond) + +**Pourquoi cet ordre ?** + +___________________________________________________________________ + + +--- + +## Partie 2 — Combinaisons à tester + +Colorie chaque "mini-composition". Observe ce qui fonctionne. + +### Combinaison A — Site web (fond clair) + +``` +┌─────────────────────────────────┐ +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ fond = papier +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ +│ ░░ ████ ░░░░░░░░░░░░░░ ▓▓▓▓ ░░░ │ titre=encre CTA=cyan +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ +│ ░░ ──── ────── ── ────── ─── ░░ │ corps=gris chaud +│ ░░ ──── ────── ── ────── ─── ░░ │ +│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ +└─────────────────────────────────┘ +``` +**Impression** : ____________________________________________________ + +### Combinaison B — Site web (fond sombre) + +``` +┌─────────────────────────────────┐ +│ ███████████████████████████████ │ fond = noir encre +│ ███████████████████████████████ │ +│ ██ ░░░░ ░░░░░░░░░░░░░░ ▓▓▓▓ ██ │ titre=blanc CTA=cyan +│ ███████████████████████████████ │ +│ ██ ──── ────── ── ────── ─── ██ │ corps=gris +│ ██ ──── ────── ── ────── ─── ██ │ +│ ███████████████████████████████ │ +└─────────────────────────────────┘ +``` +**Impression** : ____________________________________________________ + +### Combinaison C — Packaging produit + +``` +┌─────────────────────────────────┐ +│ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ fond = kraft +│ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ +│ ▒▒▒▒▒▒▒▒▒▒▒ ████ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ logo = encre noire +│ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ +│ ▒▒▒▒▒▒▒▒▒▒▒ TALAS ▒▒▒▒▒▒▒▒▒▒▒▒▒ │ +│ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ │ +└─────────────────────────────────┘ +``` +**Impression** : ____________________________________________________ + +### Combinaison D — Pochette micro + +``` +┌─────────────────────────────────┐ +│ █████████████████████████████ │ tissu = noir +│ █████████████████████████████ │ +│ ████████ ▓▓▓▓ █████████████ │ broderie = cyan +│ █████████████████████████████ │ +│ █████████████████████████████ │ +└─────────────────────────────────┘ +``` +**Impression** : ____________________________________________________ + +--- + +## Partie 3 — Proportion des couleurs + +Dans une page complète, quelle proportion pour chaque couleur ? + +**Règle du "60-30-5-5"** (à valider) : + +- _____ % papier (fond) +- _____ % encre (textes) +- _____ % gris (textes secondaires) +- _____ % cyan (accent — JAMAIS plus de 5%) + +Total = 100 ✓ + +**Dessine un rectangle de 100 cases et colorie selon tes proportions** : + +``` +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +██████████████████████████████████████████████████ +``` + +--- + +## Partie 4 — Interdits + +Interdiction formelle (coche pour confirmer) : + +- [ ] Jamais deux couleurs d'accent côte à côte +- [ ] Jamais de rouge sur cyan (ou cyan sur rouge) +- [ ] Jamais de gradient cyan → violet / cyan → vert +- [ ] Jamais d'aplat cyan qui couvre + 20% d'une surface +- [ ] Jamais de texte cyan sur fond clair (contraste insuffisant) +- [ ] Jamais de bouton cyan + bordure cyan + fond cyan (overdose) + +Ajoute tes propres interdits : + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + + +--- + +## Synthèse + +**La règle d'or de la palette Talas** (écris-la comme un serment) : + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +**Prochaine fiche** : → `10_TYPOGRAPHIE_TEST.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/10_TYPOGRAPHIE_TEST.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/10_TYPOGRAPHIE_TEST.md new file mode 100644 index 0000000..27cd94e --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/10_TYPOGRAPHIE_TEST.md @@ -0,0 +1,168 @@ +# Fiche 10 — Tests typographiques + +> **Temps estimé** : 45 min +> **Matériel** : imprimante + spécimens des 3 polices (Space Grotesk, Inter, JetBrains Mono), règle, crayon +> **Objectif** : tester la hiérarchie typographique sur papier avant de la coder. + +Date : ____ / ____ / ________ + +--- + +## Préparation + +Imprime des spécimens des 3 polices dans différentes tailles et poids. +Tu peux utiliser [fonts.google.com](https://fonts.google.com) et imprimer une capture. +**Sans internet** : recopie à la main les alphabets. + +--- + +## Partie 1 — Le logotype (Space Grotesk Bold) + +Écris **TALAS** dans 5 tailles, en variant l'espacement : + +``` +TALAS (letter-spacing 0em) + +T A L A S (letter-spacing 0.05em) + +T A L A S (letter-spacing 0.10em) + +T A L A S (letter-spacing 0.15em) + +T A L A S (letter-spacing 0.20em) +``` + +**L'espacement idéal** (entoure) : 0 / 0.05 / 0.10 / 0.15 / 0.20 + +**Pourquoi celui-là ?** + +___________________________________________________________________ + + +--- + +## Partie 2 — Hiérarchie de titres + +Recopie cette hiérarchie avec les bonnes tailles. Ajuste jusqu'à ce que ça respire. + +``` +┌───────────────────────────────────────────────────┐ +│ │ +│ GRAND TITRE PAGE (H1) │ 48-64pt +│ │ +│ SOUS-TITRE DE SECTION (H2) │ 32-40pt +│ │ +│ Titre de bloc (H3) │ 24-28pt +│ │ +│ Sous-titre (H4) │ 18-20pt +│ │ +│ Corps de texte normal, lisible à l'aise sur │ 14-16pt +│ plusieurs lignes sans effort. Le texte doit │ +│ flotter dans le papier avec un interligne de │ +│ 1.6 à 1.8. │ +│ │ +│ Note secondaire, légende, petite info. │ 11-12pt +│ │ +└───────────────────────────────────────────────────┘ +``` + +**Ce qui marche** : ______________________________________________ + +**Ce qui coince** : _______________________________________________ + + +--- + +## Partie 3 — Comparaison de polices + +Écris le même texte dans les 3 polices. **Observe lequel te parle le plus.** + +Texte : _"Talas fabrique des micros qu'on répare soi-même."_ + +### En Space Grotesk (Regular) : + +___________________________________________________________________ + +___________________________________________________________________ + +### En Inter (Regular) : + +___________________________________________________________________ + +___________________________________________________________________ + +### En JetBrains Mono (Regular) : + +___________________________________________________________________ + +___________________________________________________________________ + +**La police qui donne le ton Talas** : _____________________________ + +--- + +## Partie 4 — Le "flottement" + +Règle Talas : **le texte flotte dans le papier, il n'y est pas collé**. + +Teste l'interligne (leading) : + +``` +Version serrée (1.2) — étouffée : +Lorem ipsum dolor sit amet, consectetur adipiscing. +Lorem ipsum dolor sit amet, consectetur adipiscing. +Lorem ipsum dolor sit amet, consectetur adipiscing. + +Version moyenne (1.5) — classique : +Lorem ipsum dolor sit amet, consectetur adipiscing. + +Lorem ipsum dolor sit amet, consectetur adipiscing. + +Version aérée (1.8) — Talas : +Lorem ipsum dolor sit amet, consectetur adipiscing. + + +Lorem ipsum dolor sit amet, consectetur adipiscing. +``` + +**Mon interligne préféré** : 1.2 / 1.5 / **1.8** / autre : ______ + +--- + +## Partie 5 — Le texte dans un bloc + +Dessine un bloc de texte avec **bordures** visibles et **marge intérieure** (padding) : + +``` +┌─────────────────────────────────────────────┐ +│ │ ← marge haut = ____ +│ │ +│ Texte texte texte texte texte texte │ ← marge gauche = ____ +│ texte texte texte texte texte texte │ +│ texte texte texte texte texte texte │ +│ texte texte texte texte. │ +│ │ +│ │ +└─────────────────────────────────────────────┘ +``` + +**Règle retenue** : marge = ____ × hauteur d'une ligne de texte + +--- + +## Synthèse + +**Mes règles typographiques Talas** : + +1. Titre = **Space Grotesk** Bold, letter-spacing _____, MAJUSCULES ✓ + +2. Corps = **Inter** Regular, taille _____, interligne _____ + +3. Monospace (code/specs) = **JetBrains Mono** + +4. Marge intérieure minimum = _____ + +5. Texte secondaire = gris #9A958D (voir fiche 08) + + +**Prochaine fiche** : → `11_ICONES_CROQUIS.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/11_ICONES_CROQUIS.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/11_ICONES_CROQUIS.md new file mode 100644 index 0000000..356bd58 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/11_ICONES_CROQUIS.md @@ -0,0 +1,149 @@ +# Fiche 11 — Croquis d'icônes (grille de 24) + +> **Temps estimé** : 2h (en 2 sessions) +> **Matériel** : feutre noir fin (0.3 et 0.5), crayon, règle +> **Objectif** : dessiner 24 icônes dans le style "geste de pinceau" cohérent avec le symbole. + +Date : ____ / ____ / ________ + +--- + +## Règles du style Talas + +1. **Trait unique** si possible — pas de remplissage +2. **Irrégularités volontaires** — pas lisse +3. **Épaisseur constante** par icône (varie entre icônes) +4. **Taille cible** : 24×24 px (dessine en 40×40mm sur papier) +5. **Grille invisible** : tout tient dans un carré + +--- + +## Liste des 24 icônes nécessaires + +### Navigation / UI (8) +1. Menu (hamburger) +2. Recherche (loupe) +3. Compte (avatar/personne) +4. Notifications (cloche) +5. Paramètres (engrenage ou curseurs) +6. Flèche retour +7. Flèche suivant +8. Fermer (croix) + +### Contenu / Action (8) +9. Partager +10. Aimer / cœur +11. Commenter (bulle) +12. Télécharger +13. Uploader +14. Jouer (play) +15. Pause +16. Couper / muter + +### Audio / Talas spécifique (8) +17. Micro +18. Onde sonore +19. Volume +20. Pattern polaire (cardio/omni) +21. Sample / fichier audio +22. Bibliothèque +23. Artiste / créateur +24. Communauté / groupe + +--- + +## Grille de croquis (24 cases) + +### Première passe — crayon + +``` +┌──────┬──────┬──────┬──────┐ +│ │ │ │ │ +│ 1 │ 2 │ 3 │ 4 │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ 5 │ 6 │ 7 │ 8 │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ 9 │ 10 │ 11 │ 12 │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ 13 │ 14 │ 15 │ 16 │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ 17 │ 18 │ 19 │ 20 │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ 21 │ 22 │ 23 │ 24 │ +│ │ │ │ │ +└──────┴──────┴──────┴──────┘ +``` + +### Deuxième passe — feutre noir + +*(Recopie les icônes qui marchent sur une deuxième grille, en feutre, sans crayon.)* + +``` +┌──────┬──────┬──────┬──────┐ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ │ +├──────┼──────┼──────┼──────┤ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ │ +└──────┴──────┴──────┴──────┘ +``` + +--- + +## Test de cohérence + +**Pose la grille à 2 mètres. Regarde.** + +Les 24 icônes forment-elles **une famille** ? Oui / Non + +**Les 3 icônes qui détonnent** (à refaire) : ____, ____, ____ + +**L'icône la plus réussie** : #____ + +**Pourquoi celle-là marche** : + +___________________________________________________________________ + +___________________________________________________________________ + + +--- + +## Checklist finale + +- [ ] Les 24 icônes ont la même épaisseur de trait +- [ ] Les 24 tiennent dans le même carré +- [ ] Elles ont toutes une "respiration" similaire (densité de trait) +- [ ] Elles évoquent le geste / pinceau, pas le vecteur rigide +- [ ] Elles sont toutes reconnaissables en 1 seconde + + +**Prochaine fiche** : → `12_DIRECTION_PHOTO.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/12_DIRECTION_PHOTO.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/12_DIRECTION_PHOTO.md new file mode 100644 index 0000000..691318d --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/12_DIRECTION_PHOTO.md @@ -0,0 +1,190 @@ +# Fiche 12 — Direction photographique + +> **Temps estimé** : 1h +> **Matériel** : smartphone ou appareil photo, cette feuille, stylo +> **Objectif** : définir comment on photographie les micros et l'atelier. + +Date : ____ / ____ / ________ + +--- + +## Partie 1 — Les 6 types de photos dont Talas a besoin + +Pour chaque type, colle/dessine une référence visuelle qui te plaît. + +### Type 1 — Photo produit "fiche technique" + +*Fond neutre, lumière égale, le micro en entier, précis comme un catalogue.* + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ RÉFÉRENCE │ +│ │ +│ │ +└────────────────────────────┘ +``` + +Référence : ______________________________________________________ + +### Type 2 — Photo produit "portrait" + +*Lumière dramatique, détails, le micro comme un objet précieux.* + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ RÉFÉRENCE │ +│ │ +│ │ +└────────────────────────────┘ +``` + +Référence : ______________________________________________________ + +### Type 3 — Photo d'atelier + +*Le désordre organisé, les outils, les mains, les étapes de fabrication.* + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ RÉFÉRENCE │ +│ │ +│ │ +└────────────────────────────┘ +``` + +Référence : ______________________________________________________ + +### Type 4 — Photo "usage" + +*Quelqu'un utilise le micro — studio home, enregistrement, scène.* + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ RÉFÉRENCE │ +│ │ +│ │ +└────────────────────────────┘ +``` + +Référence : ______________________________________________________ + +### Type 5 — Photo "détail/macro" + +*Très près. Une soudure, une grille, un filetage, une signature gravée.* + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ RÉFÉRENCE │ +│ │ +│ │ +└────────────────────────────┘ +``` + +Référence : ______________________________________________________ + +### Type 6 — Photo "humaine" + +*Portrait d'un artiste/musicien. Visage, regard, mains.* + +``` +┌────────────────────────────┐ +│ │ +│ │ +│ RÉFÉRENCE │ +│ │ +│ │ +└────────────────────────────┘ +``` + +Référence : ______________________________________________________ + +--- + +## Partie 2 — Règles photo Talas + +Coche ou écris : + +**Lumière** : +- [ ] Naturelle (fenêtre) +- [ ] Artificielle douce (diffuseur) +- [ ] Dure (ombres marquées) +- [ ] Mixte + +**Traitement** : +- [ ] Noir & blanc (cohérent lavis) +- [ ] Couleur désaturée +- [ ] Couleur normale +- [ ] Noir & blanc + cyan isolé (accent couleur) + +**Grain/texture** : +- [ ] Lisse numérique +- [ ] Grain argentique léger +- [ ] Grain argentique marqué +- [ ] Flou de mouvement occasionnel + +**Cadrage** : +- [ ] Centré +- [ ] Règle des tiers +- [ ] Décentré radical +- [ ] Minimaliste (beaucoup de vide) + +**Fond** : +- [ ] Toujours neutre (papier, béton, bois) +- [ ] Contextuel (atelier, studio) +- [ ] Noir profond +- [ ] Washi/texturé + +--- + +## Partie 3 — Les anti-photos + +**On ne photographie JAMAIS Talas comme** : + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + + +*(Exemples : photo studio pack blanc, flou lens-ball, drone aérien, couleurs saturées Instagram, personnes souriant caméra...)* + +--- + +## Partie 4 — Photos à prendre cette semaine + +Liste **3 photos** que tu peux aller prendre maintenant dans ton atelier : + +1. ________________________________________________________________ + +2. ________________________________________________________________ + +3. ________________________________________________________________ + + +--- + +## Synthèse — la règle photo Talas en 1 phrase + +**"Les photos Talas sont _______________________, avec _______________________, et jamais _______________________."** + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + + +**Prochaine fiche** : → `13_IDENTITE_SONORE.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/13_IDENTITE_SONORE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/13_IDENTITE_SONORE.md new file mode 100644 index 0000000..93aa1e2 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/13_IDENTITE_SONORE.md @@ -0,0 +1,165 @@ +# Fiche 13 — Identité sonore + +> **Temps estimé** : 1h30 +> **Matériel** : stylo, casque, ton micro, DAW ou Audacity, papier à musique (facultatif) +> **Objectif** : définir le son de la marque. Talas fabrique du son — sa marque doit sonner. + +Date : ____ / ____ / ________ + +--- + +## Pourquoi cette fiche existe + +Talas est une marque **audio**. Elle doit avoir une signature sonore cohérente avec le lavis visuel : brute, artisanale, unique. + +Cette fiche n'est pas optionnelle. **Si tu la saute, ta marque sera muette.** + +--- + +## Partie 1 — Les mots qui décrivent le son Talas + +Écris 5 mots qui décrivent **comment sonne** Talas (pas comment sonnent les micros — comment sonne la **marque**) : + +1. _______________________ +2. _______________________ +3. _______________________ +4. _______________________ +5. _______________________ + +--- + +## Partie 2 — Analogies sonores + +Coche ou complète : + +**Le son Talas, c'est comme entendre** : +- [ ] Une porte en bois qui se ferme +- [ ] Un verre d'eau posé sur une table en bois +- [ ] Une page tournée +- [ ] Un pinceau qui frotte sur du papier +- [ ] Un coup de marteau sur du métal +- [ ] Un souffle (inspiration/expiration) +- [ ] Une allumette qui craque +- [ ] Autre : _______________________ + +**Le son Talas N'est PAS** : +- [ ] Un synthétiseur neon +- [ ] Un jingle de pub +- [ ] Un "ding" corporate Apple +- [ ] Un violon triste +- [ ] Un gong de yoga +- [ ] Autre à éviter : _______________________ + +--- + +## Partie 3 — Les 4 sons à créer + +Talas a besoin de 4 sons signatures. + +### Son 1 — Le logo sonore (3 secondes max) + +*À jouer en intro de vidéos YouTube, podcasts, pitchs.* + +**Description textuelle** : + +___________________________________________________________________ + +___________________________________________________________________ + +**Instrumentation** : + +___________________________________________________________________ + +**Brouillon en notation libre** (dessine la forme d'onde ou note les éléments) : + +``` +┌──────────────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────┘ + t=0 t=1s t=2s t=3s +``` + +### Son 2 — Le son de notification (Veza) + +*À jouer quand l'utilisateur reçoit un sample/message.* + +**Description** : + +___________________________________________________________________ + +**Durée** : _____ ms **Caractère** : discret / marqué / chaleureux / sec + +### Son 3 — Le son de succès (action réussie) + +*Upload réussi, achat finalisé, etc.* + +**Description** : + +___________________________________________________________________ + +**Durée** : _____ ms + +### Son 4 — Le son d'erreur + +*Discret, pas aggressif. Pas un buzzer.* + +**Description** : + +___________________________________________________________________ + +**Durée** : _____ ms + +--- + +## Partie 4 — Les vraies sources sonores + +Liste les **vrais objets** que tu peux enregistrer avec ton NT1-A ou ton prototype Talas pour construire ces sons : + +### Objets disponibles à l'atelier + +1. ________________________________________________________________ +2. ________________________________________________________________ +3. ________________________________________________________________ +4. ________________________________________________________________ +5. ________________________________________________________________ +6. ________________________________________________________________ + +**Principe Talas** : on n'utilise **pas de samples génériques**. Tous les sons sont enregistrés à l'atelier avec un micro Talas. + +--- + +## Partie 5 — Style musical d'accompagnement + +Pour les vidéos / réseaux sociaux, l'ambiance musicale est : + +- [ ] Aucune (silence + voix) +- [ ] Drone / ambient très minimal +- [ ] Piano seul dépouillé +- [ ] Field recording (sons de l'atelier) +- [ ] Musique traditionnelle (préciser : _____________________) +- [ ] Autre : _______________________ + +**3 artistes/albums de référence** : + +1. _______________________________________________________________ +2. _______________________________________________________________ +3. _______________________________________________________________ + + +--- + +## Synthèse — la règle sonore Talas + +**"Le son Talas vient toujours du _______________________, jamais du _______________________."** + +**Mon prochain pas concret** : + +___________________________________________________________________ + +___________________________________________________________________ + + +**Prochaine fiche** : → `14_SYNTHESE_FINALE.md` diff --git a/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/14_SYNTHESE_FINALE.md b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/14_SYNTHESE_FINALE.md new file mode 100644 index 0000000..b8fc854 --- /dev/null +++ b/05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/WORKBOOK_IMPRIMABLE/14_SYNTHESE_FINALE.md @@ -0,0 +1,164 @@ +# Fiche 14 — Synthèse finale (fiche de référence) + +> **Temps estimé** : 1h +> **Matériel** : les 13 fiches précédentes remplies, un stylo noir, du temps +> **Objectif** : condenser tout le workbook en UNE fiche à afficher au-dessus de la table à dessin. + +Date de synthèse : ____ / ____ / ________ + +--- + +## Identité en 1 phrase + +*(Reprend la synthèse de la fiche 01)* + +___________________________________________________________________ + +___________________________________________________________________ + +___________________________________________________________________ + +--- + +## Les 3 mots + +1. **_______________________** + +2. **_______________________** + +3. **_______________________** + +--- + +## Logo + +**Symbole retenu** : version #____ (fiche 06) + +``` +┌──────────────────────┐ +│ │ +│ │ +│ [SYMBOLE] │ +│ │ +│ │ +└──────────────────────┘ +``` + +**Logotype** : TALAS en Space Grotesk Bold, letter-spacing ____ + +--- + +## Palette définitive + +``` +┌────────┬────────┬────────┬────────┐ +│ │ │ │ │ +│ PAPIER │ ENCRE │ GRIS │ CYAN │ +│ │ │ │ │ +│ │ │ │ │ +├────────┼────────┼────────┼────────┤ +│ #____ │ #____ │ #____ │ #____ │ +└────────┴────────┴────────┴────────┘ +``` + +**Proportion 60-30-5-5** : ____ / ____ / ____ / ____ + +--- + +## Typographie + +| Usage | Police | Taille | +|-------|--------|--------| +| Titres | Space Grotesk Bold, MAJUSCULES, ls ____ | ____ | +| Corps | Inter Regular, interligne ____ | ____ | +| Code | JetBrains Mono | ____ | + +--- + +## Style photo + +En 1 phrase : _____________________________________________________ + +___________________________________________________________________ + +--- + +## Style sonore + +En 1 phrase : _____________________________________________________ + +___________________________________________________________________ + +--- + +## Les 3 interdits absolus + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +☒ _______________________________________________________________ + +--- + +## Le serment + +**Signature** (écris à la main que tu t'engages à respecter cette identité) : + +___________________________________________________________________ + +___________________________________________________________________ + +Signé le ____ / ____ / ________ à _______________________ + +Signature : _______________________________________________________ + +--- + +## Affichage + +**Photocopie cette fiche. Colle-la au-dessus de ton poste de travail.** +Tu la regardes chaque fois que tu hésites sur un choix visuel. + +--- + +## Et maintenant — la suite + +Une fois le workbook rempli : + +1. [ ] **Photographier** les meilleures fiches et les classer dans : + - `05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/logo/croquis/` + - `05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/palette/echantillons/` + - `05_EXPERIENCE_UTILISATEUR/Identite_Visuelle/icones/croquis/` + +2. [ ] **Numériser le symbole** définitif dans Inkscape (SVG + PNG 64/256/1024px) + +3. [ ] **Mettre à jour** `CHARTE_GRAPHIQUE_TALAS.md` avec les codes HEX finaux + +4. [ ] **Mettre à jour** `SUMI_V3_SPECIFICATION.md` si les couleurs web changent + +5. [ ] **Enregistrer** les 4 sons de la fiche 13 à l'atelier + +6. [ ] **Créer** un kit de presse visuel (logo, palette, règles) en PDF + +7. [ ] **Afficher** cette fiche 14 au-dessus de la table à dessin + +--- + +## Le workbook est terminé + +Tu as maintenant une identité visuelle qui n'est pas **une idée** — c'est **un objet physique** fait avec tes mains, sur papier, à l'encre. + +Cette identité est **la tienne**. Personne d'autre au monde n'a ce cahier. + +Maintenant tu peux construire Veza et Talas dessus. + +--- + +## Voir aussi + +- [[IDENTITE_VISUELLE]] — le brief initial +- [[DIRECTION_ARTISTIQUE_TALAS]] — le "pourquoi" +- [[CHARTE_GRAPHIQUE_TALAS]] — les règles à mettre à jour après workbook +- [[GUIDE_CREATION_LOGO]] — pistes complémentaires pour le symbole +- [[SUMI_V3_SPECIFICATION]] — implémentation web SUMI v3 diff --git a/06_COMMUNAUTE_ECOSYSTEME/Partage_Samples_Presets/SPEC_DEPOT_SAMPLES.md b/06_COMMUNAUTE_ECOSYSTEME/Partage_Samples_Presets/SPEC_DEPOT_SAMPLES.md new file mode 100644 index 0000000..774347c --- /dev/null +++ b/06_COMMUNAUTE_ECOSYSTEME/Partage_Samples_Presets/SPEC_DEPOT_SAMPLES.md @@ -0,0 +1,307 @@ +# Spec produit — Dépôt samples & presets Veza + +> Spécification fonctionnelle du dépôt collaboratif de samples, presets, loops et one-shots. +> Générateur d'activité à faible friction pour la beta fondateurs. +> Dernière mise à jour : avril 2026. + +--- + +## 1. Pourquoi ce dépôt + +Les créateurs ont **déjà** ces fichiers sur leur disque dur. Uploader un pack = 5 minutes. Télécharger un pack = valeur immédiate. + +C'est le générateur d'activité le plus simple à amorcer sur Veza. Il alimente aussi concrètement l'écosystème : les samples d'un artiste deviennent la matière première d'un autre, créant un circuit court **entre les membres de la communauté**. + +Et contrairement à Splice ou Loopcloud, c'est **gratuit, attribué, et fait main**. + +--- + +## 2. Principes fondateurs + +### 2.1 Attribution, pas propriété + +Le partage n'est **pas un don anonyme**. Chaque pack porte le nom de son créateur. L'utilisateur qui télécharge et utilise doit (moralement) créditer. + +### 2.2 Qualité > quantité + +Mieux vaut 10 packs travaillés que 200 dumps de samples douteux. La curation éditoriale est **au centre** du dépôt. + +### 2.3 Fait main + +Le dépôt valorise les packs créés **par la personne qui les poste**. Pas de re-partage de packs commerciaux (interdit). Pas de re-upload de Splice (interdit). + +### 2.4 Pas de monétisation en V1 + +Tout est gratuit. Pas de packs payants. Pas de tips. Pas de premium. Voir §11 pour l'évolution V2. + +--- + +## 3. Types de contenu + +| Type | Format | Taille typique | +|------|--------|---------------| +| **Sample pack** | .zip contenant des WAV/FLAC | 10-500 MB | +| **One-shot** | Fichier WAV/FLAC seul | < 5 MB | +| **Loop** | WAV/FLAC avec tempo marqué | < 10 MB | +| **Preset synth** | Fichiers .fxp, .vital, .h2p, .nmsv, etc. | < 1 MB | +| **Preset effets** | .aupreset, .vstpreset, .fxp | < 1 MB | +| **Preset DAW** | Templates Ableton/Reaper/FL/Studio One | < 50 MB | +| **IR (Impulse Response)** | WAV | < 10 MB | +| **Field recording** | WAV long format | 10-500 MB | +| **Drum kit** | .zip structuré | 10-200 MB | +| **Sound bank** | Structure custom | Variable | + +--- + +## 4. Anatomie d'un pack + +### 4.1 Métadonnées obligatoires + +| Champ | Description | +|-------|-------------| +| **Titre** | Nom du pack (80 car. max) | +| **Description** | Ce qu'il contient, comment il a été fait (1000 car.) | +| **Créateur** | Pseudo Veza (auto) | +| **Type** | Une des 10 catégories ci-dessus | +| **Licence** | CC-BY, CC-BY-SA, CC0, ou "usage non-commercial Veza uniquement" | +| **Tags** | 3 à 10 tags (genre, BPM, instrument, tonalité, ambiance) | +| **Preview audio** | MP3 court (< 30s) généré auto ou fourni par l'artiste | + +### 4.2 Métadonnées optionnelles + +- **BPM** / **Tonalité** (si applicable) +- **Genre** (hip-hop, techno, ambient, cinematic, etc.) +- **Outils utilisés** (Serum, Vital, Pigments, micro maison, etc.) +- **Inspiration / contexte** (texte libre — "enregistré dans ma cuisine un dimanche") +- **Liens vers des morceaux de démo** utilisant le pack +- **Remix permis ?** (oui/non) + +### 4.3 Exemple de fiche pack + +``` +┌────────────────────────────────────────────────────────────┐ +│ MORNING KITCHEN DRUMS │ +│ par @senke │ +│ │ +│ Type : Sample pack (drum kit) │ +│ Licence : CC-BY-SA 4.0 │ +│ Taille : 48 MB · 60 samples │ +│ Tags : lofi, home-made, one-shot, drums, organic │ +│ BPM : multi · Tonalité : - │ +│ │ +│ Description : │ +│ Enregistré dimanche matin avec deux cuillères, un bol en │ +│ inox, la machine à café, et le grille-pain. Traité au │ +│ minimum : un peu de comp et de saturation. Toutes les │ +│ sources sont étiquetées dans les filenames. │ +│ │ +│ ▶ Preview (28 sec) │ +│ │ +│ [ Télécharger ] [ Aimer ] [ Remixer ] [ Signaler ] │ +│ │ +│ 42 likes · 178 téléchargements · 3 remix │ +└────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. Licences + +### 5.1 Les 4 licences proposées (V1) + +| Licence | Droits donnés | Contraintes | +|---------|--------------|-------------| +| **CC0** | Usage libre total, même commercial | Aucune (attribution facultative) | +| **CC-BY 4.0** | Usage libre, même commercial | Doit créditer l'auteur | +| **CC-BY-SA 4.0** | Usage libre, même commercial | Doit créditer + toute œuvre dérivée doit être partagée sous la même licence | +| **Usage Veza uniquement** | Libre pour les œuvres partagées sur Veza | Pas d'usage commercial hors Veza | + +**Licence par défaut** : CC-BY-SA 4.0 (celle qui favorise le plus la réciprocité). + +### 5.2 Pas de licence propriétaire "payante" + +En V1, pas de licence de type "100€ pour usage commercial". Si un artiste veut monétiser ses samples, il doit le faire **hors Veza** (Splice, Gumroad, Beatstars). Veza reste un espace de partage gratuit. + +### 5.3 Attribution — comment créditer + +Un pack sous CC-BY demande ce format d'attribution dans les crédits du morceau ou les métadonnées : + +``` +Samples: "Morning Kitchen Drums" by @senke +via Veza (veza.talas.co), CC-BY-SA 4.0 +``` + +--- + +## 6. Flux utilisateur + +### 6.1 Uploader un pack (le créateur) + +1. Clic sur "Partager un pack" depuis son profil ou le menu +2. Upload fichier (drag&drop, jusqu'à 500 MB) +3. Choix du type +4. Remplissage des métadonnées (titre, description, tags, licence) +5. Génération auto de la preview audio (ou upload manuel) +6. Preview de la fiche pack +7. Publication + +**Durée attendue** : 5-10 minutes pour un pack standard. + +### 6.2 Parcourir le dépôt (le consommateur) + +1. Accès via l'onglet "Samples" dans le menu principal +2. Feed par défaut : **récents** + **recommandés** +3. Filtres : type, licence, genre, BPM, tonalité, créateur +4. Tri : récent, populaire, plus téléchargé, nouveauté de la semaine +5. Écoute des previews sans télécharger +6. Téléchargement (zip direct ou fichier individuel) + +### 6.3 Remixer un pack + +Un bouton "Remixer" permet de créer un **pack dérivé** qui : +- Cite l'original dans sa fiche (backlink auto) +- Apparaît dans l'arbre "remix" de l'original +- Est soumis aux contraintes de licence de l'original + +Cette fonctionnalité crée **l'arbre généalogique** d'un son — une des identités fortes de Veza. + +--- + +## 7. Curation et qualité + +### 7.1 Curation éditoriale (humaine) + +Toi (fondateur) ou des modérateurs désignés font une **sélection hebdomadaire** : + +- **"Pack de la semaine"** (1 pack mis en avant, home + newsletter) +- **"Découvertes"** (3-5 packs sélectionnés, moins vus mais qualitatifs) +- **"Best of genre"** (par mois, top 3 par genre) + +### 7.2 Signalements + +Un bouton "Signaler" permet de remonter : +- Contenu volé (re-upload non-attribué) +- Violation de licence +- Contenu inapproprié +- Qualité décevante (mal étiqueté, pas ce qu'annonce la description) + +### 7.3 Pas d'algo de recommandation IA en V1 + +La recommandation est **humaine** (curation) + **mécanique simple** (récents, populaires, tags similaires). Pas de moteur ML. + +--- + +## 8. Règles de la maison + +### 8.1 Interdit + +1. **Samples volés** — pas de re-upload de Splice, Loopcloud, Spitfire, KSHMR, etc. +2. **Samples sous droits** — pas de découpe d'un morceau commercial ("sample de drake") +3. **Dumps sans curation** — pas de "500 samples random" sans description +4. **Multi-compte** pour booster les likes +5. **Packs trompeurs** — titre et contenu doivent correspondre + +### 8.2 Toléré mais encadré + +- **Field recordings de lieux publics** : OK sauf conversations privées identifiables +- **Samples de synthés hardware** : OK, mentionner le synthé +- **Packs inspirés d'un artiste** : OK tant que ce n'est pas un re-sampling direct +- **Re-upload de ses propres vieux packs** d'autres plateformes : OK + +### 8.3 Sanctions + +| Infraction | Sanction V1 | +|-----------|-------------| +| Pack clairement volé | Suppression immédiate + avertissement | +| Récidive | Suppression des 3 derniers packs + restriction upload 30 jours | +| 3e récidive | Exclusion | +| Multi-compte | Fusion des comptes ou exclusion | + +--- + +## 9. Interface utilisateur — écrans + +### 9.1 Écran "Dépôt samples" (flux général) + +- Feed vertical type Instagram +- Chaque carte : titre, créateur, type, tags, preview audio inline, likes, téléchargements +- Filtres en sidebar (desktop) ou modale (mobile) +- Bouton flottant "Partager un pack" + +### 9.2 Écran "Fiche pack" + +- Metadata complet +- Player audio intégré (preview) +- Liste des fichiers (nom + durée + format) +- Bouton téléchargement +- Arbre des remix (si applicable) +- Section commentaires + +### 9.3 Écran "Mes packs" + +- Mes uploads (éditables, analytiques : vues, téléchargements, likes) +- Mes téléchargements (historique) +- Mes likes + +### 9.4 Section "Packs" sur un profil artiste + +- Liste chronologique des uploads de l'artiste +- Statistiques cumulées (nb packs, téléchargements totaux, likes totaux) +- Filtre par type + +--- + +## 10. Métriques de succès + +| Métrique | Objectif phase beta (juin) | Objectif fin 2026 | +|----------|---------------------------|-------------------| +| Packs publiés | 30+ | 300+ | +| Packs téléchargés (cumul) | 100+ | 3 000+ | +| Ratio upload / membre | 60% des fondateurs | 30% des membres | +| Téléchargements / pack médian | 5+ | 10+ | +| Packs "remixés" | 3+ | 50+ | +| Packs signalés | 0-2 | < 1% des packs | + +--- + +## 11. Évolutions possibles (V2+) + +### Ce qu'on peut ajouter si ça marche + +- **Tips libres** : donner 1-5 EUR à un créateur (Liberapay ou crypto) +- **Packs "éphémères"** disponibles 7 jours uniquement +- **Collaborations de packs** (3 artistes font un pack ensemble) +- **Metadata enrichies** : tempo detection auto, key detection auto, analyse spectrale +- **Preview spectrogramme** visuel +- **Compatibilité DAW** (import direct dans Ableton/FL via plugin Veza) +- **"Challenges pack"** : tout le monde fait un pack sur le même thème + +### Ce qu'on ne fera pas (même en V2) + +- Packs premium payants +- Abonnement type Splice +- Royalties algorithmiques +- Fingerprinting anti-piratage +- Embed commercial (Veza reste non-commercial pour ce module) + +--- + +## 12. Interactions avec les autres modules + +| Module | Interaction | +|--------|-------------| +| **Troc** | Un artiste peut proposer "un pack custom" contre un service | +| **Challenges hebdo** | Les résultats de certains challenges peuvent devenir des packs | +| **Profil artiste** | Packs visibles dans l'onglet "Samples" du profil | +| **Morceaux** | Un morceau peut créditer les packs Veza utilisés | +| **Chat/Groupes** | Un pack peut être partagé dans un groupe | + +--- + +## Voir aussi + +- [[STRATEGIE_LANCEMENT_COMMUNAUTAIRE]] — les Talas Starter Packs à préparer +- [[CHALLENGES_HEBDO_FONDATEURS]] — Challenge #1 génère du sample matière +- [[03_APPS_&_SERVICES/Community/Partage/SPEC_TECHNIQUE_PARTAGE]] — implémentation backend +- [[REGLES_MODERATION]] — règles générales Veza +- [[Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE]] — autre module communautaire diff --git a/06_COMMUNAUTE_ECOSYSTEME/Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE.md b/06_COMMUNAUTE_ECOSYSTEME/Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE.md new file mode 100644 index 0000000..e2b544a --- /dev/null +++ b/06_COMMUNAUTE_ECOSYSTEME/Plateforme_Echange/SPEC_TROC_COMMUNAUTAIRE.md @@ -0,0 +1,276 @@ +# Spec produit — Troc communautaire Veza + +> Spécification fonctionnelle du système de troc entre artistes sur Veza. +> Un différenciant unique de la plateforme : échanger des services et du matériel sans argent. +> Dernière mise à jour : avril 2026. + +--- + +## 1. Pourquoi le troc + +Sur les autres plateformes (SoundCloud, Discord, Reddit), les artistes s'écoutent mais n'échangent rien. Ils sont en position de **consommation mutuelle**, pas de **collaboration**. + +Le troc change ça : chaque artiste a quelque chose à offrir (un skill, un sample pack, une heure de mix, un beat) et quelque chose à demander. Le troc force l'interaction humaine, crée des relations durables et donne une **raison concrète** d'être sur Veza. + +C'est aussi un pied de nez à la logique marchande : sur Veza, on peut avancer sans argent. + +--- + +## 2. Principe général + +### 2.1 Un tableau d'annonces, pas un marketplace + +Veza n'est pas Vinted. Le troc n'est pas un e-commerce déguisé. C'est un **tableau d'annonces** où chaque artiste publie : + +- ce qu'il **offre** (son skill, son temps, un objet) +- ce qu'il **cherche** en échange + +Les deux parties discutent, tombent d'accord, et se débrouillent. Veza **n'arbitre pas l'échange**. Veza fournit : +- la visibilité de l'annonce +- un système de messagerie privée +- un système de réputation léger + +### 2.2 Pas de crédits (décision actuelle) + +**Décision** : pas de système de crédits communautaires dans la V1. + +Pourquoi : un système de crédits introduit de la complexité (comptabilité interne, émission, inflation, fraude). Il peut attendre la V2 si la communauté le demande. + +La V1 = troc direct 1-pour-1 ou N-pour-1 (un artiste peut avoir plusieurs trocs en parallèle). + +### 2.3 Catégories d'annonces + +| Catégorie | Exemples d'offres | Exemples de demandes | +|-----------|-------------------|----------------------| +| **Services audio** | Mix, mastering, enregistrement, prise de son | Idem | +| **Création visuelle** | Cover art, logo, photo, vidéo | Idem | +| **Production musicale** | Beat, topline, arrangement, collab | Idem | +| **Formation / coaching** | 1h de coaching DAW, review de mix | Idem | +| **Matériel** | Micro inutilisé, carte son, câbles | Idem | +| **Samples / presets** | Pack custom fait main | Idem (mais → plutôt dans le dépôt samples) | +| **Autre** | Tout ce qui ne rentre pas | | + +--- + +## 3. Anatomie d'une annonce + +### 3.1 Champs obligatoires + +| Champ | Description | Contrainte | +|-------|-------------|-----------| +| **Titre** | Résume l'échange en une phrase | 80 caractères max | +| **J'offre** | Ce que propose l'artiste | 500 caractères | +| **Je cherche** | Ce qu'il veut en échange | 500 caractères | +| **Catégorie** | Une des 7 catégories ci-dessus | Liste fermée | +| **Délai de réalisation** | En combien de temps l'artiste peut livrer | 1 jour à 1 mois | +| **Statut** | `ouvert`, `en discussion`, `conclu`, `annulé` | | + +### 3.2 Champs optionnels + +- **Exemples de son travail** (liens vers ses uploads Veza, ou fichiers joints) +- **Contraintes** ("pas de death metal", "uniquement en FR", etc.) +- **Localisation** (si besoin de rencontre IRL) + +### 3.3 Exemple concret + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Titre : Beat hip-hop contre mix d'un EP 4 titres │ +│ Catégorie : Production musicale ↔ Services audio │ +│ │ +│ J'offre : Un beat original (90-100 BPM, sombre, mélodique), │ +│ exclusif, avec stems. Je produis depuis 3 ans, quelques │ +│ exemples sur mon profil Veza. │ +│ │ +│ Je cherche : Le mix complet d'un EP de 4 titres rap (pas │ +│ mastering). Je fournis les stems propres. Révisions │ +│ incluses raisonnables. │ +│ │ +│ Délai de ma part : 1 semaine │ +│ Statut : ouvert · 3 personnes intéressées │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Flux de troc — du post au closing + +### 4.1 Étape 1 : publication de l'annonce + +1. L'artiste clique sur "Nouvelle annonce" dans la section Troc +2. Remplit le formulaire (obligatoires + optionnels) +3. Preview avant publication +4. L'annonce apparaît dans le flux général ET dans le groupe correspondant à sa catégorie + +### 4.2 Étape 2 : intérêt d'un autre artiste + +1. Un autre artiste voit l'annonce +2. Il clique sur "Je suis intéressé" → un DM s'ouvre avec l'annonceur +3. Le message inclut automatiquement un rappel du contexte de l'annonce +4. Les deux discutent en privé + +### 4.3 Étape 3 : discussion et accord + +- Les deux se mettent d'accord via DM +- Ils définissent leurs attentes précises +- Ils se donnent une deadline (souple) +- L'annonceur passe l'annonce en statut `en discussion` + +### 4.4 Étape 4 : réalisation + +- Chacun fait son travail de son côté +- Échanges de fichiers via DM Veza (attachments) +- Révisions si besoin + +### 4.5 Étape 5 : closing + +- Une fois les deux parties satisfaites, l'annonceur passe le statut en `conclu` +- **Système de reconnaissance** : chaque partie peut laisser une réaction simple à l'autre (voir §5 Réputation) +- L'annonce reste visible en archive avec badge "conclu" + +### 4.6 Étape d'annulation (n'importe quand) + +- L'une des deux peut annuler sans justification +- L'annonce repasse à `ouvert` ou à `annulé` +- Pas de pénalité automatique (la répétition éveille la modération — voir §5) + +--- + +## 5. Système de réputation + +### 5.1 Philosophie + +**Pas de notation 5 étoiles.** On n'est pas Uber. + +On utilise un système de **reconnaissance mutuelle simple** : après un troc conclu, chaque partie choisit une ou plusieurs qualités à attribuer à l'autre : + +### 5.2 Les 6 reconnaissances + +| Badge | Signification | +|-------|---------------| +| 🤝 **Fiable** | A livré ce qui était promis | +| ⏱️ **Ponctuel** | A respecté les délais | +| 💡 **Inventif** | A apporté plus que prévu | +| 🧘 **Patient** | Bonne communication, gestion des révisions | +| 🎯 **Précis** | Travail technique de qualité | +| 🤗 **Chaleureux** | Échange humain agréable | + +Chaque artiste accumule ces reconnaissances sur son profil (visible publiquement). + +### 5.3 Pas de notes négatives publiques + +Il n'y a pas de "mauvaise note" publique. À la place : + +- Après un échange raté, chacun peut signaler **en privé à la modération** (toi) +- La modération observe les patterns (répétition = problème) +- En cas de problème répété avéré, l'artiste est contacté puis éventuellement exclu + +### 5.4 Premier troc d'un nouveau membre + +Les nouveaux arrivants ont un **badge "nouveau"** (visible 30 jours) pour que les autres soient un peu plus indulgents/curieux. Pas un badge "débutant" — c'est un **badge d'accueil**. + +--- + +## 6. Règles de la maison (modération) + +### 6.1 Interdit + +1. **Argent** — Aucun troc n'implique d'argent. Pas de "je fais ton mix contre 100€". Pour ça, il y a une freelance. +2. **Contenu sous copyright** — Pas de samples volés, pas de beats volés, pas de plug-ins crackés. +3. **Services inappropriés** — Pas de troc à caractère sexuel, raciste, violent. +4. **Multi-compte** — Un seul compte par personne. Pas de faux comptes pour boost ses reconnaissances. +5. **Démarchage massif** — Si tu postes 10 annonces identiques, elles sont toutes supprimées. + +### 6.2 Toléré mais modéré + +- **Annonces "je cherche un feedback"** — OK, mais incite plutôt à utiliser les groupes thématiques +- **Annonces IRL** (rencontre physique) — OK, à la charge des deux parties d'évaluer le risque +- **Trocs mixtes** (part service + part matériel) — OK tant que c'est clair + +### 6.3 Sanctions + +| Infraction | Sanction V1 | +|-----------|-------------| +| Spam d'annonces | Suppression + avertissement privé | +| Contenu volé | Suppression + avertissement | +| Répétition (2e fois) | Restriction de poster des annonces 30 jours | +| Répétition (3e fois) | Exclusion de la plateforme | +| Comportement toxique en DM | Exclusion immédiate après vérification | + +--- + +## 7. Interface utilisateur — écrans + +### 7.1 Écran "Troc" (flux général) + +- Feed des annonces ouvertes +- Filtres : catégorie, délai, statut +- Tri : récent / populaire (nb de "Je suis intéressé") +- Bouton flottant "Nouvelle annonce" + +### 7.2 Écran "Nouvelle annonce" + +- Formulaire guidé (étape par étape pour aider les nouveaux) +- Preview avant publication +- Astuces contextuelles ("bon titre = verbe d'action") + +### 7.3 Écran "Mes annonces" + +- Mes annonces actives (éditables) +- Mes annonces conclues (archive, avec reconnaissances reçues) +- Mes annonces annulées + +### 7.4 Onglet "Troc" sur un profil artiste + +- Badge "a fait X trocs" (nombre total conclus) +- Badges de reconnaissance accumulés +- Annonces actives +- Historique (public, simple) + +--- + +## 8. Métriques de succès + +| Métrique | Objectif fin 2026 | Seuil d'alerte | +|----------|-------------------|----------------| +| Annonces publiées (cumul) | 200+ | < 50 | +| Annonces conclues (ratio) | > 40% des annonces | < 20% | +| Temps moyen de closing | < 3 semaines | > 6 semaines | +| Taux de réponse aux DMs | > 60% | < 30% | +| Signalements modération / mois | < 3 | > 10 | +| Trocs mentionnés spontanément sur RS | Quelques / mois | 0 | + +--- + +## 9. Ce qu'on ne fera PAS en V1 + +- Système de crédits / monnaie interne +- Escrow ou séquestre de fichiers +- Arbitrage automatique par la plateforme +- Notation 1-5 étoiles +- Algo de matching automatique (mise en relation IA) +- Annonces sponsorisées / mises en avant payantes +- Marketplace de vraies ventes (argent réel) + +Ces features sont des pièges qui transformeraient le troc en mini-Vinted. On reste simple. + +--- + +## 10. Évolutions possibles (V2+) + +- **Crédits communautaires** : si la demande émerge naturellement +- **Trocs de groupe** (3+ personnes en cercle) +- **Trocs "ouverts"** (je propose X, qui fait quoi en échange ?) +- **Templates d'annonce** pour les échanges récurrents +- **Histoire visible** d'un troc (timeline des étapes) +- **Intégration calendrier** pour les trocs ponctuels (sessions de coaching) + +--- + +## Voir aussi + +- [[STRATEGIE_LANCEMENT_COMMUNAUTAIRE]] — contexte stratégique, phases de lancement +- [[REGLES_MODERATION]] — règles générales de la maison Veza +- [[03_APPS_&_SERVICES/Community/Troc/SPEC_TECHNIQUE_TROC]] — implémentation backend +- [[05_EXPERIENCE_UTILISATEUR/CONCEPTS_INNOVANTS_VEZA]] — autres features uniques de Veza +- [[06_COMMUNAUTE_ECOSYSTEME/GUIDE_ONBOARDING_FONDATEUR]] — comment présenter le troc aux fondateurs diff --git a/06_COMMUNAUTE_ECOSYSTEME/Événements_Participatifs/SPEC_EVENEMENTS.md b/06_COMMUNAUTE_ECOSYSTEME/Événements_Participatifs/SPEC_EVENEMENTS.md new file mode 100644 index 0000000..65906df --- /dev/null +++ b/06_COMMUNAUTE_ECOSYSTEME/Événements_Participatifs/SPEC_EVENEMENTS.md @@ -0,0 +1,304 @@ +# Spec produit — Événements participatifs Veza + +> Spécification des formats événementiels créés par Talas et/ou portés par la communauté. +> Les événements sont le rythme cardiaque de la communauté. +> Dernière mise à jour : avril 2026. + +--- + +## 1. Pourquoi des événements + +Une plateforme sans rythme est un musée. Les événements créent : + +- **Un rythme** — les membres reviennent parce qu'il se passe quelque chose +- **Des pics d'activité** — le contenu se concentre, la visibilité est max +- **Des moments mémorables** — un event raté ou réussi devient une histoire qu'on raconte +- **Un ancrage temporel** — "j'étais là au premier TalasLive" + +Sans événements, Veza serait un feed infini comme les autres. Avec, c'est un lieu qui **vit par saisons**. + +--- + +## 2. Typologie des événements + +### 2.1 Les 5 formats + +| Format | Fréquence | Durée | Qui organise | +|--------|-----------|-------|--------------| +| **Challenge hebdo** | 1×/semaine | 7 jours | Talas (équipe fondatrice) | +| **TalasLive** | 1×/mois | 1-2h | Talas, en direct | +| **Jam en ligne** | 1×/2 mois | 2-4h | Talas ou membre volontaire | +| **Concours thématique** | 2-3×/an | 2-4 semaines | Talas, parfois avec partenaire | +| **IRL local** | Ponctuel | Variable | Membre (Talas soutient) | + +--- + +## 3. Challenge hebdomadaire + +*(Voir [[CHALLENGES_HEBDO_FONDATEURS]] pour le détail des 6 premiers challenges.)* + +### 3.1 Format standard + +- **Annoncé** : lundi matin dans le groupe #partage +- **Deadline** : dimanche soir +- **Règles** : contrainte créative claire (outils, durée, thème) +- **Soumissions** : upload dans #partage avec tag du challenge +- **Récompense** : zéro matérielle, partage RS du meilleur + +### 3.2 Rythme et variété + +Les challenges alternent les formats pour ne pas lasser : + +| Type de challenge | Exemples | +|-------------------|----------| +| **Contrainte d'outils** | "Fais un beat sans VST" · "Utilise uniquement des samples de chez toi" | +| **Contrainte de temps** | "30 min chrono" · "Un beat par jour pendant 7 jours" | +| **Contrainte de matière** | "Utilise le Starter Pack #3" · "Remix du morceau de X" | +| **Contrainte de format** | "10 secondes max" · "Boucle infinie" · "Field recording uniquement" | +| **Contrainte collaborative** | "Continue le morceau de quelqu'un" · "Duo imposé" | + +### 3.3 Sélection du meilleur + +**Règle** : pas de vote communautaire public. C'est subjectif, ça crée des déceptions. + +À la place : l'équipe Talas **choisit 3-5 soumissions marquantes** à mettre en avant sur les RS, sans classement. Message type : + +> "Voici les 5 soumissions qui m'ont marqué cette semaine — dans le désordre." + +--- + +## 4. TalasLive (mensuel) + +### 4.1 Concept + +Un **live streaming mensuel** où Talas (toi) : + +- Montre l'avancée du projet (hardware + logiciel) +- Répond aux questions de la communauté en direct +- Invite parfois un artiste fondateur à présenter son travail +- Partage une astuce technique ou une anecdote atelier + +### 4.2 Format type + +- **Durée** : 60 à 90 minutes +- **Plateforme** : streaming Veza direct (WebRTC) OU YouTube en fallback +- **Interaction** : chat en temps réel via Veza +- **Annonce** : 1 semaine avant dans #fondateurs et sur les RS + +### 4.3 Structure d'un live (exemple) + +``` +00:00 — Intro, news du mois +10:00 — Ce qui a été fait sur le micro (photos, schémas) +25:00 — Ce qui a été fait sur Veza (screenshots, démo live) +40:00 — Invité surprise (artiste fondateur présente son setup) +55:00 — Questions du chat +85:00 — Prochaines étapes, teasing du mois suivant +90:00 — Fin +``` + +### 4.4 Archive + +Chaque live est **enregistré** et disponible : +- Sur Veza dans une section "Archives TalasLive" +- Sur le YouTube Talas (avec chapitrage) + +--- + +## 5. Jam en ligne + +### 5.1 Concept + +Un **créneau synchrone** où les membres travaillent ensemble sur un même thème, en partageant leur progression en temps réel. + +### 5.2 Format type + +- **Durée** : 2 à 4 heures +- **Thème** : annoncé 48h avant +- **Participation** : inscription préalable (limite 30 participants en V1) +- **Plateforme** : chat Veza + salon vocal (intégré ou Jitsi/Mumble self-hosted) +- **Livrable** : chaque participant upload son résultat dans un thread dédié + +### 5.3 Rythme + +``` +00:00 — Intro, présentation du thème, règles +00:15 — Début de la jam, silence ou bruit ambiant optionnel +01:00 — Premier point check-in (chacun partage son progrès en 30 sec) +02:00 — Deuxième point check-in +02:30 — Deadline soft, finalisation +02:45 — Partage des résultats dans le thread +03:00 — Écoute collective commentée +``` + +### 5.4 Thèmes possibles + +- "Fais une boucle de 8 mesures avec 3 pistes max" +- "Transforme ce field recording en beat" +- "Compose une intro de podcast en 20 sec" +- "Sound design : le son d'un robot qui pleure" + +--- + +## 6. Concours thématique + +### 6.1 Concept + +Événement plus long et plus ambitieux qu'un challenge hebdo. **2-3 fois par an max** pour préserver la valeur. + +### 6.2 Format type + +- **Durée** : 2 à 4 semaines +- **Annonce** : 2 semaines avant +- **Thème** : large mais précis ("Le son de ta ville", "Hommage à une chanson oubliée") +- **Format attendu** : morceau audio (1-5 min) +- **Jury** : 3 à 5 personnes (Talas + 2-4 artistes invités) +- **Récompense** : visibilité (feature sur RS, passage dans le TalasLive suivant) + **bon matos** à gagner (1 micro prototype Talas en dotation, ou équivalent) + +### 6.3 Critères de jugement (transparents) + +| Critère | Poids | +|---------|-------| +| Originalité du parti pris | 30% | +| Qualité de réalisation | 25% | +| Cohérence avec le thème | 20% | +| Émotion / impact | 25% | + +### 6.4 Règles anti-compétition toxique + +- Pas de classement 1-2-3, juste un "top 3 à ex-æquo" +- Mention encourageante pour 5-10 soumissions au-delà du top 3 +- Feedback public constructif sur toutes les soumissions (ou au minimum sur 50%) +- **Aucun perdant** — tout le monde a eu le courage de soumettre + +--- + +## 7. IRL local (Talas Live physique) + +### 7.1 Concept + +Un **membre de la communauté** organise un événement physique dans sa ville : +- Écoute de morceaux communautaires +- Mini-jam improvisée +- Démo d'un setup ou d'une technique +- Rencontre entre créateurs + +**Talas ne les organise pas** — Talas les soutient. + +### 7.2 Kit organisateur (fourni par Talas) + +- Un **brief d'organisation** (how-to, checklist) +- Des **visuels** à imprimer (affiche, stickers, badges) +- Une **annonce relai** sur les canaux Veza et RS Talas +- Éventuellement un **micro prototype prêté** pour la session + +### 7.3 Conditions pour soutenir un IRL + +- Gratuit ou participation modique (max 5 EUR) +- Pas de sponsors commerciaux (sauf validés par Talas) +- Respect de la charte Talas (inclusivité, bienveillance) +- Retour de l'organisateur post-event (photos, feedback) + +--- + +## 8. Calendrier-type d'une année + +``` +JAN │ Challenge hebdo ×4 │ TalasLive │ Jam en ligne (début d'année) +FEV │ Challenge hebdo ×4 │ TalasLive +MAR │ Challenge hebdo ×4 │ TalasLive │ Concours thématique (semaine 2-4) +AVR │ Challenge hebdo ×4 │ TalasLive │ Jam en ligne +MAI │ Challenge hebdo ×4 │ TalasLive +JUN │ Challenge hebdo ×4 │ TalasLive │ Concours "solstice d'été" +JUL │ Challenge hebdo ×4 │ TalasLive │ Jam en ligne +AOU │ Challenge hebdo ×2 │ (pause été partielle) +SEP │ Challenge hebdo ×4 │ TalasLive │ Jam en ligne +OCT │ Challenge hebdo ×4 │ TalasLive │ Concours "Halloween / sound design" +NOV │ Challenge hebdo ×4 │ TalasLive │ Jam en ligne +DEC │ Challenge hebdo ×2 │ TalasLive │ "Best of de l'année" (non-compétitif) +``` + +**Total année** : ~46 challenges hebdo, 11 TalasLive, 6 jams, 3 concours, IRL variable. + +--- + +## 9. Documentation des événements + +### 9.1 Chaque événement a sa page Veza + +- Description + règles +- Liste des participants +- Soumissions (pour challenges/concours) +- Archive (replays, photos, résultats) +- Page indexée dans la section "Événements" + +### 9.2 Archives permanentes + +Les événements passés restent **accessibles pour toujours**. C'est le patrimoine de la communauté. + +### 9.3 Bilan après chaque événement + +Court message de bilan dans #fondateurs : +- Nombre de participants / soumissions +- Ce qui a bien marché +- Ce qui a moins bien marché +- Une anecdote marquante + +--- + +## 10. Interface utilisateur — écrans + +### 10.1 Écran "Événements" + +- **À venir** : prochains événements (dates, inscriptions) +- **En cours** : événements actifs avec CTA (participer, soumettre) +- **Archive** : événements passés avec résumé + replays + +### 10.2 Écran "Fiche événement" + +- Titre, type, dates, règles +- Liste/grille des soumissions (pour challenges/concours) +- Bouton "Participer" / "Soumettre" +- Fil de discussion autour de l'événement + +### 10.3 Widget "Événement du moment" sur la home + +Sur la page d'accueil Veza, un widget met en avant : +- Le challenge de la semaine en cours +- Le prochain TalasLive (si < 7 jours) +- Un concours si actif + +--- + +## 11. Métriques de succès + +| Métrique | Objectif phase beta | Objectif fin 2026 | +|----------|---------------------|-------------------| +| Taux de participation challenge hebdo | 20% des actifs | 15% des actifs | +| Inscrits TalasLive live | 20+ / session | 50+ / session | +| Replays TalasLive vus | 50+ / mois | 200+ / mois | +| Jam : participants | 10-20 | 25-40 | +| Concours : soumissions | 15+ | 50+ | +| IRL organisés par membres | 0 | 3-5 dans l'année | + +--- + +## 12. Risques et garde-fous + +| Risque | Garde-fou | +|--------|-----------| +| Participation décroissante aux challenges | Varier les formats (§3.2) | +| Jams peu peuplées | Limiter à 1×/2 mois, bien préparer | +| Concours crée des jalousies | Pas de classement serré (§6.4) | +| TalasLive usant pour le fondateur | Raccourcir si besoin, pré-enregistrer certaines parties | +| IRL : risques juridiques/sécurité | Charte claire, pas de sponsoring Talas en V1 | + +--- + +## Voir aussi + +- [[CHALLENGES_HEBDO_FONDATEURS]] — les 6 premiers challenges détaillés +- [[STRATEGIE_LANCEMENT_COMMUNAUTAIRE]] — rôle des événements dans le lancement +- [[07_CONTENUS_MARKETING/STRATEGIE_CONTENU]] — comment relayer les événements sur RS +- [[REGLES_MODERATION]] — règles comportementales pendant les événements +- [[Artistes_&_Ambassadeurs/README]] — rôle des ambassadeurs dans les événements diff --git a/talas-wiki/static/style.css b/talas-wiki/static/style.css index be3e9cd..e6d639e 100644 --- a/talas-wiki/static/style.css +++ b/talas-wiki/static/style.css @@ -1,54 +1,93 @@ /* ============================================================= - TALAS WIKI — Terminal Aesthetic - Palette Nord: derived from one.html + TALAS WIKI — Sumi-e Aesthetic + Based on Talas Graphic Charter ============================================================= */ :root { - --bg: #121212; - --bg-alt: #0e0e0e; - --border: #2e3440; - --text: #a3be8c; - --text-dim: #6b7f5a; - --heading: #eceff4; - --yellow: #ebcb8b; - --cyan: #88c0d0; - --red: #bf616a; - --purple: #b48ead; - --orange: #d08770; - --font: 'JetBrains Mono', 'Consolas', 'Fira Code', monospace; + /* Day Theme (Washi Paper & Ink) */ + --bg: #F2EDE6; + --bg-alt: #EAE6DF; + --border: #D1CFC9; + --border-hover: #A09E98; + --text: #1A1A1E; + --text-dim: #8A857D; + --heading: #111111; + --yellow: #BE9F3C; + --cyan: #006B7F; + --red: #B45046; + --purple: #8B6B85; + --orange: #B87352; + --font-heading: 'Space Grotesk', system-ui, sans-serif; + --font-body: 'Inter', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', 'Consolas', 'Fira Code', monospace; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --shadow: 0 4px 12px rgba(26, 26, 30, 0.05); +} + +@media (prefers-color-scheme: dark) { + :root { + /* Night Theme (Dark Wash) */ + --bg: #0D0D0F; + --bg-alt: #161618; + --border: #3A352D; + --border-hover: #5A5348; + --text: #E8E3DB; + --text-dim: #9A958D; + --heading: #F2EDE6; + --yellow: #D1B85D; + --cyan: #0098B5; + --red: #D66D60; + --purple: #AB8CA5; + --orange: #D19070; + --shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } } *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } -::selection { background: rgba(163, 190, 140, 0.25); color: var(--heading); } +::selection { background: rgba(0, 107, 127, 0.15); color: var(--heading); } +@media (prefers-color-scheme: dark) { + ::selection { background: rgba(0, 152, 181, 0.25); } +} html { background: var(--bg); } body { - font-family: var(--font); - font-size: 14px; + font-family: var(--font-body); + font-size: 15px; line-height: 1.7; color: var(--text); overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } a { color: var(--cyan); text-decoration: none; - border-bottom: 1px dashed rgba(136, 192, 208, 0.3); - transition: all 0.15s; + border-bottom: 1px dashed rgba(0, 107, 127, 0.3); + transition: all 0.2s ease; } +@media (prefers-color-scheme: dark) { + a { border-bottom: 1px dashed rgba(0, 152, 181, 0.3); } +} + a:hover { - background: var(--text); - color: var(--bg); - border-bottom-color: transparent; + background: rgba(0, 107, 127, 0.05); + color: var(--cyan); + border-bottom-color: var(--cyan); +} +@media (prefers-color-scheme: dark) { + a:hover { background: rgba(0, 152, 181, 0.1); } } /* ═══ LAYOUT ═══ */ .layout { display: grid; - grid-template-columns: 260px 1fr; + grid-template-columns: 280px 1fr; min-height: 100vh; } @@ -57,125 +96,143 @@ a:hover { .sidebar { background: var(--bg-alt); border-right: 1px solid var(--border); - padding: 20px 16px; + padding: 24px 20px; overflow-y: auto; position: sticky; top: 0; height: 100vh; + display: flex; + flex-direction: column; } .sidebar-header { - margin-bottom: 20px; - padding-bottom: 16px; + margin-bottom: 24px; + padding-bottom: 20px; border-bottom: 1px solid var(--border); } .logo { - font-size: 16px; - font-weight: bold; - color: var(--text); + font-family: var(--font-heading); + font-size: 20px; + font-weight: 700; + color: var(--heading); border: none; + letter-spacing: -0.02em; + display: inline-flex; + align-items: baseline; } -.logo:hover { background: none; color: var(--heading); } -.logo-dot { color: var(--yellow); } +.logo:hover { background: none; color: var(--cyan); } +.logo-dot { color: var(--cyan); font-weight: bold; margin-left: 2px;} .sidebar-search { - margin-bottom: 20px; + margin-bottom: 24px; } .search-input { width: 100%; background: var(--bg); border: 1px solid var(--border); + border-radius: var(--radius-sm); color: var(--text); - font-family: var(--font); - font-size: 12px; - padding: 8px 10px; + font-family: var(--font-body); + font-size: 14px; + padding: 10px 14px; outline: none; - transition: border-color 0.2s; + transition: border-color 0.2s, box-shadow 0.2s; } .search-input:focus { border-color: var(--cyan); + box-shadow: 0 0 0 2px rgba(0, 107, 127, 0.1); } .search-input::placeholder { color: var(--text-dim); } .sidebar-nav { display: flex; flex-direction: column; - gap: 2px; + gap: 4px; } .nav-domain { - display: block; - padding: 6px 10px; - font-size: 12px; - color: var(--yellow); + display: flex; + align-items: center; + padding: 8px 12px; + font-size: 13px; + font-weight: 600; + color: var(--text-dim); border: none; - border-radius: 3px; - transition: all 0.15s; + border-radius: var(--radius-sm); + transition: all 0.2s ease; } .nav-domain:hover { - background: rgba(235, 203, 139, 0.1); - color: var(--yellow); + background: var(--bg); + color: var(--heading); + transform: translateX(2px); } /* ═══ MAIN CONTENT ═══ */ .main { - padding: 32px 48px; - max-width: 960px; - border-left: 2px solid var(--text); + padding: 48px 64px; + max-width: 1080px; + margin: 0 auto; + width: 100%; } -/* ═══ BREADCRUMB ��══ */ +/* ═══ BREADCRUMB ═══ */ .breadcrumb { color: var(--text-dim); - font-size: 12px; - margin-bottom: 24px; + font-size: 13px; + margin-bottom: 32px; + font-family: var(--font-mono); } -.breadcrumb::before { content: ">_ "; color: var(--yellow); } -.breadcrumb a { color: var(--cyan); font-size: 12px; border: none; } -.breadcrumb a:hover { background: var(--cyan); color: var(--bg); } +.breadcrumb::before { content: ">_ "; color: var(--cyan); } +.breadcrumb a { color: var(--text-dim); font-size: 13px; border: none; } +.breadcrumb a:hover { background: none; color: var(--cyan); border-bottom: 1px solid var(--cyan); } .edit-marker { color: var(--orange); } -/* ═���═ HEADINGS ═��═ */ +/* ═══ HEADINGS ═══ */ -h1, h2, h3, h4, h5, h6 { color: var(--heading); font-weight: 600; } +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + color: var(--heading); + font-weight: 700; + line-height: 1.3; + letter-spacing: -0.01em; +} -h1 { font-size: 24px; margin-bottom: 16px; } -h1::before { content: "# "; color: var(--yellow); font-weight: 400; } +h1 { font-size: 36px; margin-bottom: 16px; } +h1::before { content: ""; display: inline-block; width: 6px; height: 32px; background: var(--cyan); margin-right: 12px; vertical-align: middle; border-radius: 3px; } -h2 { font-size: 20px; margin: 32px 0 12px; } -h2::before { content: "## "; color: var(--yellow); font-weight: 400; } - -h3 { font-size: 16px; margin: 24px 0 8px; } -h3::before { content: "### "; color: var(--yellow); font-weight: 400; } - -h4 { font-size: 14px; margin: 20px 0 8px; color: var(--text); } -h5, h6 { font-size: 13px; margin: 16px 0 8px; color: var(--text-dim); } +h2 { font-size: 24px; margin: 48px 0 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border); } +h3 { font-size: 18px; margin: 32px 0 12px; } +h4 { font-size: 16px; margin: 24px 0 8px; color: var(--text); font-family: var(--font-body); font-weight: 600; } +h5, h6 { font-size: 14px; margin: 20px 0 8px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; } /* ═══ PAGE HEADER ═══ */ .page-header { display: flex; align-items: center; - gap: 16px; - margin-bottom: 8px; + gap: 20px; + margin-bottom: 12px; } .page-header h1 { margin-bottom: 0; flex: 1; } +.page-header h1::before { display: none; } /* removed before mark on main title if we use another style, wait let's keep it */ .page-meta { display: flex; + flex-wrap: wrap; gap: 16px; - font-size: 11px; + font-size: 13px; color: var(--text-dim); - margin-bottom: 24px; - padding-bottom: 16px; + margin-bottom: 40px; + padding-bottom: 24px; border-bottom: 1px solid var(--border); + font-family: var(--font-mono); } -.meta-domain { color: var(--yellow); } +.meta-domain { color: var(--cyan); font-weight: 600; } /* ═══ PAGE CONTENT ═══ */ @@ -183,420 +240,531 @@ h5, h6 { font-size: 13px; margin: 16px 0 8px; color: var(--text-dim); } line-height: 1.8; } -.page-content p { margin-bottom: 12px; } +.page-content p { margin-bottom: 16px; } .page-content ul, .page-content ol { - margin: 8px 0 16px 24px; + margin: 12px 0 20px 24px; } -.page-content li { margin-bottom: 4px; } -.page-content li::marker { color: var(--yellow); } +.page-content li { margin-bottom: 6px; } +.page-content li::marker { color: var(--cyan); } .page-content blockquote { - border-left: 3px solid var(--yellow); - padding: 8px 16px; - margin: 16px 0; + border-left: 4px solid var(--cyan); + padding: 16px 24px; + margin: 24px 0; color: var(--text-dim); - background: rgba(235, 203, 139, 0.03); + background: var(--bg-alt); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + font-style: italic; } .page-content pre { background: var(--bg-alt); border: 1px solid var(--border); - padding: 16px; + border-radius: var(--radius-md); + padding: 20px; overflow-x: auto; - margin: 16px 0; - font-size: 13px; - line-height: 1.5; + margin: 24px 0; + font-size: 14px; + line-height: 1.6; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); } .page-content code { - color: var(--cyan); - font-family: var(--font); + color: var(--orange); + font-family: var(--font-mono); font-size: 13px; + background: var(--bg-alt); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid var(--border); } -.page-content pre code { color: var(--text); } +.page-content pre code { color: var(--text); background: transparent; padding: 0; border: none; font-size: 14px; } .page-content img { max-width: 100%; border: 1px solid var(--border); - margin: 16px 0; + border-radius: var(--radius-md); + margin: 24px 0; + box-shadow: var(--shadow); } .page-content table { - border-collapse: collapse; + border-collapse: separate; + border-spacing: 0; width: 100%; - margin: 16px 0; - font-size: 13px; + margin: 24px 0; + font-size: 14px; + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; } .page-content th { background: var(--bg-alt); - color: var(--cyan); + color: var(--heading); text-align: left; - padding: 10px 12px; - border: 1px solid var(--border); + padding: 12px 16px; font-weight: 600; + border-bottom: 1px solid var(--border); } .page-content td { - padding: 8px 12px; - border: 1px solid var(--border); + padding: 12px 16px; + border-bottom: 1px solid var(--border); } +.page-content tr:last-child td { border-bottom: none; } .page-content tr:hover td { - background: rgba(163, 190, 140, 0.04); + background: rgba(0, 107, 127, 0.03); +} +@media (prefers-color-scheme: dark) { + .page-content tr:hover td { background: rgba(0, 152, 181, 0.08); } } .page-content hr { border: none; border-top: 1px solid var(--border); - margin: 24px 0; + margin: 40px 0; } .page-content input[type="checkbox"] { - accent-color: var(--text); - margin-right: 6px; + accent-color: var(--cyan); + margin-right: 8px; + transform: scale(1.1); } -.page-content strong { color: var(--heading); } -.page-content em { color: var(--purple); font-style: italic; } +.page-content strong { color: var(--heading); font-weight: 600; } +.page-content em { color: var(--text); font-style: italic; } /* ═══ WIKILINKS ═══ */ .wikilink { color: var(--cyan); - border-bottom: 1px solid rgba(136, 192, 208, 0.4); + border-bottom: 1px solid rgba(0, 107, 127, 0.3); + font-weight: 500; } .wikilink:hover { - background: var(--cyan); - color: var(--bg); + background: rgba(0, 107, 127, 0.08); + color: var(--cyan); + border-bottom-color: var(--cyan); } +@media (prefers-color-scheme: dark) { + .wikilink { border-bottom: 1px solid rgba(0, 152, 181, 0.4); } + .wikilink:hover { background: rgba(0, 152, 181, 0.15); border-bottom-color: var(--cyan); } +} + .wikilink-broken { color: var(--red); border-bottom: 1px dashed var(--red); cursor: help; } -.asset-link { color: var(--orange); border-bottom-color: rgba(208, 135, 112, 0.4); } -.asset-link:hover { background: var(--orange); } +.asset-link { color: var(--orange); border-bottom-color: rgba(184, 115, 82, 0.4); } +.asset-link:hover { background: rgba(184, 115, 82, 0.08); } -/* ═══ BACKLINKS ═���═ */ +/* ═══ BACKLINKS ═══ */ .backlinks { - margin-top: 48px; - padding-top: 24px; + margin-top: 64px; + padding-top: 32px; border-top: 1px solid var(--border); } -.backlinks h3 { font-size: 13px; color: var(--text-dim); margin-bottom: 8px; } -.backlinks h3::before { content: "← "; color: var(--yellow); } -.backlinks ul { list-style: none; margin: 0; padding: 0; } -.backlinks li { margin-bottom: 4px; font-size: 13px; } -.backlink-domain { color: var(--text-dim); font-size: 11px; margin-left: 8px; } +.backlinks h3 { font-size: 14px; color: var(--text-dim); margin-bottom: 16px; font-family: var(--font-body); font-weight: 600; } +.backlinks h3::before { content: "← "; color: var(--cyan); } +.backlinks ul { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; } +.backlinks li { margin: 0; font-size: 13px; } +.backlinks a { + display: inline-block; + padding: 6px 12px; + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + border-bottom: 1px solid var(--border); +} +.backlinks a:hover { background: var(--bg); border-color: var(--cyan); color: var(--cyan); transform: translateY(-1px); box-shadow: var(--shadow); } +.backlink-domain { color: var(--text-dim); font-size: 11px; margin-left: 8px; font-family: var(--font-mono); } /* ═══ DIRECTORY LISTING ═══ */ .dir-listing { - font-size: 13px; + font-size: 14px; + margin: 24px 0; + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: hidden; } .dir-header { display: grid; - grid-template-columns: 24px 1fr 130px 80px; - gap: 8px; - padding: 8px 0; + grid-template-columns: 32px 1fr 140px 100px; + gap: 16px; + padding: 12px 16px; + background: var(--bg-alt); border-bottom: 1px solid var(--border); color: var(--text-dim); - font-size: 11px; + font-size: 12px; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; } .dir-entry { display: grid; - grid-template-columns: 24px 1fr 130px 80px; - gap: 8px; - padding: 6px 0; - border-bottom: 1px solid rgba(46, 52, 64, 0.4); + grid-template-columns: 32px 1fr 140px 100px; + gap: 16px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + align-items: center; + transition: background 0.2s; +} +.dir-entry:last-child { border-bottom: none; } +.dir-entry:hover { background: rgba(0, 107, 127, 0.02); } +@media (prefers-color-scheme: dark) { + .dir-entry:hover { background: rgba(0, 152, 181, 0.06); } } -.dir-entry:hover { background: rgba(163, 190, 140, 0.04); } -.dir-type-d { color: var(--yellow); } +.dir-type-d { color: var(--cyan); } .dir-type-f { color: var(--text-dim); } .dir-type-a { color: var(--orange); } -.dir-folder { color: var(--yellow); border: none; font-weight: 600; } -.dir-folder:hover { background: var(--yellow); color: var(--bg); } -.dir-file { border: none; } +.dir-folder { color: var(--heading); border: none; font-weight: 600; } +.dir-folder:hover { color: var(--cyan); border: none; background: none; } +.dir-file { border: none; color: var(--text); } .dir-asset { color: var(--orange); border: none; } -.dir-col-date { color: var(--text-dim); font-size: 12px; } -.dir-col-size { color: var(--text-dim); font-size: 12px; text-align: right; } +.dir-col-date { color: var(--text-dim); font-size: 13px; font-family: var(--font-mono); } +.dir-col-size { color: var(--text-dim); font-size: 13px; text-align: right; font-family: var(--font-mono); } .dir-readme { - margin-bottom: 24px; - padding: 16px; - background: rgba(163, 190, 140, 0.03); + margin-bottom: 32px; + padding: 32px; + background: var(--bg); border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow); } -.dir-readme h1::before, .dir-readme h2::before, .dir-readme h3::before { content: none; } +.dir-readme h1::before { display: inline-block; } .divider { border: none; border-top: 1px solid var(--border); - margin: 24px 0; + margin: 32px 0; } /* ═══ HOME PAGE ═══ */ .ascii-art { - color: var(--red); + color: var(--text-dim); white-space: pre; - font-size: 11px; - margin-bottom: 16px; - line-height: 1.3; + font-family: var(--font-mono); + font-size: 12px; + margin-bottom: 24px; + line-height: 1.4; + opacity: 0.7; } .subtitle { color: var(--text-dim); - margin-bottom: 32px; - font-size: 13px; + margin-bottom: 48px; + font-size: 16px; + font-weight: 400; } .domain-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 12px; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; } .domain-card { display: flex; flex-direction: column; - padding: 16px; + padding: 24px; background: var(--bg-alt); border: 1px solid var(--border); + border-radius: var(--radius-md); color: var(--text); - border-bottom: none; - transition: all 0.15s; + border-bottom: 1px solid var(--border); + transition: all 0.2s ease; } .domain-card:hover { - border-color: var(--text); - background: rgba(163, 190, 140, 0.05); - color: var(--text); + border-color: var(--cyan); + background: var(--bg); + box-shadow: var(--shadow); + transform: translateY(-2px); + border-bottom: 1px solid var(--cyan); } .domain-num { - font-size: 11px; - color: var(--text-dim); - margin-bottom: 4px; -} -.domain-name { - font-size: 15px; - font-weight: 600; - color: var(--yellow); - margin-bottom: 4px; -} -.domain-count { - font-size: 11px; - color: var(--text-dim); - margin-bottom: 8px; -} -.domain-desc { + font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); - line-height: 1.5; + margin-bottom: 8px; + font-weight: 600; +} +.domain-name { + font-family: var(--font-heading); + font-size: 18px; + font-weight: 700; + color: var(--heading); + margin-bottom: 8px; +} +.domain-count { + font-size: 12px; + color: var(--text-dim); + margin-bottom: 12px; + display: inline-block; + padding: 2px 8px; + background: var(--border); + border-radius: 10px; +} +.domain-desc { + font-size: 14px; + color: var(--text-dim); + line-height: 1.6; } /* ═══ SEARCH ═══ */ .search-form { display: flex; - gap: 8px; - margin-bottom: 24px; + gap: 12px; + margin-bottom: 32px; } .search-input-main { flex: 1; - background: var(--bg-alt); + background: var(--bg); border: 1px solid var(--border); + border-radius: var(--radius-md); color: var(--text); - font-family: var(--font); - font-size: 14px; - padding: 10px 14px; + font-family: var(--font-body); + font-size: 16px; + padding: 12px 16px; outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} +.search-input-main:focus { + border-color: var(--cyan); + box-shadow: 0 0 0 3px rgba(0, 107, 127, 0.1); } -.search-input-main:focus { border-color: var(--cyan); } .search-input-main::placeholder { color: var(--text-dim); } .btn-search, .btn-save { background: var(--text); color: var(--bg); border: none; - padding: 10px 20px; - font-family: var(--font); - font-size: 13px; + border-radius: var(--radius-md); + padding: 12px 24px; + font-family: var(--font-body); + font-size: 14px; cursor: pointer; font-weight: 600; - transition: all 0.15s; + transition: all 0.2s ease; } .btn-search:hover, .btn-save:hover { - background: var(--heading); + background: var(--cyan); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 107, 127, 0.2); } .search-count { color: var(--text-dim); - font-size: 12px; - margin-bottom: 16px; + font-size: 14px; + margin-bottom: 24px; } .search-results { display: flex; flex-direction: column; - gap: 16px; + gap: 20px; } .search-result { - padding: 16px; + padding: 24px; border: 1px solid var(--border); + border-radius: var(--radius-md); background: var(--bg-alt); + transition: all 0.2s ease; +} +.search-result:hover { + border-color: var(--cyan); + background: var(--bg); + box-shadow: var(--shadow); } -.search-result:hover { border-color: var(--text); } .result-title { - font-size: 15px; - font-weight: 600; - margin-right: 8px; + font-family: var(--font-heading); + font-size: 18px; + font-weight: 700; + color: var(--cyan); + margin-right: 12px; } .result-domain { - font-size: 11px; - color: var(--yellow); -} -.result-score { - font-size: 11px; - color: var(--text-dim); - margin-left: 8px; -} -.result-snippet { - margin-top: 8px; + font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); + padding: 2px 8px; + background: var(--border); + border-radius: 4px; +} +.result-score { + font-size: 12px; + color: var(--text-dim); + margin-left: 12px; +} +.result-snippet { + margin-top: 12px; + font-size: 14px; + color: var(--text); line-height: 1.6; } .result-path { display: block; - margin-top: 4px; - font-size: 11px; + margin-top: 8px; + font-size: 12px; color: var(--text-dim); + font-family: var(--font-mono); } /* ═══ EDIT ═══ */ -.edit-form { margin-top: 16px; } +.edit-form { margin-top: 24px; } .edit-textarea { width: 100%; - min-height: 70vh; - background: var(--bg-alt); + min-height: 65vh; + background: var(--bg); border: 1px solid var(--border); + border-radius: var(--radius-md); color: var(--text); - font-family: var(--font); - font-size: 13px; + font-family: var(--font-mono); + font-size: 14px; line-height: 1.7; - padding: 16px; + padding: 20px; outline: none; resize: vertical; tab-size: 4; + transition: border-color 0.2s, box-shadow 0.2s; +} +.edit-textarea:focus { + border-color: var(--cyan); + box-shadow: 0 0 0 3px rgba(0, 107, 127, 0.1); } -.edit-textarea:focus { border-color: var(--cyan); } .edit-actions { display: flex; - gap: 12px; - margin-top: 12px; + gap: 16px; + margin-top: 20px; align-items: center; } .btn-edit { - font-size: 12px; + font-size: 13px; color: var(--text-dim); + background: var(--bg-alt); border: 1px solid var(--border); - padding: 4px 12px; - border-bottom: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; } .btn-edit:hover { - background: var(--text); + background: var(--cyan); color: var(--bg); - border-color: var(--text); + border-color: var(--cyan); } .btn-cancel { - font-size: 13px; + font-size: 14px; color: var(--text-dim); - padding: 10px 16px; + padding: 12px 20px; + background: transparent; + border: none; + cursor: pointer; + font-weight: 500; } +.btn-cancel:hover { color: var(--red); } /* ═══ SIDEBAR TOGGLE (mobile) ═══ */ .sidebar-toggle { display: none; position: fixed; - top: 12px; - left: 12px; + top: 16px; + left: 16px; z-index: 100; - background: var(--bg-alt); + background: var(--bg); border: 1px solid var(--border); - padding: 8px; + border-radius: var(--radius-sm); + padding: 10px; cursor: pointer; flex-direction: column; - gap: 4px; + gap: 5px; + box-shadow: var(--shadow); } .sidebar-toggle span { display: block; - width: 20px; + width: 22px; height: 2px; background: var(--text); + border-radius: 1px; } -/* ��══ SEARCH SUGGESTIONS ═══ */ +/* ═══ SEARCH SUGGESTIONS ═══ */ .search-wrapper { position: relative; } .search-suggestions { display: none; position: absolute; - top: 100%; + top: calc(100% + 4px); left: 0; right: 0; background: var(--bg); border: 1px solid var(--border); - border-top: none; + border-radius: var(--radius-md); + box-shadow: 0 8px 24px rgba(0,0,0,0.1); z-index: 60; - max-height: 320px; + max-height: 360px; overflow-y: auto; } +@media (prefers-color-scheme: dark) { + .search-suggestions { box-shadow: 0 8px 24px rgba(0,0,0,0.4); } +} + .suggestion { display: flex; flex-direction: column; - padding: 8px 10px; + padding: 10px 14px; border: none; - border-bottom: 1px solid rgba(46, 52, 64, 0.4); - font-size: 12px; + border-bottom: 1px solid var(--border); + font-size: 13px; + transition: background 0.15s; } -.suggestion:hover { background: rgba(163, 190, 140, 0.1); } -.sug-title { color: var(--text); } -.sug-domain { color: var(--text-dim); font-size: 10px; } +.suggestion:last-child { border-bottom: none; } +.suggestion:hover { background: rgba(0, 107, 127, 0.05); } +.sug-title { color: var(--text); font-weight: 500; } +.sug-domain { color: var(--text-dim); font-size: 11px; font-family: var(--font-mono); margin-top: 2px; } /* ═══ SIDEBAR NAV EXTRAS ═══ */ -.nav-separator { border-top: 1px solid var(--border); margin: 8px 0; } +.nav-separator { border-top: 1px solid var(--border); margin: 16px 0; } .nav-link { display: block; - padding: 4px 10px; - font-size: 11px; + padding: 8px 12px; + font-size: 13px; + font-weight: 500; color: var(--text-dim); border: none; - transition: all 0.15s; + border-radius: var(--radius-sm); + transition: all 0.2s ease; } -.nav-link:hover { color: var(--cyan); background: rgba(136, 192, 208, 0.05); } -.nav-new { color: var(--text); font-weight: 600; } +.nav-link:hover { color: var(--cyan); background: rgba(0, 107, 127, 0.05); transform: translateX(2px); } +.nav-new { color: var(--text); font-weight: 600; background: var(--bg); border: 1px solid var(--border); text-align: center; margin-top: 8px;} +.nav-new:hover { background: var(--text); color: var(--bg); border-color: var(--text); transform: none; } /* ═══ PAGE ACTIONS ═══ */ -.page-actions { display: flex; gap: 8px; } +.page-actions { display: flex; gap: 12px; } /* ═══ TOC ═══ */ @@ -604,67 +772,78 @@ h5, h6 { font-size: 13px; margin: 16px 0 8px; color: var(--text-dim); } .toc { position: sticky; - top: 16px; + top: 32px; float: right; - width: 220px; - margin-left: 24px; - margin-bottom: 16px; - padding: 12px; + width: 260px; + margin-left: 40px; + margin-bottom: 24px; + padding: 20px; background: var(--bg-alt); border: 1px solid var(--border); - font-size: 11px; - max-height: calc(100vh - 100px); + border-radius: var(--radius-md); + font-size: 13px; + max-height: calc(100vh - 64px); overflow-y: auto; } -.toc-title { color: var(--yellow); font-weight: 600; margin-bottom: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; } +.toc-title { color: var(--heading); font-weight: 700; margin-bottom: 12px; font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid var(--border); padding-bottom: 8px;} .toc ul { list-style: none; margin: 0; padding: 0; } -.toc li { margin-bottom: 2px; } -.toc a { color: var(--text-dim); border: none; display: block; padding: 2px 0; } +.toc li { margin-bottom: 4px; } +.toc a { color: var(--text-dim); border: none; display: block; padding: 4px 0; transition: color 0.15s; } .toc a:hover { color: var(--cyan); background: none; } .toc-level-2 { padding-left: 8px; } -.toc-level-3 { padding-left: 16px; font-size: 10px; } -.toc-level-4 { padding-left: 24px; font-size: 10px; } +.toc-level-3 { padding-left: 16px; font-size: 12px; } +.toc-level-4 { padding-left: 24px; font-size: 12px; } .toc-level-5, .toc-level-6 { display: none; } /* ═══ TAGS ═══ */ .meta-tag { - background: rgba(180, 142, 173, 0.15); + background: rgba(139, 107, 133, 0.1); color: var(--purple); - padding: 1px 6px; - border: none; - font-size: 10px; + padding: 2px 8px; + border-radius: 10px; + border: 1px solid rgba(139, 107, 133, 0.2); + font-size: 11px; + font-weight: 500; } .meta-tag:hover { background: var(--purple); color: var(--bg); } -.tag-cloud { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; } +.tag-cloud { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 32px; } .tag { - padding: 4px 12px; + padding: 6px 16px; background: var(--bg-alt); border: 1px solid var(--border); + border-radius: 20px; color: var(--purple); - font-size: 12px; + font-size: 13px; + font-weight: 500; + transition: all 0.2s ease; } -.tag:hover { background: var(--purple); color: var(--bg); border-color: var(--purple); } -.tag-active { background: var(--purple); color: var(--bg); border-color: var(--purple); } +.tag:hover, .tag-active { background: var(--purple); color: var(--bg); border-color: var(--purple); box-shadow: 0 2px 8px rgba(139, 107, 133, 0.3); } /* ═══ TRANSCLUSION ═══ */ .transclusion { - margin: 16px 0; - padding: 16px; - border-left: 3px solid var(--cyan); - background: rgba(136, 192, 208, 0.03); + margin: 24px 0; + padding: 24px; + border: 1px solid var(--border); + border-left: 4px solid var(--cyan); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + background: rgba(0, 107, 127, 0.02); +} +@media (prefers-color-scheme: dark) { + .transclusion { background: rgba(0, 152, 181, 0.05); } } .transclusion-header { - font-size: 11px; + font-size: 12px; color: var(--text-dim); - margin-bottom: 8px; - padding-bottom: 8px; + margin-bottom: 12px; + padding-bottom: 12px; border-bottom: 1px solid var(--border); + font-family: var(--font-mono); } .transclusion-header::before { content: "↗ "; color: var(--cyan); } -.transclusion-error { color: var(--red); font-size: 12px; padding: 8px; border: 1px dashed var(--red); } +.transclusion-error { color: var(--red); font-size: 13px; padding: 12px; border: 1px dashed var(--red); border-radius: var(--radius-sm); background: rgba(180, 80, 70, 0.05); } /* ═══ EDITOR TOOLBAR ═══ */ @@ -672,211 +851,206 @@ h5, h6 { font-size: 13px; margin: 16px 0 8px; color: var(--text-dim); } display: flex; gap: 8px; align-items: center; - margin-bottom: 8px; - padding: 8px 0; - border-bottom: 1px solid var(--border); -} -.toolbar-btn, .toolbar-upload { + margin-bottom: 16px; + padding: 12px; background: var(--bg-alt); border: 1px solid var(--border); - color: var(--text-dim); - font-family: var(--font); - font-size: 11px; - padding: 4px 12px; + border-radius: var(--radius-md); +} +.toolbar-btn, .toolbar-upload { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + font-family: var(--font-body); + font-size: 13px; + font-weight: 500; + padding: 6px 12px; cursor: pointer; transition: all 0.15s; } .toolbar-btn:hover, .toolbar-upload:hover { color: var(--cyan); border-color: var(--cyan); } -.toolbar-hint { font-size: 10px; color: var(--text-dim); margin-left: auto; } +.toolbar-hint { font-size: 12px; color: var(--text-dim); margin-left: auto; font-family: var(--font-mono); } /* ═══ EDITOR SPLIT ═══ */ -.editor-split { display: flex; gap: 0; } +.editor-split { display: flex; gap: 20px; } .editor-split .edit-textarea { flex: 1; min-height: 70vh; } -.editor-split.split-active .edit-textarea { width: 50%; min-height: 70vh; border-right: none; } +.editor-split.split-active .edit-textarea { width: 50%; min-height: 70vh; } .preview-pane { flex: 1; background: var(--bg); border: 1px solid var(--border); - padding: 16px; + border-radius: var(--radius-md); + padding: 24px; overflow-y: auto; max-height: 70vh; } /* ═══ FORM ═══ */ -.form-group { margin-bottom: 16px; } -.form-label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 4px; } +.form-group { margin-bottom: 20px; } +.form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 8px; } .form-input { width: 100%; - background: var(--bg-alt); + background: var(--bg); border: 1px solid var(--border); + border-radius: var(--radius-sm); color: var(--text); - font-family: var(--font); - font-size: 14px; - padding: 10px 14px; + font-family: var(--font-body); + font-size: 15px; + padding: 12px 16px; outline: none; + transition: border-color 0.2s, box-shadow 0.2s; } -.form-input:focus { border-color: var(--cyan); } - -/* ═══ GRAPH ═══ */ - -.graph-container { - margin-top: 16px; - border: 1px solid var(--border); - background: var(--bg-alt); -} -.graph-controls { - display: flex; - gap: 12px; - align-items: center; - margin-bottom: 12px; -} -.graph-controls select { - background: var(--bg-alt); - border: 1px solid var(--border); - color: var(--text); - font-family: var(--font); - font-size: 12px; - padding: 4px 8px; - outline: none; -} -.graph-controls label { font-size: 12px; color: var(--text-dim); } +.form-input:focus { border-color: var(--cyan); box-shadow: 0 0 0 3px rgba(0, 107, 127, 0.1); } /* ═══ DASHBOARD ═══ */ .dash-stats { display: flex; - gap: 16px; - margin-bottom: 32px; + gap: 20px; + margin-bottom: 40px; flex-wrap: wrap; } .dash-stat { background: var(--bg-alt); border: 1px solid var(--border); - padding: 16px 24px; + border-radius: var(--radius-md); + padding: 24px 32px; display: flex; flex-direction: column; align-items: center; - min-width: 100px; + min-width: 140px; + box-shadow: var(--shadow); } -.dash-stat-num { font-size: 28px; font-weight: 700; color: var(--heading); } -.dash-stat-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; } +.dash-stat-num { font-family: var(--font-heading); font-size: 36px; font-weight: 700; color: var(--heading); line-height: 1; margin-bottom: 4px;} +.dash-stat-label { font-size: 12px; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; } .dash-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); - gap: 24px; + grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); + gap: 32px; } .dash-section { background: var(--bg-alt); border: 1px solid var(--border); - padding: 20px; + border-radius: var(--radius-md); + padding: 24px; max-height: 500px; overflow-y: auto; } -.dash-section h2 { font-size: 14px; margin-bottom: 12px; } -.dash-section h2::before { content: none; } +.dash-section h2 { font-size: 16px; margin: 0 0 16px 0; border: none; padding: 0; } -.dash-hint { font-size: 11px; color: var(--text-dim); margin-bottom: 8px; } +.dash-hint { font-size: 12px; color: var(--text-dim); margin-bottom: 16px; font-family: var(--font-mono); } .dash-list { display: flex; flex-direction: column; } .dash-item { display: flex; justify-content: space-between; - align-items: baseline; - padding: 6px 0; - border-bottom: 1px solid rgba(46, 52, 64, 0.4); - font-size: 12px; + align-items: center; + padding: 10px 12px; + border-bottom: 1px solid var(--border); + font-size: 13px; border: none; - gap: 8px; + gap: 12px; + border-radius: var(--radius-sm); } -.dash-item:hover { background: rgba(163, 190, 140, 0.04); } -.dash-item-title { color: var(--text); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.dash-item-meta { color: var(--text-dim); font-size: 11px; white-space: nowrap; } -.dash-item-meta.broken { color: var(--red); } -.git-msg { color: var(--text-dim); } +.dash-item:hover { background: var(--bg); } +.dash-item-title { color: var(--text); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } +.dash-item-meta { color: var(--text-dim); font-size: 12px; white-space: nowrap; font-family: var(--font-mono); } +.dash-item-meta.broken { color: var(--red); background: rgba(180, 80, 70, 0.1); padding: 2px 6px; border-radius: 4px; } +.git-msg { color: var(--text-dim); font-style: italic; } /* ═══ HISTORY ═══ */ -.history-list { display: flex; flex-direction: column; gap: 4px; } +.history-list { display: flex; flex-direction: column; gap: 8px; } .history-entry { display: grid; - grid-template-columns: 80px 1fr auto; - gap: 12px; - padding: 8px 0; - border-bottom: 1px solid rgba(46, 52, 64, 0.4); - font-size: 12px; + grid-template-columns: 90px 1fr auto; + gap: 16px; + padding: 12px 16px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-alt); + font-size: 13px; align-items: baseline; } -.history-hash { color: var(--cyan); font-size: 12px; } -.history-msg { color: var(--text); } -.history-meta { color: var(--text-dim); font-size: 11px; } +.history-hash { color: var(--cyan); font-family: var(--font-mono); font-size: 12px; } +.history-msg { color: var(--text); font-weight: 500; } +.history-meta { color: var(--text-dim); font-size: 12px; font-family: var(--font-mono); } -.diff-view { margin-bottom: 32px; } -.diff-message { color: var(--text-dim); font-size: 12px; margin-bottom: 8px; } +.diff-view { margin-bottom: 40px; } +.diff-message { color: var(--text); font-size: 14px; font-weight: 500; margin-bottom: 12px; } .diff-content { background: var(--bg-alt); border: 1px solid var(--border); - padding: 16px; - font-size: 12px; + border-radius: var(--radius-md); + padding: 20px; + font-size: 13px; + font-family: var(--font-mono); overflow-x: auto; white-space: pre; - line-height: 1.5; + line-height: 1.6; } /* ═══ RESPONSIVE ═══ */ -@media (max-width: 768px) { +@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } .sidebar { position: fixed; top: 0; - left: -280px; - width: 280px; - z-index: 50; - transition: left 0.2s; + left: -320px; + width: 300px; + z-index: 150; + transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 4px 0 24px rgba(0,0,0,0.1); + } + @media (prefers-color-scheme: dark) { + .sidebar { box-shadow: 4px 0 24px rgba(0,0,0,0.5); } } .sidebar.open { left: 0; } .sidebar-toggle { display: flex; } .main { - padding: 60px 20px 32px; - border-left: none; + padding: 80px 32px 48px; } - .page-header { flex-direction: column; align-items: flex-start; } + .page-header { flex-direction: column; align-items: flex-start; gap: 8px;} .dir-header, .dir-entry { - grid-template-columns: 20px 1fr 80px; + grid-template-columns: 24px 1fr 80px; } .dir-col-size { display: none; } .domain-grid { grid-template-columns: 1fr; } - .toc { float: none; width: 100%; margin: 0 0 16px 0; position: static; } + .toc { float: none; width: 100%; margin: 0 0 24px 0; position: static; max-height: none; } .dash-grid { grid-template-columns: 1fr; } .editor-split.split-active { flex-direction: column; } .editor-split.split-active .edit-textarea { width: 100%; min-height: 40vh; } .preview-pane { max-height: 40vh; } - .history-entry { grid-template-columns: 1fr; gap: 2px; } + .history-entry { grid-template-columns: 1fr; gap: 6px; } } @media (max-width: 480px) { - .main { padding: 60px 12px 24px; } - h1 { font-size: 20px; } - h2 { font-size: 17px; } + .main { padding: 80px 20px 32px; } + h1 { font-size: 28px; } + h2 { font-size: 20px; } .search-form { flex-direction: column; } + .dash-stats { flex-direction: column; } + .dash-stat { width: 100%; } } -/* ═══ F13: PAGE TRANSITIONS ═══ */ +/* ═══ TRANSITIONS ═══ */ -@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } -.main { animation: fadeIn 0.15s ease-out; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } +.main { animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); } -/* ═══ F14: PRINT STYLESHEET ═══ */ +/* ═══ PRINT STYLESHEET ═══ */ @media print { .sidebar, .sidebar-toggle, .btn-edit, .btn-cancel, .btn-save, .btn-search, @@ -884,140 +1058,251 @@ h5, h6 { font-size: 13px; margin: 16px 0 8px; color: var(--text-dim); } .nav-link, .nav-new, .graph-controls, .backlinks { display: none !important; } .layout { display: block !important; } .main { max-width: 100% !important; padding: 0 !important; border: none !important; animation: none !important; } - body { background: white !important; color: #111 !important; font-size: 12px; } - h1, h2, h3, h4, h5, h6 { color: #111 !important; } - h1::before, h2::before, h3::before { content: none !important; } - a { color: #111 !important; border: none !important; } - a[href]::after { content: " (" attr(href) ")"; font-size: 10px; color: #666; } + body { background: white !important; color: black !important; font-size: 11pt; } + h1, h2, h3, h4, h5, h6 { color: black !important; } + h1::before { content: none !important; } + a { color: black !important; border: none !important; text-decoration: underline; } + a[href]::after { content: " (" attr(href) ")"; font-size: 9pt; color: #666; } a.wikilink::after, a.nav-domain::after { content: none; } - pre, code { background: #f5f5f5 !important; color: #111 !important; border: 1px solid #ddd !important; } + pre, code { background: #f5f5f5 !important; color: black !important; border: 1px solid #ddd !important; } table, th, td { border-color: #ccc !important; } - th { background: #eee !important; color: #111 !important; } + th { background: #eee !important; color: black !important; } h1 { page-break-before: auto; } .page-content { page-break-inside: auto; } img { max-width: 100% !important; } } -/* ═══ F11: COLORED DIFF ═══ */ +/* ═══ COLORED DIFF ═══ */ -.diff-line { display: block; padding: 1px 8px; margin: 0 -8px; } -.diff-add { color: var(--text); background: rgba(163, 190, 140, 0.12); } -.diff-del { color: var(--red); background: rgba(191, 97, 106, 0.12); } -.diff-hunk { color: var(--cyan); background: rgba(136, 192, 208, 0.08); font-weight: 600; } +.diff-line { display: block; padding: 2px 12px; margin: 0 -20px; } +.diff-add { color: #2A5A35; background: rgba(90, 140, 100, 0.15); } +.diff-del { color: var(--red); background: rgba(180, 80, 70, 0.15); } +@media (prefers-color-scheme: dark) { + .diff-add { color: #85C096; background: rgba(90, 140, 100, 0.25); } + .diff-del { color: #E89A92; background: rgba(180, 80, 70, 0.25); } +} +.diff-hunk { color: var(--cyan); background: rgba(0, 107, 127, 0.08); font-weight: 600; } .diff-ctx { color: var(--text-dim); } -/* ═══ F12: DOMAIN ACCENT COLORS ═══ */ - -.nav-domain { border-left: 3px solid transparent; padding-left: 8px; } -.meta-domain-colored { padding: 1px 6px; font-size: 10px; border: none; } - -/* ═══ F2: DRAG & DROP ═══ */ - -.editor-dragover { border-color: var(--cyan) !important; box-shadow: inset 0 0 20px rgba(136, 192, 208, 0.1); } - -/* ═══ F1: WIKILINK AUTOCOMPLETE ═══ */ - -.wl-autocomplete { - position: absolute; - z-index: 70; - background: var(--bg); - border: 1px solid var(--border); - max-height: 200px; - overflow-y: auto; - min-width: 280px; - display: none; -} -.wl-autocomplete .suggestion { border: none; border-bottom: 1px solid rgba(46, 52, 64, 0.4); } - -/* ═══ F3: DRAFT BANNER ═══ */ +/* ═══ DRAFT BANNER ═══ */ .draft-banner { - background: rgba(235, 203, 139, 0.1); + background: rgba(190, 159, 60, 0.1); border: 1px solid var(--yellow); - padding: 8px 16px; - margin-bottom: 12px; - font-size: 12px; + border-radius: var(--radius-md); + padding: 12px 20px; + margin-bottom: 24px; + font-size: 14px; display: flex; align-items: center; - gap: 12px; + gap: 16px; } -.draft-banner-text { color: var(--yellow); flex: 1; } +.draft-banner-text { color: var(--yellow); flex: 1; font-weight: 500; } .draft-banner-btn { - background: none; + background: var(--bg); border: 1px solid var(--border); + border-radius: var(--radius-sm); color: var(--text); - font-family: var(--font); - font-size: 11px; - padding: 3px 10px; + font-family: var(--font-body); + font-size: 13px; + font-weight: 500; + padding: 6px 12px; cursor: pointer; } .draft-banner-btn:hover { background: var(--text); color: var(--bg); } -/* ═══ F4: EDITOR TOOLBAR BUTTONS ═══ */ - -.toolbar-buttons { display: flex; gap: 4px; } -.toolbar-buttons button { - background: var(--bg-alt); - border: 1px solid var(--border); - color: var(--text-dim); - font-family: var(--font); - font-size: 11px; - padding: 3px 8px; - cursor: pointer; - min-width: 28px; -} -.toolbar-buttons button:hover { color: var(--cyan); border-color: var(--cyan); } - -/* ═══ F17: SHORTCUTS HELP ═══ */ +/* ═══ SHORTCUTS HELP ═══ */ .shortcuts-overlay { display: none; position: fixed; - bottom: 16px; - right: 16px; + bottom: 24px; + right: 24px; z-index: 200; background: var(--bg-alt); border: 1px solid var(--border); - padding: 16px; - font-size: 11px; - min-width: 220px; + border-radius: var(--radius-md); + padding: 20px; + font-size: 13px; + min-width: 240px; + box-shadow: 0 12px 40px rgba(0,0,0,0.15); } -.shortcuts-overlay.visible { display: block; } -.shortcuts-overlay h4 { color: var(--heading); margin-bottom: 8px; font-size: 12px; } -.shortcuts-overlay h4::before { content: none; } -.shortcut-row { display: flex; justify-content: space-between; padding: 2px 0; } -.shortcut-key { color: var(--cyan); background: var(--bg); padding: 1px 6px; font-size: 10px; border: 1px solid var(--border); } +@media (prefers-color-scheme: dark) { + .shortcuts-overlay { box-shadow: 0 12px 40px rgba(0,0,0,0.5); } +} +.shortcuts-overlay.visible { display: block; animation: fadeIn 0.2s; } +.shortcuts-overlay h4 { font-size: 14px; margin-bottom: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px;} +.shortcut-row { display: flex; justify-content: space-between; padding: 4px 0; align-items: center; } +.shortcut-key { color: var(--text); background: var(--bg); padding: 2px 8px; font-size: 11px; font-family: var(--font-mono); border: 1px solid var(--border); border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } -/* ═══ F7: HEALTH BARS ═══ */ +/* ═══ COMMAND PALETTE ═══ */ -.health-table { width: 100%; font-size: 12px; border-collapse: collapse; } -.health-table th { text-align: left; color: var(--text-dim); font-weight: 400; padding: 6px 8px; border-bottom: 1px solid var(--border); font-size: 11px; } -.health-table td { padding: 6px 8px; border-bottom: 1px solid rgba(46, 52, 64, 0.4); } -.health-bar { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; width: 60px; display: inline-block; vertical-align: middle; } -.health-fill { height: 100%; border-radius: 3px; } -.health-good { background: var(--text); } -.health-mid { background: var(--yellow); } -.health-low { background: var(--red); } - -/* ═══ F8: READING TIME ═══ */ - -.meta-reading { color: var(--text-dim); } - -/* ═══ F6: BROKEN LINK FIX ═══ */ - -.fix-btn { - background: none; - border: 1px solid var(--text); - color: var(--text); - font-family: var(--font); - font-size: 10px; - padding: 2px 8px; +.cmd-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.4); + backdrop-filter: blur(2px); + z-index: 400; +} +.cmd-palette { + position: fixed; + top: 15%; + left: 50%; + transform: translateX(-50%); + width: 600px; + max-width: 95vw; + z-index: 401; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: 0 24px 64px rgba(0,0,0,0.2); + overflow: hidden; +} +@media (prefers-color-scheme: dark) { + .cmd-palette { box-shadow: 0 24px 64px rgba(0,0,0,0.6); } +} +.cmd-input { + width: 100%; + background: transparent; + border: none; + border-bottom: 1px solid var(--border); + color: var(--heading); + font-family: var(--font-body); + font-size: 18px; + padding: 20px 24px; + outline: none; +} +.cmd-input::placeholder { color: var(--text-dim); } +.cmd-results { max-height: 400px; overflow-y: auto; background: var(--bg-alt); } +.cmd-result { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 24px; + border: none; + border-bottom: 1px solid var(--border); + font-size: 14px; cursor: pointer; - margin-left: 8px; } -.fix-btn:hover { background: var(--text); color: var(--bg); } +.cmd-result:last-child { border-bottom: none; } +.cmd-result:hover, .cmd-result.active { background: var(--bg); border-left: 3px solid var(--cyan); padding-left: 21px; } +.cmd-result-title { color: var(--heading); font-weight: 500; } +.cmd-result-type { color: var(--text-dim); font-size: 12px; font-family: var(--font-mono); background: var(--border); padding: 2px 8px; border-radius: 4px; } +.cmd-empty { padding: 32px; color: var(--text-dim); font-size: 14px; text-align: center; font-style: italic; } -/* ═══ F16: LOGIN ═══ */ +/* ═══ SCROLL PROGRESS BAR ═══ */ + +.scroll-progress { + position: fixed; + top: 0; + left: 0; + height: 3px; + width: 0; + background: var(--cyan); + z-index: 500; + transition: width 0.1s; + box-shadow: 0 0 8px rgba(0, 107, 127, 0.4); +} + +/* ═══ CODE COPY BUTTON ═══ */ + +.code-copy-btn { + position: absolute; + top: 12px; + right: 12px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 11px; + padding: 4px 8px; + cursor: pointer; + opacity: 0; + transition: all 0.2s ease; +} +pre:hover .code-copy-btn { opacity: 1; } +.code-copy-btn:hover { color: var(--cyan); border-color: var(--cyan); background: var(--bg-alt); } + +/* ═══ LIGHTBOX ═══ */ + +.lightbox-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.9); + backdrop-filter: blur(4px); + z-index: 600; + display: flex; + align-items: center; + justify-content: center; + cursor: zoom-out; +} +.lightbox-img { + max-width: 90vw; + max-height: 90vh; + border: 1px solid var(--border); + border-radius: var(--radius-md); + box-shadow: 0 12px 48px rgba(0,0,0,0.5); + cursor: default; +} + +/* ═══ MINIMAP ═══ */ + +.minimap { + position: fixed; + right: 16px; + top: 120px; + width: 12px; + height: 120px; + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: 6px; + z-index: 30; + overflow: hidden; +} +.minimap-dot { + position: absolute; + left: 2px; + width: 6px; + height: 4px; + background: var(--cyan); + cursor: pointer; + border-radius: 2px; + opacity: 0.6; +} +.minimap-dot:hover { opacity: 1; background: var(--heading); } +.minimap-viewport { + position: absolute; + left: 0; + width: 100%; + height: 20px; + background: rgba(0, 107, 127, 0.1); + border: 1px solid rgba(0, 107, 127, 0.3); + pointer-events: none; + border-radius: 2px; +} + +/* ═══ ACTIVITY FEED ═══ */ + +.activity-feed { display: flex; flex-direction: column; } +.activity-entry { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px 0; + border-bottom: 1px solid var(--border); +} +.activity-entry:last-child { border-bottom: none; } +.activity-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--cyan); margin-top: 6px; flex-shrink: 0; box-shadow: 0 0 0 4px var(--bg-alt); } +.activity-content { flex: 1; min-width: 0; } +.activity-title { display: block; font-size: 14px; font-weight: 500; color: var(--heading); margin-bottom: 4px; } +.activity-meta { font-size: 12px; color: var(--text-dim); font-family: var(--font-mono); } + +/* ═══ TOC ACTIVE ═══ */ + +.toc a.toc-active { color: var(--cyan); font-weight: 600; border-left: 2px solid var(--cyan); padding-left: 6px; margin-left: -8px;} + +/* ═══ LOGIN ═══ */ .login-container { display: flex; @@ -1029,245 +1314,99 @@ h5, h6 { font-size: 13px; margin: 16px 0 8px; color: var(--text-dim); } .login-box { background: var(--bg-alt); border: 1px solid var(--border); - padding: 32px; - width: 360px; + border-radius: var(--radius-lg); + padding: 48px; + width: 420px; max-width: 90vw; + box-shadow: var(--shadow); } -.login-box h1 { text-align: center; margin-bottom: 24px; } -.login-error { color: var(--red); font-size: 12px; margin-bottom: 12px; } -.login-box .form-group { margin-bottom: 16px; } -.login-box .btn-save { width: 100%; } +.login-box h1 { text-align: center; margin-bottom: 32px; font-size: 28px; } +.login-box h1::before { display: none; } +.login-error { color: var(--red); font-size: 14px; margin-bottom: 16px; background: rgba(180, 80, 70, 0.1); padding: 12px; border-radius: var(--radius-sm); text-align: center; } +.login-box .form-group { margin-bottom: 24px; } +.login-box .btn-save { width: 100%; padding: 14px; font-size: 16px; } -/* ═══ SIMILAR PAGES ═══ */ - -.similar-pages { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); } -.similar-pages h3 { font-size: 13px; color: var(--text-dim); margin-bottom: 8px; } -.similar-pages h3::before { content: "≈ "; color: var(--purple); } -.similar-list { display: flex; flex-wrap: wrap; gap: 8px; } -.similar-item { font-size: 12px; padding: 4px 10px; background: var(--bg-alt); border: 1px solid var(--border); } -.similar-item:hover { border-color: var(--purple); } -.similar-domain { color: var(--text-dim); font-size: 10px; margin-left: 4px; } - -/* ═══ COMMENTS ═══ */ - -.comments-section { margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--border); } -.comments-section h3 { font-size: 13px; color: var(--text-dim); margin-bottom: 12px; } -.comments-section h3::before { content: "💬 "; } -.comment { padding: 10px; margin-bottom: 8px; background: var(--bg-alt); border: 1px solid var(--border); position: relative; } -.comment-meta { font-size: 10px; color: var(--text-dim); margin-bottom: 4px; } -.comment-content { font-size: 13px; line-height: 1.6; } -.comment-form { margin-top: 12px; } -.comment-input { - width: 100%; background: var(--bg-alt); border: 1px solid var(--border); - color: var(--text); font-family: var(--font); font-size: 12px; padding: 8px; outline: none; resize: vertical; -} -.comment-input:focus { border-color: var(--cyan); } -.comment-del-btn { position: absolute; top: 8px; right: 8px; background: none; border: none; color: var(--text-dim); font-size: 10px; cursor: pointer; font-family: var(--font); } -.comment-del-btn:hover { color: var(--red); } -.comment-delete { display: inline; } - -/* ═══ LINK TOOLTIP (hover preview) ═══ */ +/* ═══ LINK TOOLTIP ═══ */ .link-tooltip { position: absolute; z-index: 200; background: var(--bg-alt); border: 1px solid var(--border); - padding: 10px 14px; - max-width: 320px; - font-size: 12px; - line-height: 1.5; - pointer-events: none; - box-shadow: 0 4px 12px rgba(0,0,0,0.4); -} - -/* ═══ MINIMAP ═══ */ - -.minimap { - position: fixed; - right: 8px; - top: 100px; - width: 8px; - height: 100px; - background: var(--bg-alt); - border: 1px solid var(--border); - z-index: 30; -} -.minimap-dot { - position: absolute; - left: 1px; - width: 6px; - height: 3px; - background: var(--yellow); - cursor: pointer; - border-radius: 1px; -} -.minimap-dot:hover { background: var(--heading); } -.minimap-viewport { - position: absolute; - left: 0; - width: 100%; - height: 15px; - background: rgba(163, 190, 140, 0.15); - border: 1px solid rgba(163, 190, 140, 0.3); - pointer-events: none; -} - -/* ═══ VIEWERS ═══ */ - -.meta-viewers { color: var(--cyan); } - -/* ═══ LINK SUGGESTIONS (dashboard) ═══ */ - -.suggestion-target { color: var(--cyan); font-size: 11px; } - -/* ═══ SCROLL PROGRESS BAR ═══ */ - -.scroll-progress { - position: fixed; - top: 0; - left: 0; - height: 2px; - width: 0; - background: linear-gradient(to right, var(--text), var(--cyan)); - z-index: 300; - transition: width 0.1s; -} - -/* ═══ COMMAND PALETTE ═══ */ - -.cmd-overlay { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.5); - z-index: 400; -} -.cmd-palette { - position: fixed; - top: 20%; - left: 50%; - transform: translateX(-50%); - width: 520px; - max-width: 90vw; - z-index: 401; - background: var(--bg-alt); - border: 1px solid var(--border); - box-shadow: 0 8px 32px rgba(0,0,0,0.5); -} -.cmd-input { - width: 100%; - background: var(--bg); - border: none; - border-bottom: 1px solid var(--border); - color: var(--text); - font-family: var(--font); - font-size: 15px; - padding: 14px 16px; - outline: none; -} -.cmd-input::placeholder { color: var(--text-dim); } -.cmd-results { max-height: 320px; overflow-y: auto; } -.cmd-result { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 16px; - border: none; - border-bottom: 1px solid rgba(46, 52, 64, 0.3); + border-radius: var(--radius-md); + padding: 16px; + max-width: 360px; font-size: 13px; + line-height: 1.6; + pointer-events: none; + box-shadow: 0 12px 32px rgba(0,0,0,0.2); } -.cmd-result:hover, .cmd-result.active { background: rgba(163, 190, 140, 0.08); } -.cmd-result-title { color: var(--text); } -.cmd-result-type { color: var(--text-dim); font-size: 11px; } -.cmd-empty { padding: 16px; color: var(--text-dim); font-size: 13px; text-align: center; } - -/* ═══ SEARCH FILTER ═══ */ - -.search-filter { - background: var(--bg-alt); - border: 1px solid var(--border); - color: var(--text); - font-family: var(--font); - font-size: 13px; - padding: 10px; - outline: none; - min-width: 180px; +@media (prefers-color-scheme: dark) { + .link-tooltip { box-shadow: 0 12px 32px rgba(0,0,0,0.6); } } -.search-filter:focus { border-color: var(--cyan); } - -/* ═══ CODE COPY BUTTON ═══ */ - -.code-copy-btn { - position: absolute; - top: 4px; - right: 4px; - background: var(--bg); - border: 1px solid var(--border); - color: var(--text-dim); - font-family: var(--font); - font-size: 10px; - padding: 2px 8px; - cursor: pointer; - opacity: 0; - transition: opacity 0.15s; -} -pre:hover .code-copy-btn { opacity: 1; } -.code-copy-btn:hover { color: var(--cyan); border-color: var(--cyan); } - -/* ═══ LIGHTBOX ═══ */ - -.lightbox-overlay { - position: fixed; - inset: 0; - background: rgba(0,0,0,0.85); - z-index: 500; - display: flex; - align-items: center; - justify-content: center; - cursor: zoom-out; -} -.lightbox-img { - max-width: 90vw; - max-height: 90vh; - border: 1px solid var(--border); - cursor: default; -} - -/* ═══ TABLE SORT ═══ */ - -th.sorted-asc::after { content: " ▲"; color: var(--yellow); font-size: 10px; } -th.sorted-desc::after { content: " ▼"; color: var(--yellow); font-size: 10px; } - -/* ═══ TOC ACTIVE ═══ */ - -.toc a.toc-active { color: var(--cyan); font-weight: 600; } /* ═══ STALE WARNING ═══ */ .meta-stale { - background: rgba(191, 97, 106, 0.15); + background: rgba(180, 80, 70, 0.1); color: var(--red); - padding: 1px 6px; - font-size: 10px; + padding: 2px 8px; + font-size: 11px; + border-radius: 4px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; } -/* ═══ ACTIVITY FEED ═══ */ +/* ═══ FIX BTN ═══ */ -.activity-feed { display: flex; flex-direction: column; } -.activity-entry { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 10px 0; - border-bottom: 1px solid rgba(46, 52, 64, 0.4); +.fix-btn { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-dim); + font-family: var(--font-body); + font-size: 11px; + padding: 4px 10px; + cursor: pointer; + margin-left: 12px; + transition: all 0.2s; } -.activity-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; } -.activity-content { flex: 1; min-width: 0; } -.activity-title { display: block; font-size: 13px; color: var(--text); } -.activity-meta { font-size: 11px; color: var(--text-dim); } +.fix-btn:hover { background: var(--text); color: var(--bg); border-color: var(--text); } -@media (max-width: 768px) { - .minimap { display: none; } - .link-tooltip { display: none; } - .cmd-palette { width: 95vw; top: 10%; } +/* ═══ COMMENTS ═══ */ + +.comments-section { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--border); } +.comments-section h3 { font-size: 16px; color: var(--heading); margin-bottom: 20px; font-family: var(--font-heading); } +.comment { padding: 16px 20px; margin-bottom: 12px; background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--radius-md); position: relative; } +.comment-meta { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; font-family: var(--font-mono); } +.comment-content { font-size: 14px; line-height: 1.6; color: var(--text); } +.comment-form { margin-top: 24px; } +.comment-input { + width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-sm); + color: var(--text); font-family: var(--font-body); font-size: 14px; padding: 12px; outline: none; resize: vertical; transition: border-color 0.2s; } +.comment-input:focus { border-color: var(--cyan); box-shadow: 0 0 0 3px rgba(0, 107, 127, 0.1); } +.comment-del-btn { position: absolute; top: 12px; right: 12px; background: none; border: none; color: var(--text-dim); font-size: 11px; cursor: pointer; font-family: var(--font-mono); padding: 4px; border-radius: 4px; } +.comment-del-btn:hover { color: var(--red); background: rgba(180, 80, 70, 0.1); } + +/* ═══ SIMILAR PAGES ═══ */ + +.similar-pages { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--border); } +.similar-pages h3 { font-size: 16px; color: var(--heading); margin-bottom: 16px; font-family: var(--font-heading); } +.similar-list { display: flex; flex-wrap: wrap; gap: 12px; } +.similar-item { font-size: 13px; padding: 8px 14px; background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--radius-md); transition: all 0.2s; } +.similar-item:hover { border-color: var(--cyan); background: var(--bg); box-shadow: var(--shadow); transform: translateY(-1px); } +.similar-domain { color: var(--text-dim); font-size: 11px; margin-left: 8px; font-family: var(--font-mono); } + +/* ═══ HEALTH BARS ═══ */ + +.health-table { width: 100%; font-size: 13px; border-collapse: collapse; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; } +.health-table th { text-align: left; color: var(--heading); font-weight: 600; padding: 12px 16px; border-bottom: 1px solid var(--border); background: var(--bg-alt); } +.health-table td { padding: 12px 16px; border-bottom: 1px solid var(--border); } +.health-table tr:last-child td { border-bottom: none; } +.health-bar { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; width: 80px; display: inline-block; vertical-align: middle; } +.health-fill { height: 100%; border-radius: 4px; } +.health-good { background: #5A8C64; } +.health-mid { background: var(--yellow); } +.health-low { background: var(--red); } \ No newline at end of file diff --git a/talas-wiki/templates/base.html b/talas-wiki/templates/base.html index 14dedc4..c635749 100644 --- a/talas-wiki/templates/base.html +++ b/talas-wiki/templates/base.html @@ -5,8 +5,12 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{template "title" .}} + + + - + + @@ -68,7 +72,19 @@ - +