diff --git a/cmd/server/main.go b/cmd/server/main.go index 606a8f8..a4784a9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -78,18 +78,59 @@ func main() { http.ServeFile(w, r, filepath.Join(webDir, "index.html")) }) r.Get(base+"/*", func(w http.ResponseWriter, r *http.Request) { - // strip base path + // O BASE_PATH é apenas o prefixo público: /initpage/apps/ aponta para + // /app/web/apps/, sem repetir "initpage" dentro da pasta web. p := strings.TrimPrefix(r.URL.Path, base) - if p == "" { - p = "/" + rel := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(p, "/"))) + serveIndex := func(indexPath string) bool { + st, err := os.Stat(indexPath) + if err != nil || st.IsDir() { + return false + } + http.ServeFile(w, r, indexPath) + return true } - // tenta servir arquivo - full := filepath.Join(webDir, filepath.Clean(strings.TrimPrefix(p, "/"))) - if st, err := os.Stat(full); err == nil && !st.IsDir() { + + if rel == "." { + serveIndex(filepath.Join(webDir, "index.html")) + return + } + if !filepath.IsLocal(rel) { + http.NotFound(w, r) + return + } + + full := filepath.Join(webDir, rel) + st, err := os.Stat(full) + if err == nil && st.IsDir() { + if !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently) + return + } + if serveIndex(filepath.Join(full, "index.html")) { + return + } + } else if err == nil { http.StripPrefix(base, fileServer).ServeHTTP(w, r) return } - http.ServeFile(w, r, filepath.Join(webDir, "index.html")) + + // Em rotas sem extensão, usa o index.html da pasta mais próxima. + // Isso permite recarregar rotas internas de SPAs estáticas em web//. + if filepath.Ext(rel) == "" { + dir := filepath.Dir(rel) + for { + if serveIndex(filepath.Join(webDir, dir, "index.html")) { + return + } + parent := filepath.Dir(dir) + if parent == dir || dir == "." { + break + } + dir = parent + } + } + http.NotFound(w, r) }) log.Printf("Servidor rodando em :%s base=%s", cfg.Port, base) diff --git a/internal/sync/sync.go b/internal/sync/sync.go index a955ad4..38dd88f 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "net/http" + "strconv" "time" mw "github.com/weltonmoura/initpage/internal/middleware" @@ -34,7 +35,7 @@ func (s *Sync) Get(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"data":` + data + `,"version":` + itoa(version) + `}`)) + w.Write([]byte(`{"data":` + data + `,"version":` + strconv.Itoa(version) + `}`)) } func (s *Sync) Put(w http.ResponseWriter, r *http.Request) { @@ -70,22 +71,3 @@ func (s *Sync) Put(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"ok":true}`)) } - -func itoa(i int) string { - if i == 0 { - return "0" - } - sign := "" - if i < 0 { - sign = "-" - i = -i - } - b := [12]byte{} - n := len(b) - for i > 0 { - n-- - b[n] = byte('0' + i%10) - i /= 10 - } - return sign + string(b[n:]) -} diff --git a/web/app.js b/web/app.js index 6081cd3..2c3d48e 100644 --- a/web/app.js +++ b/web/app.js @@ -99,6 +99,7 @@ const API_BASE = "api"; let authUser = null; let syncTimer = null; let pendingCloudData = null; +let applyingCloudData = false; // Drag & Drop let draggedShortcutId = null; @@ -251,6 +252,9 @@ function saveState() { console.warn("Falha ao salvar dados locais:", err); } try { updateTasksBadge(); } catch (e) {} + // Mantém localStorage como cópia offline, mas trata a nuvem como fonte + // principal sempre que houver uma sessão autenticada. + if (authUser && !applyingCloudData) scheduleSync(); } // ------------------------------------------------------------- @@ -1452,16 +1456,17 @@ window.addEventListener("DOMContentLoaded", () => { }); // Widget de Calendário Integrado ao Relógio / Saudação - const clockTrigger = document.getElementById("clock-calendar-trigger"); - clockTrigger.addEventListener("click", (e) => { - if (e.target.id === "user-display-name") { + document.querySelectorAll("#clock-calendar-trigger, #clock-calendar-sidebar-trigger").forEach(clockTrigger => { + clockTrigger.addEventListener("click", (e) => { + if (e.target.id === "user-display-name") { + e.stopPropagation(); + openSettingsModal(); + document.getElementById("setting-user-name").focus(); + return; + } e.stopPropagation(); - openSettingsModal(); - document.getElementById("setting-user-name").focus(); - return; - } - e.stopPropagation(); - toggleCalendarWidget(); + toggleCalendarWidget(); + }); }); // Navegação do Calendário @@ -2176,7 +2181,7 @@ async function checkAuth() { if (data.logged) { authUser = data; updateSyncUI(); - pullSync(); + await pullSync(); } else { authUser = null; updateSyncUI(); @@ -2214,33 +2219,53 @@ function updateSyncUI() { } async function pushSync() { - if (!authUser) return; + if (!authUser) return false; try { - await fetch(API_BASE + "/sync", { + const res = await fetch(API_BASE + "/sync", { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: state, version: Date.now() }) }); + if (!res.ok) throw new Error(`sync upload failed: ${res.status}`); const badge = document.getElementById("sync-badge"); if (badge) { badge.innerText = "Sincronizado"; badge.classList.remove("hidden"); } - } catch (e) {} + return true; + } catch (e) { + const badge = document.getElementById("sync-badge"); + if (badge) { + badge.innerText = "Pendente"; + badge.classList.remove("hidden"); + } + return false; + } } async function pullSync() { - if (!authUser) return; + if (!authUser) return false; try { const res = await fetch(API_BASE + "/sync", { credentials: "include" }); + if (!res.ok) throw new Error(`sync download failed: ${res.status}`); const json = await res.json(); if (json.data) { - pendingCloudData = json.data; - const m = document.getElementById("modal-sync-merge"); - if (m) m.classList.remove("hidden"); + // Para uma conta conectada, os dados online prevalecem sobre a cópia local. + applyCloudData(json.data); + } else { + // Conta nova: preserva os dados locais existentes ao criar a cópia online. + if (!await pushSync()) return false; } - } catch (e) {} + return true; + } catch (e) { + const badge = document.getElementById("sync-badge"); + if (badge) { + badge.innerText = "Offline"; + badge.classList.remove("hidden"); + } + return false; + } } function applyCloudData(data) { @@ -2249,7 +2274,9 @@ function applyCloudData(data) { if (!Array.isArray(state.categories) || state.categories.length === 0) { state.categories = [...DEFAULT_CONFIG.categories]; } + applyingCloudData = true; saveState(); + applyingCloudData = false; applyAllSettings(); renderTabs(); renderShortcuts(); @@ -2257,6 +2284,7 @@ function applyCloudData(data) { if (document.getElementById("modal-sync-merge")) document.getElementById("modal-sync-merge").classList.add("hidden"); showToast("Dados da nuvem aplicados!", "cloud"); } catch (e) { + applyingCloudData = false; showToast("Erro ao aplicar dados da nuvem", "alert-circle"); } } @@ -2318,7 +2346,6 @@ function closeAuthModal() { closeAuthModal(); await checkAuth(); showToast("Logado com sucesso!", "log-in"); - pullSync(); } catch (ex) { errEl.innerText = "Erro de conexão"; errEl.classList.remove("hidden"); @@ -2348,7 +2375,6 @@ function closeAuthModal() { closeAuthModal(); await checkAuth(); showToast("Conta criada e logada!", "user-plus"); - scheduleSync(); } catch (ex) { errEl.innerText = "Erro de conexão"; errEl.classList.remove("hidden"); @@ -2367,8 +2393,8 @@ function closeAuthModal() { document.getElementById("btn-sync-now")?.addEventListener("click", async () => { if (!authUser) return; showToast("Sincronizando...", "refresh-cw"); - await pushSync(); - showToast("Sincronização enviada!", "cloud"); + const synced = await pullSync(); + showToast(synced ? "Sincronização concluída!" : "Não foi possível sincronizar", synced ? "cloud" : "alert-circle"); }); document.getElementById("modal-sync-merge")?.addEventListener("click", (e) => { @@ -2385,8 +2411,8 @@ function closeAuthModal() { document.getElementById("btn-sync-upload")?.addEventListener("click", async () => { document.getElementById("modal-sync-merge").classList.add("hidden"); showToast("Enviando dados locais...", "upload-cloud"); - await pushSync(); - showToast("Dados enviados para a nuvem!", "cloud"); + const synced = await pushSync(); + showToast(synced ? "Dados enviados para a nuvem!" : "Não foi possível sincronizar", synced ? "cloud" : "alert-circle"); }); checkAuth(); diff --git a/web/index.html b/web/index.html index 4db9df3..b2c1ada 100644 --- a/web/index.html +++ b/web/index.html @@ -80,8 +80,8 @@ -