rotas no back
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
http.ServeFile(w, r, indexPath)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
// tenta servir arquivo
|
|
||||||
full := filepath.Join(webDir, filepath.Clean(strings.TrimPrefix(p, "/")))
|
if rel == "." {
|
||||||
if st, err := os.Stat(full); err == nil && !st.IsDir() {
|
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)
|
||||||
|
|||||||
@@ -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:])
|
|
||||||
}
|
|
||||||
|
|||||||
74
web/app.js
74
web/app.js
@@ -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,16 +1456,17 @@ 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();
|
||||||
|
openSettingsModal();
|
||||||
|
document.getElementById("setting-user-name").focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openSettingsModal();
|
toggleCalendarWidget();
|
||||||
document.getElementById("setting-user-name").focus();
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.stopPropagation();
|
|
||||||
toggleCalendarWidget();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Navegação do Calendário
|
// Navegação do Calendário
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
} 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) {
|
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();
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
25
web/sw.js
25
web/sw.js
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user