Browse Source

WIP

websockets-2025
Edward Ribeiro 4 weeks ago
parent
commit
6c577e494c
  1. 12
      docker/docker-compose.yaml
  2. 58
      frontend/src/__apps/painel/main.js
  3. 301
      sapl/templates/painel/painel_v2.html

12
docker/docker-compose.yaml

@ -1,6 +1,6 @@
services:
sapldb:
image: postgres:10.5-alpine
image: postgres:16-bookworm
restart: always
container_name: postgres
labels:
@ -36,11 +36,11 @@ services:
image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no"]
sapl:
image: interlegis/sapl:3.1.164-RC5
# build:
# context: ../
# dockerfile: ./docker/Dockerfile
# container_name: sapl
# image: interlegis/sapl:3.1.164-RC5
build:
context: ../
dockerfile: ./docker/Dockerfile
container_name: sapl
labels:
NAME: "sapl"
restart: always

58
frontend/src/__apps/painel/main.js

@ -1,5 +1,6 @@
import './scss/painel.scss'
// main.js (Vue 2)
import Vue from 'vue'
new Vue({
@ -7,8 +8,61 @@ new Vue({
delimiters: ['[[', ']]'],
data() {
return {
parlamentares: [{ nome: 'zé' }, { nome: 'maria' }, { nome: 'antonio' }]
controllerId: null,
state: {
sessao_plenaria: '',
sessao_plenaria_data: '',
sessao_plenaria_hora_inicio: '',
votacao: [],
parlamentares: []
},
ws: null,
isOpen: false,
error: null,
}
},
mounted() { console.log('Vue mounted'); }
mounted() {
// $el is guaranteed here
const el = this.$el
// prefer data-attr; fallback to global if you set it
this.controllerId = el.dataset.controllerId || window.controllerId
console.log(`ControllerId: ${this.controllerId}`)
this.connectWS()
},
beforeDestroy() { this.closeWS() },
methods: {
wsUrl() {
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
return `${proto}://${location.host}/ws/painel/${this.controllerId}/`
},
connectWS() {
const url = this.wsUrl()
this.ws = new WebSocket(url)
this.ws.addEventListener('open', () => { this.isOpen = true; this.error = null })
this.ws.addEventListener('message', (evt) => {
try {
const msg = JSON.parse(evt.data)
// SETUP STATE
console.log(`FROM VUEJS: ${evt.data}`) // DEBUG
this.state.parlamentares = msg.presentes
//TODO: group in a single sessao object
this.state.sessao_plenaria = msg.sessao.sessao_plenaria
this.state.sessao_plenaria_data = msg.sessao.sessao_plenaria_data
this.state.sessao_plenaria_hora_inicio = msg.sessao.sessao_plenaria_hora_inicio
this.state.brasao = msg.sessao.brasao
this.state.oradores = msg.oradores
this.state.materia_legislativa_ementa = msg.mensagem_legislativa_ementa
this.state.resultado = msg.votacao
if (msg.type === 'state') this.state = msg.payload || {}
} catch (e) { console.warn('WS parse error:', e) }
})
this.ws.addEventListener('close', () => { this.isOpen = false; setTimeout(() => this.connectWS(), 800) })
this.ws.addEventListener('error', (e) => { this.error = e })
},
closeWS() { try { this.ws && this.ws.close() } catch (_) {} },
startStopwatch() { if (this.isOpen) this.ws.send(JSON.stringify({ type:'notify', stopwatch:'start' })) },
}
})

301
sapl/templates/painel/painel_v2.html

@ -39,63 +39,64 @@
</head>
<body class="painel-principal">
<!--<div id="display">00:00:00.00</div>-->
<!--<button id="startBtn">Start</button>-->
<!--<button id="stopBtn">Stop</button>-->
<!--<button id="resumeBtn">Resume</button>-->
<!--<button id="resetBtn">Reset</button>-->
{% block vue_content %}
<audio type="hidden" id="alarm" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<style>[v-cloak]{display:none}</style>
<div id="painel" v-cloak class="col text-center" data-controller-id="{{ controller_id }}">
<div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title"></h1>
</div>
<audio type="hidden" id="alarm" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<div class="row ">
<div class="col text-center">
<span id="sessao_plenaria_data" class="text-value"></span>
<div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title">[[ state.sessao_plenaria ]] </h1>
</div>
<div class="col text-center">
<span id="sessao_plenaria_hora_inicio" class="text-value"></span>
</div>
</div>
{% block vue_content %}
<div id="painel" class="col text-center">
<div v-for="parlamentar in parlamentares" class="text-value">
<strong>[[ parlamentar.nome ]]</strong>
<div class="row ">
<div class="col text-center">
<span id="sessao_plenaria_data" class="text-value">[[ state.sessao_plenaria_data ]] </span>
</div>
<div class="col text-center">
<span id="sessao_plenaria_hora_inicio" class="text-value"> [[ state.sessao_plenaria_data ]] </span>
</div>
</div>
</div>
{% endblock %}
<div class="row justify-content-center">
<div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/>
<div class="row justify-content-center">
<div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/>
</div>
</div>
</div>
<div class="row justify-content-center">
<h2 class="text-danger"><span id="message"></span></h2>
</div>
<div class="row">
<div class="col text-center"><span class="text-value data-hora" id="date"></span></div>
<div class="col text-center"><span class="text-value data-hora" id="relogio"></span></div>
</div>
<div class="row justify-content-center">
<div class="row justify-content-center">
<h2 class="text-danger"><span id="message"></span></h2>
</div>
<div class="row">
<div class="col text-center"><span class="text-value data-hora" id="date"></span></div>
<div class="col text-center"><span class="text-value data-hora" id="relogio"></span></div>
</div>
<div class="">
<div class="d-flex justify-content-start">
<div class="col-md-4">
<div class="text-center painel">
<h2 class="text-subtitle">Parlamentares</h2>
<span id="parlamentares" class="text-value text-center"></span>
<table id="parlamentares_list">
<tr v-for="p in state.parlamentares" :key="p.parlamentar_id" class="text-value text-center">
<td style="padding-right:20px; color:yellow" > [[ p.nome_parlamentar ]]</td>
<td style="padding-right:20px; color:yellow"> [[ p.filiacao ]]</td>
<td style="padding-right:20px; color:yellow"></td>
</tr>
</table>
</div>
</div>
<div class="d-flex col-md-8 painels">
<div class="col-md-6 text-center painel" id="aparecer_oradores">
<h2 class="text-subtitle">Oradores</h2>
<span id="orador"></span>
<table id="oradores_list">
<tr v-for="o in state.oradores" :key="o.ordem_pronunciamento"><td style="padding-right:20px; color:white">
[[ o.ordem_pronunciamento ]]º &nbsp [[ o.nome_parlamentar ]]</td>
</tr>
</table>
</div>
<div class="col-md-6 text-left painel">
@ -136,7 +137,7 @@
<h2 class="text-subtitle" id="mat_em_votacao">Matéria em Votação</h2>
<span id="materia_legislativa_texto" class="text-value"></span>
<br>
<span id="materia_legislativa_ementa" class="text-value"></span>
<span id="materia_legislativa_ementa" class="text-value">[[ materia_legislativa_ementa ]] </span>
<br>
<span id="observacao_materia" class="text-value"></span>
</div>
@ -148,7 +149,8 @@
</div>
</div>
</div>
</div>
{% endblock %}
</body>
{% block webpack_loader_js %}
{% render_chunk_vendors 'js' %}
@ -158,227 +160,4 @@
{% block webpack_loader_chunks_js %}
{% endblock webpack_loader_chunks_js %}
<script>
$(document).ready(function() {
const controllerId = "{{ controller_id }}";
const proto = location.protocol === "https:" ? "wss" : "ws";
let ws, backoff = 500, connTimer;
const url = `${proto}://${location.host}/ws/painel/${controllerId}/`
function update_view(data) {
console.log(data)
let sessao = data.sessao
// DADOS SESSAO
$("#sessao_plenaria").text(sessao.sessao_plenaria)
$("#sessao_plenaria_data").text("Data Início: " + sessao.sessao_plenaria_data)
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + sessao.sessao_plenaria_hora_inicio)
$("#sessao_solene_tema").text(sessao.tema_solene)
// PANEL CONFIG
if (sessao.status_painel === false) {
$("#message").text("PAINEL ENCONTRA-SE FECHADO");
}
else {
$("#message").text("");
}
if (sessao.sessao_solene){
$("#resultado_votacao_div").hide();
$("#obs_materia_div").hide();
$('#tema_solene_div').show();
}
if (sessao.brasao != null)
$("#logo-painel").attr("src", sessao.brasao);
// PARLAMENTARES
var presentes_list = data.presentes;
var presentes = $("#parlamentares");
presentes.children().remove();
console.log(presentes_list.length);
presentes.append('<table id="parlamentares_list">');
$.each(presentes_list, function (index, parlamentar) {
$('#parlamentares_list').append('<tr><td style="padding-right:20px; color:yellow" >' +
parlamentar.nome_parlamentar +
'</td> <td style="padding-right:20px; color:yellow">' +
parlamentar.filiacao + '</td> <td style="padding-right:20px; color:yellow">'
+ '</td></tr>');
});
// VOTOS
var votos = data.votacao;
var votacao = $("#votacao");
// retornar o total no JSON
let total_votos = votos["sim"] + votos["não"] + votos["abstencoes"]
votacao.append("<li>Sim: " + votos["sim"] + "</li>");
votacao.append("<li>Não: " + votos["não"] + "</li>");
votacao.append("<li>Abstenções: " + votos["abstencoes"] + "</li>");
votacao.append("<li>Presentes: " + presentes_list.length + "</li>");
votacao.append("<li>Total votos: " + total_votos + "</li>");
// ORADORES
var oradores_list = data.oradores;
var oradores = $("#orador")
oradores.children().remove();
if (oradores_list.length > 0){
$('#aparecer_oradores').show();
oradores.append('<table id="oradores_list">');
$.each(oradores_list, function (index, orador) {
$('#oradores_list').append('<tr><td style="padding-right:20px; color:white" >' +
orador.ordem_pronunciamento + 'º &nbsp' +
orador.nome_parlamentar +'</td></tr>')
});
oradores.append('</table>');
}
else {
$('#aparecer_oradores').hide();
}
// Matéria
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]);
$("#materia_legislativa_ementa").text(data["materia_legislativa_ementa"]);
}
function connect() {
console.log(`Connecting to ${url}...`);
const ws = new WebSocket(url);
let lastPong = Date.now(), pingTimer;
// When the connection is established
ws.onopen = function () {
console.log("✅ WebSocket connected");
// Optionally send an initial message to the server
ws.send(JSON.stringify({ type: "echo", message: "Client connected" }));
// Ping keep-alive
pingTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type:"ping", ts:Date.now()}));
if (Date.now() - lastPong > 75000) ws.close(); // force reconnect
}, 30000);
};
ws.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
console.log(data);
if (data.type === "pong") lastPong = Date.now();
if (data.type === "data") {
console.log("📩 Message from server:", data);
update_view(data);
}
if (data.type == "notify") {
console.log("command from backend:", data);
if (data.stopwatch === "start") {
startTimer();
} else if (data.stopwatch === "stop") {
stopTimer();
} else if (data.stopwatch === "resume") {
resumeTimer();
} else if (data.stopwatch === "reset") {
resetTimer();
}
}
} catch (e) {
console.error(e);
}
};
// When the connection closes
ws.onclose = function (event) {
console.log("❌ WebSocket closed:", event);
clearInterval(pingTimer);
clearTimeout(connTimer);
// retry with capped exponential backoff
connTimer = setTimeout(connect, Math.min(backoff, 10000));
backoff *= 2;
};
// When an error occurs
ws.onerror = function (error) {
console.error("⚠️ WebSocket error:", event);
};
}
let startTime = 0;
let elapsed = 0;
let timer = null;
let durationMs = 1 * 60 * 1000; // <-- initial time (1 minutes)
let remaining = durationMs;
let alarmPlayed = false;
$("#cronometro_discurso").text(formatTime(durationMs));
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');
const minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
const centis = String(Math.floor((ms % 1000) / 10)).padStart(2, '0');
return `${hours}:${minutes}:${seconds}.${centis}`;
}
function updateDisplay() {
const now = Date.now();
const diff = remaining - (now - startTime);
const display = Math.max(0, diff);
$("#cronometro_discurso").text(formatTime(display));
if (diff <= 0 && !alarmPlayed) {
alarmPlayed = true;
$("#alarm")[0].play();
clearInterval(timer);
timer = null;
}
}
function startTimer() {
console.log("start");
if (timer) return;
alarmPlayed = false;
startTime = Date.now();
timer = setInterval(updateDisplay, 10);
}
function stopTimer() {
if (!timer) return;
clearInterval(timer);
timer = null;
remaining -= Date.now() - startTime;
}
function resumeTimer() {
if (timer) return;
startTime = Date.now();
timer = setInterval(updateDisplay, 10);
}
function resetTimer() {
clearInterval(timer);
timer = null;
remaining = durationMs;
alarmPlayed = false;
$("#cronometro_discurso").text(formatTime(remaining));
}
// Bind buttons
/*
$("#startBtn").click(startTimer);
$("#stopBtn").click(stopTimer);
$("#resumeBtn").click(resumeTimer);
$("#resetBtn").click(resetTimer);
*/
// ENTRYPOINT
connect();
})
</script>
</html>
Loading…
Cancel
Save