Commit avant changement d'agent vers devstral

This commit is contained in:
2025-11-13 17:00:47 +01:00
parent a09b73e4f1
commit cc1d6880a7
25 changed files with 2903 additions and 89 deletions

View File

@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"html/template"
@ -16,6 +17,7 @@ import (
yaml "gopkg.in/yaml.v3"
"github.com/mathieu/personotes/internal/i18n"
"github.com/mathieu/personotes/internal/indexer"
)
@ -39,15 +41,17 @@ type Handler struct {
idx *indexer.Indexer
templates *template.Template
logger *log.Logger
i18n *i18n.Translator
}
// NewHandler construit un handler unifié pour l'API.
func NewHandler(notesDir string, idx *indexer.Indexer, tpl *template.Template, logger *log.Logger) *Handler {
func NewHandler(notesDir string, idx *indexer.Indexer, tpl *template.Template, logger *log.Logger, translator *i18n.Translator) *Handler {
return &Handler{
notesDir: notesDir,
idx: idx,
templates: tpl,
logger: logger,
i18n: translator,
}
}
@ -55,6 +59,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
h.logger.Printf("%s %s", r.Method, path)
// I18n endpoint - serve translation files
if strings.HasPrefix(path, "/api/i18n/") {
h.handleI18n(w, r)
return
}
// REST API v1 endpoints
if strings.HasPrefix(path, "/api/v1/notes") {
h.handleRESTNotes(w, r)
@ -278,7 +288,7 @@ func (h *Handler) handleHome(w http.ResponseWriter, r *http.Request) {
}
// Générer le contenu Markdown avec la liste de toutes les notes
content := h.generateHomeMarkdown()
content := h.generateHomeMarkdown(r)
// Utiliser le template editor.html pour afficher la page d'accueil
data := struct {
@ -317,12 +327,12 @@ func (h *Handler) handleAbout(w http.ResponseWriter, r *http.Request) {
}
// generateHomeMarkdown génère le contenu Markdown de la page d'accueil
func (h *Handler) generateHomeMarkdown() string {
func (h *Handler) generateHomeMarkdown(r *http.Request) string {
var sb strings.Builder
// En-tête
sb.WriteString("# 📚 Index\n\n")
sb.WriteString("_Mise à jour automatique • " + time.Now().Format("02/01/2006 à 15:04") + "_\n\n")
sb.WriteString("_" + h.t(r, "home.autoUpdate") + " • " + time.Now().Format("02/01/2006 à 15:04") + "_\n\n")
// Construire l'arborescence
tree, err := h.buildFileTree()
@ -339,15 +349,15 @@ func (h *Handler) generateHomeMarkdown() string {
h.generateTagsSection(&sb)
// Section des favoris (après les tags)
h.generateFavoritesSection(&sb)
h.generateFavoritesSection(&sb, r)
// Section des notes récemment modifiées (après les favoris)
h.generateRecentNotesSection(&sb)
h.generateRecentNotesSection(&sb, r)
// Section de toutes les notes avec accordéon
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('all-notes')\">\n")
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\">📂 Toutes les notes (%d)</h2>\n", noteCount))
sb.WriteString(fmt.Sprintf(" <h2 class=\"home-section-title\">📂 %s (%d)</h2>\n", h.t(r, "home.allNotes"), noteCount))
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-all-notes\">\n")
@ -390,7 +400,7 @@ func (h *Handler) generateTagsSection(sb *strings.Builder) {
}
// generateFavoritesSection génère la section des favoris avec arborescence dépliable
func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
func (h *Handler) generateFavoritesSection(sb *strings.Builder, r *http.Request) {
favorites, err := h.loadFavorites()
if err != nil || len(favorites.Items) == 0 {
return
@ -398,7 +408,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('favorites')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">⭐ Favoris</h2>\n")
sb.WriteString(" <h2 class=\"home-section-title\">⭐ " + h.t(r, "favorites.title") + "</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-favorites\">\n")
sb.WriteString(" <div class=\"note-tree favorites-tree\">\n")
@ -423,7 +433,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
} else {
// Fichier
sb.WriteString(fmt.Sprintf(" <div class=\"file indent-level-1\">\n"))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", fav.Path))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">", fav.Path))
sb.WriteString(fmt.Sprintf("📄 %s", fav.Title))
sb.WriteString("</a>\n")
sb.WriteString(fmt.Sprintf(" </div>\n"))
@ -436,7 +446,7 @@ func (h *Handler) generateFavoritesSection(sb *strings.Builder) {
}
// generateRecentNotesSection génère la section des notes récemment modifiées
func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
func (h *Handler) generateRecentNotesSection(sb *strings.Builder, r *http.Request) {
recentDocs := h.idx.GetRecentDocuments(5)
if len(recentDocs) == 0 {
@ -445,7 +455,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
sb.WriteString("<div class=\"home-section\">\n")
sb.WriteString(" <div class=\"home-section-header\" onclick=\"toggleFolder('recent')\">\n")
sb.WriteString(" <h2 class=\"home-section-title\">🕒 Récemment modifiés</h2>\n")
sb.WriteString(" <h2 class=\"home-section-title\">🕒 " + h.t(r, "home.recentlyModified") + "</h2>\n")
sb.WriteString(" </div>\n")
sb.WriteString(" <div class=\"home-section-content\" id=\"folder-recent\">\n")
sb.WriteString(" <div class=\"recent-notes-container\">\n")
@ -464,7 +474,7 @@ func (h *Handler) generateRecentNotesSection(sb *strings.Builder) {
}
sb.WriteString(" <div class=\"recent-note-card\">\n")
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">\n", doc.Path))
sb.WriteString(fmt.Sprintf(" <a href=\"#\" class=\"recent-note-link\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">\n", doc.Path))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-title\">%s</div>\n", doc.Title))
sb.WriteString(fmt.Sprintf(" <div class=\"recent-note-meta\">\n"))
sb.WriteString(fmt.Sprintf(" <span class=\"recent-note-date\">📅 %s</span>\n", dateStr))
@ -527,7 +537,7 @@ func (h *Handler) generateFavoriteFolderContent(sb *strings.Builder, folderPath
// Fichier markdown
displayName := strings.TrimSuffix(name, ".md")
sb.WriteString(fmt.Sprintf("%s<div class=\"file %s\">\n", indent, indentClass))
sb.WriteString(fmt.Sprintf("%s <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\">", indent, relativePath))
sb.WriteString(fmt.Sprintf("%s <a href=\"#\" hx-get=\"/api/notes/%s\" hx-target=\"#editor-container\" hx-swap=\"innerHTML\" hx-push-url=\"true\">", indent, relativePath))
sb.WriteString(fmt.Sprintf("📄 %s", displayName))
sb.WriteString("</a>\n")
sb.WriteString(fmt.Sprintf("%s</div>\n", indent))
@ -1484,3 +1494,60 @@ func (h *Handler) generateFolderViewMarkdown(folderPath string) string {
return sb.String()
}
// getLanguage extrait la langue préférée depuis les cookies ou Accept-Language header
func (h *Handler) getLanguage(r *http.Request) string {
// 1. Vérifier le cookie
if cookie, err := r.Cookie("language"); err == nil && cookie.Value != "" {
return cookie.Value
}
// 2. Vérifier l'en-tête Accept-Language
acceptLang := r.Header.Get("Accept-Language")
if acceptLang != "" {
// Parse simple: prendre le premier code de langue
parts := strings.Split(acceptLang, ",")
if len(parts) > 0 {
lang := strings.Split(parts[0], ";")[0]
lang = strings.Split(lang, "-")[0] // "fr-FR" -> "fr"
return strings.TrimSpace(lang)
}
}
// 3. Par défaut: anglais
return "en"
}
// t est un helper pour traduire une clé dans la langue de la requête
func (h *Handler) t(r *http.Request, key string, args ...map[string]string) string {
lang := h.getLanguage(r)
return h.i18n.T(lang, key, args...)
}
// handleI18n sert les fichiers de traduction JSON pour le frontend
func (h *Handler) handleI18n(w http.ResponseWriter, r *http.Request) {
// Extraire le code de langue depuis l'URL: /api/i18n/en ou /api/i18n/fr
lang := strings.TrimPrefix(r.URL.Path, "/api/i18n/")
if lang == "" {
lang = "en"
}
// Récupérer les traductions pour cette langue
translations, ok := h.i18n.GetTranslations(lang)
if !ok {
// Fallback vers l'anglais si la langue n'existe pas
translations, ok = h.i18n.GetTranslations("en")
if !ok {
http.Error(w, "translations not found", http.StatusNotFound)
return
}
}
// Retourner le JSON
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(translations); err != nil {
h.logger.Printf("error encoding translations: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}