mmpSearch/assets/js/creations/server/server.js

657 lines
27 KiB
JavaScript

// server.js - VERSÃO 6 (ACK + Clock Sync + Logs Dinâmicos + Persistência Autoritativa) - HTTPS ENABLED
// =========================
// IMPORTS PRINCIPAIS
// =========================
const express = require("express");
const https = require("https");
const { Server } = require("socket.io");
const fs = require("fs");
const path = require("path");
const pino = require("pino");
const DEFAULT_PROJECT_XML = `<?xml version="1.0"?>
<!DOCTYPE lmms-project>
<lmms-project version="1.0" type="song" creatorversion="1.2.2" creator="LMMS">
<head mastervol="100" masterpitch="0" timesig_denominator="4" timesig_numerator="4" bpm="140"/>
<song>
<trackcontainer visible="1" y="5" minimized="0" height="300" type="song" x="5" maximized="0" width="600">
<track muted="0" name="TripleOscillator" type="0" solo="0">
<instrumenttrack pitchrange="1" fxch="0" usemasterpitch="1" vol="100" pitch="0" pan="0" basenote="57">
<instrument name="tripleoscillator">
<tripleoscillator wavetype1="0" pan2="0" finer1="0" finel1="0" userwavefile0="" wavetype0="0" userwavefile1="" finer0="0" stphdetun1="0" vol0="33" wavetype2="0" coarse0="0" finel2="0" finer2="0" modalgo2="2" finel0="0" coarse1="-12" phoffset1="0" pan0="0" modalgo3="2" coarse2="-24" phoffset2="0" pan1="0" vol1="33" phoffset0="0" stphdetun0="0" modalgo1="2" vol2="33" stphdetun2="0" userwavefile2=""/>
</instrument>
<eldata fres="0.5" fwet="0" ftype="0" fcut="14000">
<elvol ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
<elcut ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
<elres ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
</eldata>
<chordcreator chordrange="1" chord-enabled="0" chord="0"/>
<arpeggiator arpcycle="0" arptime_denominator="4" arpgate="100" arpdir="0" arpmode="0" arptime_syncmode="0" arp="0" arpmiss="0" arp-enabled="0" arptime="100" arprange="1" arpskip="0" arptime_numerator="4"/>
<midiport inputchannel="0" outputcontroller="0" fixedoutputvelocity="-1" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" inputcontroller="0" readable="0" fixedoutputnote="-1" basevelocity="63" writable="0"/>
<fxchain enabled="0" numofeffects="0"/>
</instrumenttrack>
</track>
<track muted="0" name="Sample track" type="2" solo="0">
<sampletrack vol="100" pan="0">
<fxchain enabled="0" numofeffects="0"/>
</sampletrack>
</track>
<track muted="0" name="Beat/Bassline 0" type="1" solo="0">
<bbtrack>
<trackcontainer visible="0" y="5" minimized="0" height="400" type="bbtrackcontainer" x="610" maximized="0" width="700">
<track muted="0" name="Kicker" type="0" solo="0">
<instrumenttrack pitchrange="1" fxch="0" usemasterpitch="1" vol="100" pitch="0" pan="0" basenote="57">
<instrument name="kicker">
<kicker decay="440" version="1" noise="0" endfreq="40" decay_syncmode="0" decay_denominator="4" startfreq="150" env="0.163" startnote="1" dist="0.8" slope="0.06" decay_numerator="4" click="0.4" distend="0.8" gain="1" endnote="0"/>
</instrument>
<eldata fres="0.5" fwet="0" ftype="0" fcut="14000">
<elvol ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
<elcut ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
<elres ctlenvamt="0" lspd_syncmode="0" x100="0" lpdel="0" latt="0" pdel="0" lspd="0.1" lshp="0" hold="0.5" lspd_numerator="4" userwavefile="" att="0" lamt="0" dec="0.5" lspd_denominator="4" sustain="0.5" rel="0.1" amt="0"/>
</eldata>
<chordcreator chordrange="1" chord-enabled="0" chord="0"/>
<arpeggiator arpcycle="0" arptime_denominator="4" arpgate="100" arpdir="0" arpmode="0" arptime_syncmode="0" arp="0" arpmiss="0" arp-enabled="0" arptime="100" arprange="1" arpskip="0" arptime_numerator="4"/>
<midiport inputchannel="0" outputcontroller="0" fixedoutputvelocity="-1" outputchannel="1" fixedinputvelocity="-1" outputprogram="1" inputcontroller="0" readable="0" fixedoutputnote="-1" basevelocity="63" writable="0"/>
<fxchain enabled="0" numofeffects="0"/>
</instrumenttrack>
<pattern pos="0" muted="0" name="Kicker" steps="16" type="0"/>
</track>
</trackcontainer>
</bbtrack>
</track>
<track muted="0" name="Automation track" type="5" solo="0">
<automationtrack/>
</track>
</trackcontainer>
<track muted="0" name="Automation track" type="6" solo="0">
<automationtrack/>
<automationpattern pos="0" len="192" name="Numerator" prog="0" tens="1" mute="0"/>
<automationpattern pos="0" len="192" name="Denominator" prog="0" tens="1" mute="0"/>
<automationpattern pos="0" len="192" name="Tempo" prog="0" tens="1" mute="0"/>
<automationpattern pos="0" len="192" name="Master volume" prog="0" tens="1" mute="0"/>
<automationpattern pos="0" len="192" name="Master pitch" prog="0" tens="1" mute="0"/>
</track>
<fxmixer visible="1" y="310" minimized="0" height="333" x="5" maximized="0" width="543">
<fxchannel volume="1" muted="0" num="0" name="Master" soloed="0">
<fxchain enabled="0" numofeffects="0"/>
</fxchannel>
</fxmixer>
<ControllerRackView visible="1" y="310" minimized="0" height="200" x="680" maximized="0" width="350"/>
<pianoroll visible="0" y="5" minimized="0" height="480" x="5" maximized="0" width="860"/>
<automationeditor visible="0" y="1" minimized="0" height="400" x="1" maximized="0" width="860"/>
<projectnotes visible="0" y="10" minimized="0" height="400" x="700" maximized="0" width="679"><![CDATA[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.5pt; font-weight:400; font-style:normal;">
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>]]></projectnotes>
<timeline lp1pos="192" lpstate="0" lp0pos="0"/>
<controllers/>
</song>
</lmms-project>`;
// -------------------------
// LOGGER DINÂMICO (GERENCIADOR)
// -------------------------
// Vamos manter um mapa de loggers ativos, um para cada sala
const loggersByRoom = new Map();
// Função para gerar um timestamp formatado para nome de arquivo
function getTimestampForFile() {
const d = new Date();
const date = d.toISOString().split("T")[0]; // YYYY-MM-DD
// HH-MM-SS (formato 24h, fuso local)
const time = d.toTimeString().split(" ")[0].replace(/:/g, "-");
return `${date}_${time}`; // ex: 2025-10-26_12-30-05
}
// Função principal: Pega o logger de uma sala ou cria um novo
function getActionLoggerForRoom(roomName) {
// 1. Se já criamos um logger para esta sala, reutilize-o
if (loggersByRoom.has(roomName)) {
return loggersByRoom.get(roomName);
}
// 2. Se for a primeira vez, crie um novo logger
const timestamp = getTimestampForFile();
// Limpa o nome da sala para evitar caracteres inválidos no nome do arquivo
const safeRoomName = roomName.replace(/[^a-z0-9_-]/gi, "_");
const fileName = `${timestamp}_${safeRoomName}.log`;
const filePath = path.join(process.cwd(), "data", fileName);
console.log(`[Logger] Novo log de sessão iniciado: ${filePath}`);
// Garante que a pasta 'data' exista
fs.mkdirSync(path.join(process.cwd(), "data"), { recursive: true });
// Cria a instância do pino para este arquivo específico
const newLogger = pino(pino.destination(filePath));
// Armazena no mapa para reutilização
loggersByRoom.set(roomName, newLogger);
return newLogger;
}
// -------------------------
// Configuração do Servidor (HTTPS)
// -------------------------
const app = express();
const PORT = process.env.PORT || 33001;
// ====== HTTPS (Opção B) ======
const CERT_FULLCHAIN = process.env.SSL_FULLCHAIN || "/etc/letsencrypt/live/alice.ufsj.edu.br/fullchain.pem";
const CERT_PRIVKEY = process.env.SSL_PRIVKEY || "/etc/letsencrypt/live/alice.ufsj.edu.br/privkey.pem";
if (!fs.existsSync(CERT_FULLCHAIN) || !fs.existsSync(CERT_PRIVKEY)) {
console.error("[HTTPS] Certificados não encontrados.\n" +
` fullchain: ${CERT_FULLCHAIN}\n` +
` privkey : ${CERT_PRIVKEY}\n` +
"Defina SSL_FULLCHAIN/SSL_PRIVKEY ou instale os certificados no caminho padrão.");
process.exit(1);
}
const httpsOptions = {
cert: fs.readFileSync(CERT_FULLCHAIN),
key : fs.readFileSync(CERT_PRIVKEY),
};
const httpsServer = https.createServer(httpsOptions, app);
// -------------------------
// Persistência por sala
// -------------------------
/*
Estrutura na memória:
roomStates = {
[roomName]: {
projectXml: '<xml...>' | undefined,
audio: { tracks: [], clips: [] },
seq: number,
tokensSeen: Set<string> // idempotência
}
}
*/
const roomStates = {}; // compat com V4 (já existia), ampliado para incluir audio/seq
function dataFile(roomName) {
return path.join(process.cwd(), "data", `${roomName}.json`);
}
function ensureRoom(roomName) {
if (roomStates[roomName]) return roomStates[roomName];
// tenta carregar do disco
try {
const p = dataFile(roomName);
if (fs.existsSync(p)) {
const j = JSON.parse(fs.readFileSync(p, "utf8"));
roomStates[roomName] = {
// 🔥 CORREÇÃO: Fallback para DEFAULT_PROJECT_XML se o JSON salvo estiver sem XML
projectXml: j.projectXml || DEFAULT_PROJECT_XML,
audio: j.audio || { tracks: [], clips: [] },
seq: j.seq || 0,
tokensSeen: new Set(),
};
return roomStates[roomName];
}
} catch (e) {
console.warn(`[persist] falha ao carregar estado da sala ${roomName}:`, e);
}
// Cria sala NOVA na memória
roomStates[roomName] = {
// 🔥 CORREÇÃO: Inicia com o XML do LMMS completo, não mais null
projectXml: DEFAULT_PROJECT_XML,
audio: { tracks: [], clips: [] },
seq: 0,
tokensSeen: new Set(),
};
return roomStates[roomName];
}
function saveRoom(roomName) {
const r = ensureRoom(roomName);
try {
fs.mkdirSync(path.join(process.cwd(), "data"), { recursive: true });
fs.writeFileSync(
dataFile(roomName),
JSON.stringify({ projectXml: r.projectXml, audio: r.audio, seq: r.seq })
);
} catch (e) {
console.warn(`[persist] falha ao salvar estado da sala ${roomName}:`, e);
}
}
// -------------------------
// Server / IO
// -------------------------
const io = new Server(httpsServer, {
cors: {
// CORREÇÃO: Simplificado para a origem real do cliente, que é onde o HTML é servido.
// O cliente provavelmente é servido a partir de https://alice.ufsj.edu.br (porta 443).
origin: "https://alice.ufsj.edu.br",
methods: ["GET", "POST"],
credentials: true,
},
});
console.log("Backend V6 (Logs Dinâmicos) iniciado. Aguardando conexões...");
// util de log das salas deste socket
function logMyRooms(socket, prefix = "SALAS") {
const myRooms = Array.from(socket.rooms || []);
console.log(`[${prefix}] socket ${socket.id} está em:`, myRooms);
}
// -------------------------
// REDUCER autoritativo do editor de áudio
// -------------------------
function basenameNoExt(p) {
if (!p) return "";
const base = String(p).split(/[\\/]/).pop();
return base.replace(/\.[^/.]+$/, "");
}
function applyAuthoritativeAction(roomName, action) {
const room = ensureRoom(roomName);
const state = room.audio;
// idempotência por token (se fornecido)
if (action.__token && room.tokensSeen.has(action.__token)) {
return null;
}
if (action.__token) room.tokensSeen.add(action.__token);
let mutated = false;
switch (action.type) {
case "ADD_AUDIO_LANE": {
// Usa o trackId enviado pelo cliente, se existir
const id = action.trackId || generateUniqueId("track_server");
if (!state.tracks.find((t) => t.id === id)) {
state.tracks.push({
id,
name: `Pista de Áudio ${state.tracks.length + 1}`,
});
mutated = true;
}
break;
}
case "ADD_AUDIO_CLIP": {
const { clipId, filePath, trackId, startTimeInSeconds, name } = action;
// Blindagem do Servidor (Evitar faixas fantasma)
if (
!trackId ||
clipId == null ||
startTimeInSeconds == null ||
isNaN(startTimeInSeconds)
) {
console.warn(
`[Server] Ação ADD_AUDIO_CLIP rejeitada (dados inválidos):`,
action
);
return null; // Rejeita a ação, não faz broadcast
}
if (!state.tracks.find((t) => t.id === trackId)) {
state.tracks.push({
id: trackId,
name: `Pista de Áudio ${state.tracks.length + 1}`,
});
mutated = true;
}
if (!state.clips.find((c) => String(c.id) === String(clipId))) {
state.clips.push({
id: clipId,
trackId,
name: name || basenameNoExt(filePath) || "sample",
sourcePath: filePath || null, // ideal: URL pública
startTimeInSeconds,
durationInSeconds: action.durationInSeconds || 0,
offset: action.offset || 0,
pitch: action.pitch || 0,
volume: action.volume ?? 1,
pan: action.pan ?? 0,
originalDuration: action.originalDuration || 0,
});
mutated = true;
}
break;
}
case "UPDATE_AUDIO_CLIP": {
if (!action.clipId || !action.props) {
console.warn(
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (dados base inválidos):`,
action
);
return null;
}
const { trackId, startTimeInSeconds } = action.props;
if (
trackId !== undefined &&
(trackId == null || (typeof trackId === "number" && isNaN(trackId)))
) {
console.warn(
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (trackId inválido):`,
action
);
return null;
}
if (
startTimeInSeconds !== undefined &&
(startTimeInSeconds == null || isNaN(startTimeInSeconds))
) {
console.warn(
`[Server] Ação UPDATE_AUDIO_CLIP rejeitada (startTimeInSeconds inválido):`,
action
);
return null;
}
const c = state.clips.find((x) => String(x.id) === String(action.clipId));
if (c && action.props && typeof action.props === "object") {
Object.assign(c, action.props);
mutated = true;
}
break;
}
case "REMOVE_AUDIO_CLIP": {
const i = state.clips.findIndex(
(x) => String(x.id) === String(action.clipId)
);
if (i >= 0) {
state.clips.splice(i, 1);
mutated = true;
}
break;
}
case "REMOVE_AUDIO_LANE_BY_ID": {
// 1. Acha e remove a Track
const idx = state.tracks.findIndex((t) => t.id === action.trackId);
if (idx !== -1) {
state.tracks.splice(idx, 1);
mutated = true;
}
// 2. LIMPEZA PROFUNDA: Remove clips dessa track E clips órfãos (trackId null)
// Se removemos a track, os clips dela têm que sumir.
const prevCount = state.clips.length;
state.clips = state.clips.filter((c) => {
// Mantém apenas se tiver trackId E se esse trackId ainda existir na lista de tracks
const parentTrackExists = state.tracks.some(t => t.id === c.trackId);
return c.trackId && parentTrackExists;
});
if (state.clips.length !== prevCount) {
mutated = true;
}
break;
}
default:
// outras ações não são da persistência do editor de áudio
mutated = false;
}
if (!mutated) return null;
room.seq += 1; // numeração autoritativa
saveRoom(roomName); // persistência em disco
return { ...action, __seq: room.seq }; // devolve ação com seq para broadcast
}
io.on("connection", (socket) => {
console.log(`Novo usuário conectado: ${socket.id}`);
// =======================================
// JOIN ROOM
// =======================================
socket.on("join_room", async (data) => {
const { roomName, userName } = data || {};
if (!roomName) {
console.warn(`join_room inválido de ${socket.id} (sem roomName)`);
return;
}
await socket.join(roomName);
console.log(
`Usuário ${userName || "(sem nome)"} (${
socket.id
}) entrou na sala: ${roomName}`
);
logMyRooms(socket, "JOIN");
// envia estado salvo do projeto (XML) — compat V4
const room = ensureRoom(roomName);
if (room.projectXml) {
socket.emit("load_project_state", room.projectXml);
console.log(
`Estado XML enviado para ${userName || socket.id} na sala ${roomName}`
);
} else {
console.log(`Sala ${roomName} sem XML salvo ainda.`);
}
// envia snapshot autoritativo do editor de áudio
socket.emit("action_broadcast", {
action: {
type: "AUDIO_SNAPSHOT",
snapshot: room.audio,
__seq: room.seq,
__target: socket.id,
},
});
socket
.to(roomName)
.emit("feedback", `Usuário ${userName || socket.id} entrou na sala.`);
});
// =======================================
// CLOCK SYNC
// =======================================
socket.on("what_time_is_it", (_data, cb) => {
cb && cb({ serverNowMs: Date.now() });
});
// =======================================
// BROADCAST COM ACK + PERSISTÊNCIA
// =======================================
socket.on("broadcast_action", async (payload, cb) => {
const { roomName, action } = payload || {};
if (!roomName || !action) {
cb && cb({ ok: false, error: "invalid_payload" });
console.warn(
`[broadcast_action] payload inválido de ${socket.id}:`,
payload
);
return;
}
// 👇 *** NOSSO NOVO LOG DINÂMICO *** 👇
try {
// 1. Pega o logger específico para ESTA sala
const roomLogger = getActionLoggerForRoom(roomName);
// 2. Loga a ação (não precisamos mais salvar roomName, já está no nome do arq.)
roomLogger.info(
{
timestamp: Date.now(),
socketId: socket.id,
action: action,
},
"action_received"
);
} catch (e) {
console.warn(`[Logger] Falha ao logar ação para sala ${roomName}:`, e);
}
// *** FIM DO LOG ***
// Confirma recebimento imediatamente
cb && cb({ ok: true, token: action.__token });
// Garante que o emissor está na sala (caso o join tenha falhado/atrasado)
if (!socket.rooms.has(roomName)) {
console.warn(
`[broadcast_action] ${socket.id} NÃO estava na sala ${roomName}. Forçando join...`
);
await socket.join(roomName);
logMyRooms(socket, "FORCED_JOIN");
}
// Carrega estado da sala
const room = ensureRoom(roomName);
// Persiste estado do Pattern (Notas/Sequenciador)
// Aceita tanto carregamento total quanto atualização de notas
if ((action.type === "LOAD_PROJECT" || action.type === "SYNC_PATTERN_STATE") && action.xml) {
// Proteção: Não salva se o XML for vazio (evita corromper a sala com o bug antigo)
if (action.xml.trim().length > 0) {
room.projectXml = action.xml;
saveRoom(roomName);
console.log(
`[broadcast_action] XML da sala ${roomName} atualizado via ${action.type}.`
);
} else {
console.warn(`[Server] Ignorando XML vazio vindo de ${action.type}`);
}
}
// Aplica ações autoritativas do editor de áudio
const maybeApplied = applyAuthoritativeAction(roomName, action);
// Debug: quem está na sala neste momento?
const socketsInRoom = await io.in(roomName).fetchSockets();
const idsInRoom = socketsInRoom.map((s) => s.id);
console.log(
`[broadcast_action] ${action.type} (token=${action.__token}) para sala "${roomName}" com ${idsInRoom.length} cliente(s):`,
idsInRoom
);
// Se foi uma ação do editor de áudio e foi aplicada, rebroadcast com __seq
if (maybeApplied) {
io.to(roomName).emit("action_broadcast", { action: maybeApplied });
return;
}
// Caso contrário, apenas repercute a ação como antes (sem __seq)
io.to(roomName).emit("action_broadcast", { action });
});
// =======================================
// Retrocompat (daw_action)
// =======================================
socket.on("daw_action", async (data) => {
const { room, action } = data || {};
if (!room || !action) return;
console.warn(
"[DEPRECATION] Recebido daw_action. Migre para broadcast_action com ACK."
);
// (Opcional, mas recomendado) Adicionando log dinâmico aqui também
try {
const roomLogger = getActionLoggerForRoom(room);
roomLogger.info(
{
timestamp: Date.now(),
socketId: socket.id,
action: action,
},
"action_received (deprecated daw_action)"
);
} catch (e) {
console.warn(`[Logger] Falha ao logar daw_action para sala ${room}:`, e);
}
if (!socket.rooms.has(room)) {
console.warn(
`[daw_action] ${socket.id} NÃO estava na sala ${room}. Forçando join...`
);
await socket.join(room);
logMyRooms(socket, "FORCED_JOIN(daw)");
}
const r = ensureRoom(room);
if (action.type === "LOAD_PROJECT" && action.xml) {
r.projectXml = action.xml;
saveRoom(room);
console.log(
`[daw_action] Estado da sala ${room} atualizado (LOAD_PROJECT).`
);
}
const socketsInRoom = await io.in(room).fetchSockets();
const idsInRoom = socketsInRoom.map((s) => s.id);
console.log(
`[daw_action] ${action.type} para sala "${room}" (${idsInRoom.length} cliente(s)):`,
idsInRoom
);
io.in(room).emit("action_broadcast", { action });
});
// =======================================
// Re-sync explícito (quando cliente detectar buraco de seq)
// =======================================
socket.on("audio_resync", ({ roomName, lastSeq }) => {
const room = ensureRoom(roomName);
// caminho simples: envia snapshot atual (estado autoritativo)
socket.emit("action_broadcast", {
action: {
type: "AUDIO_SNAPSHOT",
snapshot: room.audio,
__seq: room.seq,
__target: socket.id,
},
});
});
socket.on("disconnect", () => {
console.log(`Usuário desconectado: ${socket.id}`);
});
});
// --- ENDPOINT DE NOTIFICAÇÃO EXTERNA ---
app.post("/notify-update", express.json(), (req, res) => {
const { updateType } = req.body;
if (updateType === "samples") {
// 1. Emitir o evento para TODAS as salas/clientes.
// O evento deve ser algo que seu ui.js entenda.
io.emit("system_update", {
type: "RELOAD_SAMPLES",
message: "Novo Sample/Project adicionado. Recarregando o navegador de arquivos...",
});
console.log(
"[Notificação] Evento 'RELOAD_SAMPLES' emitido para todos os clientes."
);
return res.status(200).send({ success: true, message: "Notificação de Samples/Projetos enviada." });
}
res.status(400).send({ success: false, message: "updateType inválido." });
});
app.get("/", (req, res) => {
res.send(
"Servidor Backend V6 (Logs Dinâmicos) da DAW colaborativa está no ar!"
);
});
// ====== START ======
httpsServer.listen(PORT, () => {
console.log(`Servidor escutando na porta https://localhost:${PORT}`);
});