rotas no back

This commit is contained in:
2026-10-22 00:11:59 -03:00
parent 94920e9825
commit c68d12f4a7
5 changed files with 126 additions and 54 deletions

View File

@@ -78,18 +78,59 @@ func main() {
http.ServeFile(w, r, filepath.Join(webDir, "index.html")) http.ServeFile(w, r, filepath.Join(webDir, "index.html"))
}) })
r.Get(base+"/*", func(w http.ResponseWriter, r *http.Request) { 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) p := strings.TrimPrefix(r.URL.Path, base)
if p == "" { rel := filepath.Clean(filepath.FromSlash(strings.TrimPrefix(p, "/")))
p = "/" serveIndex := func(indexPath string) bool {
st, err := os.Stat(indexPath)
if err != nil || st.IsDir() {
return false
} }
// tenta servir arquivo http.ServeFile(w, r, indexPath)
full := filepath.Join(webDir, filepath.Clean(strings.TrimPrefix(p, "/"))) return true
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) http.StripPrefix(base, fileServer).ServeHTTP(w, r)
return 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/<site>/.
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) log.Printf("Servidor rodando em :%s base=%s", cfg.Port, base)

View File

@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"time" "time"
mw "github.com/weltonmoura/initpage/internal/middleware" mw "github.com/weltonmoura/initpage/internal/middleware"
@@ -34,7 +35,7 @@ func (s *Sync) Get(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "application/json") 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) { 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.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"ok":true}`)) 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:])
}

View File

@@ -99,6 +99,7 @@ const API_BASE = "api";
let authUser = null; let authUser = null;
let syncTimer = null; let syncTimer = null;
let pendingCloudData = null; let pendingCloudData = null;
let applyingCloudData = false;
// Drag & Drop // Drag & Drop
let draggedShortcutId = null; let draggedShortcutId = null;
@@ -251,6 +252,9 @@ function saveState() {
console.warn("Falha ao salvar dados locais:", err); console.warn("Falha ao salvar dados locais:", err);
} }
try { updateTasksBadge(); } catch (e) {} 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,7 +1456,7 @@ window.addEventListener("DOMContentLoaded", () => {
}); });
// Widget de Calendário Integrado ao Relógio / Saudação // Widget de Calendário Integrado ao Relógio / Saudação
const clockTrigger = document.getElementById("clock-calendar-trigger"); document.querySelectorAll("#clock-calendar-trigger, #clock-calendar-sidebar-trigger").forEach(clockTrigger => {
clockTrigger.addEventListener("click", (e) => { clockTrigger.addEventListener("click", (e) => {
if (e.target.id === "user-display-name") { if (e.target.id === "user-display-name") {
e.stopPropagation(); e.stopPropagation();
@@ -1463,6 +1467,7 @@ window.addEventListener("DOMContentLoaded", () => {
e.stopPropagation(); e.stopPropagation();
toggleCalendarWidget(); toggleCalendarWidget();
}); });
});
// Navegação do Calendário // Navegação do Calendário
document.getElementById("cal-prev-month").addEventListener("click", (e) => { document.getElementById("cal-prev-month").addEventListener("click", (e) => {
@@ -2176,7 +2181,7 @@ async function checkAuth() {
if (data.logged) { if (data.logged) {
authUser = data; authUser = data;
updateSyncUI(); updateSyncUI();
pullSync(); await pullSync();
} else { } else {
authUser = null; authUser = null;
updateSyncUI(); updateSyncUI();
@@ -2214,33 +2219,53 @@ function updateSyncUI() {
} }
async function pushSync() { async function pushSync() {
if (!authUser) return; if (!authUser) return false;
try { try {
await fetch(API_BASE + "/sync", { const res = await fetch(API_BASE + "/sync", {
method: "PUT", method: "PUT",
credentials: "include", credentials: "include",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: state, version: Date.now() }) 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"); const badge = document.getElementById("sync-badge");
if (badge) { if (badge) {
badge.innerText = "Sincronizado"; badge.innerText = "Sincronizado";
badge.classList.remove("hidden"); 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() { async function pullSync() {
if (!authUser) return; if (!authUser) return false;
try { try {
const res = await fetch(API_BASE + "/sync", { credentials: "include" }); 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(); const json = await res.json();
if (json.data) { if (json.data) {
pendingCloudData = json.data; // Para uma conta conectada, os dados online prevalecem sobre a cópia local.
const m = document.getElementById("modal-sync-merge"); applyCloudData(json.data);
if (m) m.classList.remove("hidden"); } else {
// Conta nova: preserva os dados locais existentes ao criar a cópia online.
if (!await pushSync()) return false;
}
return true;
} catch (e) {
const badge = document.getElementById("sync-badge");
if (badge) {
badge.innerText = "Offline";
badge.classList.remove("hidden");
}
return false;
} }
} catch (e) {}
} }
function applyCloudData(data) { function applyCloudData(data) {
@@ -2249,7 +2274,9 @@ function applyCloudData(data) {
if (!Array.isArray(state.categories) || state.categories.length === 0) { if (!Array.isArray(state.categories) || state.categories.length === 0) {
state.categories = [...DEFAULT_CONFIG.categories]; state.categories = [...DEFAULT_CONFIG.categories];
} }
applyingCloudData = true;
saveState(); saveState();
applyingCloudData = false;
applyAllSettings(); applyAllSettings();
renderTabs(); renderTabs();
renderShortcuts(); renderShortcuts();
@@ -2257,6 +2284,7 @@ function applyCloudData(data) {
if (document.getElementById("modal-sync-merge")) document.getElementById("modal-sync-merge").classList.add("hidden"); if (document.getElementById("modal-sync-merge")) document.getElementById("modal-sync-merge").classList.add("hidden");
showToast("Dados da nuvem aplicados!", "cloud"); showToast("Dados da nuvem aplicados!", "cloud");
} catch (e) { } catch (e) {
applyingCloudData = false;
showToast("Erro ao aplicar dados da nuvem", "alert-circle"); showToast("Erro ao aplicar dados da nuvem", "alert-circle");
} }
} }
@@ -2318,7 +2346,6 @@ function closeAuthModal() {
closeAuthModal(); closeAuthModal();
await checkAuth(); await checkAuth();
showToast("Logado com sucesso!", "log-in"); showToast("Logado com sucesso!", "log-in");
pullSync();
} catch (ex) { } catch (ex) {
errEl.innerText = "Erro de conexão"; errEl.innerText = "Erro de conexão";
errEl.classList.remove("hidden"); errEl.classList.remove("hidden");
@@ -2348,7 +2375,6 @@ function closeAuthModal() {
closeAuthModal(); closeAuthModal();
await checkAuth(); await checkAuth();
showToast("Conta criada e logada!", "user-plus"); showToast("Conta criada e logada!", "user-plus");
scheduleSync();
} catch (ex) { } catch (ex) {
errEl.innerText = "Erro de conexão"; errEl.innerText = "Erro de conexão";
errEl.classList.remove("hidden"); errEl.classList.remove("hidden");
@@ -2367,8 +2393,8 @@ function closeAuthModal() {
document.getElementById("btn-sync-now")?.addEventListener("click", async () => { document.getElementById("btn-sync-now")?.addEventListener("click", async () => {
if (!authUser) return; if (!authUser) return;
showToast("Sincronizando...", "refresh-cw"); showToast("Sincronizando...", "refresh-cw");
await pushSync(); const synced = await pullSync();
showToast("Sincronização enviada!", "cloud"); showToast(synced ? "Sincronização concluída!" : "Não foi possível sincronizar", synced ? "cloud" : "alert-circle");
}); });
document.getElementById("modal-sync-merge")?.addEventListener("click", (e) => { document.getElementById("modal-sync-merge")?.addEventListener("click", (e) => {
@@ -2385,8 +2411,8 @@ function closeAuthModal() {
document.getElementById("btn-sync-upload")?.addEventListener("click", async () => { document.getElementById("btn-sync-upload")?.addEventListener("click", async () => {
document.getElementById("modal-sync-merge").classList.add("hidden"); document.getElementById("modal-sync-merge").classList.add("hidden");
showToast("Enviando dados locais...", "upload-cloud"); showToast("Enviando dados locais...", "upload-cloud");
await pushSync(); const synced = await pushSync();
showToast("Dados enviados para a nuvem!", "cloud"); showToast(synced ? "Dados enviados para a nuvem!" : "Não foi possível sincronizar", synced ? "cloud" : "alert-circle");
}); });
checkAuth(); checkAuth();

View File

@@ -80,8 +80,8 @@
</div> </div>
<!-- Sidebar Lateral Direita (desktop) e Barra Inferior (mobile) --> <!-- Sidebar Lateral Direita (desktop) e Barra Inferior (mobile) -->
<aside id="app-sidebar" class="fixed right-0 top-auto bottom-0 w-full h-12 z-600 flex flex-row items-center justify-center px-2 gap-2 border-t border-white/10 sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:w-12 sm:h-auto sm:flex-col sm:justify-start sm:pt-4 sm:pb-4 sm:border-t-0 sm:border-l sm:border-white/10 sm:border-r-0"> <aside id="app-sidebar" class="fixed right-0 top-auto bottom-0 w-full h-12 z-50 flex flex-row items-center justify-center px-2 gap-2 border-t border-white/10 sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:w-12 sm:h-auto sm:flex-col sm:justify-start sm:pt-4 sm:pb-4 sm:border-t-0 sm:border-l sm:border-white/10 sm:border-r-0">
<button id="clock-calendar-trigger" title="Calendário" aria-label="Abrir Calendário" class="sidebar-btn relative group"> <button id="clock-calendar-sidebar-trigger" title="Calendário" aria-label="Abrir Calendário" class="sidebar-btn relative group">
<i data-lucide="calendar" class="w-6 h-6"></i> <i data-lucide="calendar" class="w-6 h-6"></i>
</button> </button>
<button id="btn-open-tasks" title="Tarefas" aria-label="Abrir Tarefas" class="sidebar-btn relative group"> <button id="btn-open-tasks" title="Tarefas" aria-label="Abrir Tarefas" class="sidebar-btn relative group">

View File

@@ -1,4 +1,4 @@
const CACHE = "initpage-v4"; const CACHE = "initpage-v6";
const ASSETS = [ const ASSETS = [
"./", "./",
"./index.html", "./index.html",
@@ -20,12 +20,35 @@ self.addEventListener("install", (event) => {
self.addEventListener("activate", (event) => { self.addEventListener("activate", (event) => {
event.waitUntil( event.waitUntil(
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
.then(() => caches.open(CACHE))
.then((cache) => cache.keys())
.then((requests) => Promise.all(requests.filter((request) => {
const url = new URL(request.url);
return /\/api(?:\/|$)/.test(url.pathname);
}).map((request) => caches.open(CACHE).then((cache) => cache.delete(request))))
); );
self.clients.claim(); self.clients.claim();
}); });
self.addEventListener("fetch", (event) => { self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return; if (event.request.method !== "GET") return;
const requestURL = new URL(event.request.url);
// Respostas autenticadas da API nunca devem entrar no cache compartilhado do SW.
if (requestURL.origin === self.location.origin && /\/api(?:\/|$)/.test(requestURL.pathname)) return;
// Consulte o backend em cada navegação para não reaproveitar a página inicial
// que o servidor pode ter enviado como fallback para uma rota de site estático.
if (event.request.mode === "navigate") {
const appHome = new URL("./", self.registration.scope);
event.respondWith(
fetch(event.request).catch(async () => {
const cachedHome = await caches.match(appHome.href);
return cachedHome || Response.error();
})
);
return;
}
event.respondWith( event.respondWith(
caches.match(event.request).then((cached) => { caches.match(event.request).then((cached) => {
const fetchPromise = fetch(event.request).then((response) => { const fetchPromise = fetch(event.request).then((response) => {