Browse Source

Websockets with pure JS

websockets-2025
Edward Ribeiro 2 months ago
parent
commit
acdb813c19
  1. 3
      RUNBOOK.txt
  2. 13
      frontend/src/__apps/painel/main.js
  3. 320
      sapl/painel/consumers.py
  4. 3
      sapl/painel/urls.py
  5. 36
      sapl/painel/views.py
  6. 90
      sapl/sessao/models.py
  7. 583
      sapl/templates/painel/painel_v2.html
  8. 12
      vue.config.js

3
RUNBOOK.txt

@ -0,0 +1,3 @@
docker run --rm -p 6379:6379 redis:7-alpine redis-server --save "" --appendonly no
daphne -b 127.0.0.1 -p 8000 sapl.asgi:application

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

@ -1 +1,14 @@
import './scss/painel.scss'
import Vue from 'vue'
new Vue({
el: '#painel',
delimiters: ['[[', ']]'],
data() {
return {
parlamentares: [{ nome: 'zé' }, { nome: 'maria' }, { nome: 'antonio' }]
}
},
mounted() { console.log('Vue mounted'); }
})

320
sapl/painel/consumers.py

@ -10,24 +10,53 @@ from django.db.models import Q
from sapl.base.models import CasaLegislativa, AppConfig
from sapl.sessao.models import SessaoPlenaria, OrdemDia, ExpedienteMateria, RegistroVotacao, RegistroLeitura, \
PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia
PresencaOrdemDia, SessaoPlenariaPresenca, OradorExpediente, VotoParlamentar, AbstractOrdemDia, SessaoPresencaView, \
SessaoOradorView, SessaoMateriaVotacaoView
log = logging.getLogger(__name__)
def get_dados_painel(pk: int) -> dict:
app_config = AppConfig.objects.first()
sessao = SessaoPlenaria.objects.get(id=pk)
casa = CasaLegislativa.objects.first()
app_config = AppConfig.objects.first()
if casa and app_config and (bool(casa.logotipo)):
brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None
ordem_dia = OrdemDia.objects.filter(sessao_plenaria_id=pk, votacao_aberta=True).last()
expediente = ExpedienteMateria.objects.filter(sessao_plenaria_id=pk, votacao_aberta=True).last()
dados_sessao = {
"type": "data",
brasao = casa.logotipo.url \
if app_config.mostrar_brasao_painel else None
# FILIACAO
# { 1: { "id": 1, "nome": "fulano", "filiacao": "aquela"}, ...}
# [ { "id": 1, "nome": "fulano", "filiacao": "aquela"}, ... ]
# Painel
presentes = SessaoPresencaView.objects.filter(sessao_plenaria_id=4984,
etapa_sessao='expediente').values_list('parlamentar_id',
'nome_parlamentar',
'filiacao', )
presentes = [dict(zip(['parlamentar_id', 'nome_parlamentar', 'filiacao'], p)) for p in presentes]
oradores = SessaoOradorView.objects.filter(sessao_plenaria_id=4983,
etapa_sessao='expediente').values_list('ordem_pronunciamento',
'nome_parlamentar',
)
oradores = [dict(zip(['ordem_pronunciamento', 'nome_parlamentar'], o)) for o in oradores]
votos = SessaoMateriaVotacaoView.objects.get(sessao_plenaria_id=4984, etapa_sessao='ordemdia', materia_id=4148)
# TODO: recover stopwatch state from DB/Cache
stopwatch = {
"type": "stopwatch.state",
"id": "sw:main",
"status": "running", # "running" | "paused" | "stopped"
"started_at_ms": 1699990000123, # epoch ms when (re)started
"elapsed_ms": 5320
}
dados_sessao = {
"type": "data",
"data": {}, # legacy
"sessao": {
"status_painel": sessao.painel_aberto,
"brasao": brasao,
"mostrar_voto": app_config.mostrar_voto,
@ -37,260 +66,20 @@ def get_dados_painel(pk: int) -> dict:
"sessao_solene": sessao.tipo.nome == "Solene",
"sessao_finalizada": sessao.finalizada,
"tema_solene": sessao.tema_solene,
# "cronometro_aparte": get_cronometro_status(request, "aparte"),
# "cronometro_discurso": get_cronometro_status(request, "discurso"),
# "cronometro_ordem": get_cronometro_status(request, "ordem"),
# "cronometro_consideracoes": get_cronometro_status(request, "consideracoes"),
}
# Caso tenha alguma matéria com votação aberta, ela é mostrada no painel
# com prioridade para Ordem Dia.
if ordem_dia:
dados_sessao.update(get_presentes(pk, ordem_dia))
dados_sessao.update(get_votos(ordem_dia, app_config.mostrar_voto))
elif expediente:
dados_sessao.update(get_presentes(pk, expediente))
dados_sessao.update(get_votos(expediente, app_config.mostrar_voto))
# Caso não tenha nenhuma aberta,
# a matéria a ser mostrada no Painel deve ser a última votada
last_ordem_voto = RegistroVotacao.objects.filter(
ordem__sessao_plenaria=sessao).order_by("data_hora").last()
last_expediente_voto = RegistroVotacao.objects.filter(
expediente__sessao_plenaria=sessao).order_by("data_hora").last()
last_ordem_leitura = RegistroLeitura.objects.filter(
ordem__sessao_plenaria=sessao).order_by("data_hora").last()
last_expediente_leitura = RegistroLeitura.objects.filter(
expediente__sessao_plenaria=sessao).order_by("data_hora").last()
# Obtém última matéria votada, através do timestamp mais recente
ordem_expediente = None
ultimo_timestamp = None
if last_ordem_voto:
ordem_expediente = last_ordem_voto.ordem
ultimo_timestamp = last_ordem_voto.data_hora
if (last_expediente_voto and ultimo_timestamp and last_expediente_voto.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_expediente_voto):
ordem_expediente = last_expediente_voto.expediente
ultimo_timestamp = last_expediente_voto.data_hora
if (last_ordem_leitura and ultimo_timestamp and last_ordem_leitura.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_ordem_leitura):
ordem_expediente = last_ordem_leitura.ordem
ultimo_timestamp = last_ordem_leitura.data_hora
if (last_expediente_leitura and ultimo_timestamp and last_expediente_leitura.data_hora > ultimo_timestamp) or \
(not ultimo_timestamp and last_expediente_leitura):
ordem_expediente = last_expediente_leitura.expediente
ultimo_timestamp = last_expediente_leitura.data_hora
# if ordem_expediente:
# dados_sessao.update(get_presentes(pk, ordem_expediente))
# dados_sessao.update(get_votos(ordem_expediente, app_config.mostrar_voto))
# Retorna que não há nenhuma matéria já votada ou aberta
dados_sessao.update({
'msg_painel': str('Nenhuma matéria disponivel para votação.')})
"status_painel": False, # TODO: recover from DB **and** move status to other place.
},
"presentes": presentes,
"oradores": oradores,
"votacao": votos.total_votos, # TODO unify into single json
"votos_parlamentar": votos.votos_parlamentares, # TODO: unify into single JSON
"materia_legislativa_ementa": votos.materia.ementa,
"stopwatch": stopwatch, # TODO: array of stopwatches
}
print(json.dumps(dados_sessao, indent=4))
return dados_sessao
def get_votos(materia: AbstractOrdemDia, mostrar_voto: bool):
logger = logging.getLogger(__name__)
if type(materia) == OrdemDia:
if materia.tipo_votacao != 4:
registro = RegistroVotacao.objects.filter(
ordem=materia, materia=materia.materia).order_by('data_hora').last()
leitura = None
else:
leitura = RegistroLeitura.objects.filter(
ordem=materia, materia=materia.materia).order_by('data_hora').last()
registro = None
tipo = 'ordem'
elif type(materia) == ExpedienteMateria:
if materia.tipo_votacao != 4:
registro = RegistroVotacao.objects.filter(
expediente=materia, materia=materia.materia).order_by('data_hora').last()
leitura = None
else:
leitura = RegistroLeitura.objects.filter(
expediente=materia, materia=materia.materia).order_by('data_hora').last()
registro = None
tipo = 'expediente'
response = {}
if not registro and not leitura:
response.update({
'numero_votos_sim': 0,
'numero_votos_nao': 0,
'numero_abstencoes': 0,
'registro': None,
'total_votos': 0,
'tipo_resultado': 'Ainda não foi votada.',
})
if materia.tipo_votacao == 2:
if tipo == 'ordem':
votos_parlamentares = VotoParlamentar.objects.filter(
ordem_id=materia.id).order_by(
'parlamentar__nome_parlamentar')
else:
votos_parlamentares = VotoParlamentar.objects.filter(
expediente_id=materia.id).order_by(
'parlamentar__nome_parlamentar')
for i, p in enumerate(response.get('presentes', [])):
try:
logger.info("Tentando obter votos do parlamentar (id={}).".format(p['parlamentar_id']))
voto = votos_parlamentares.get(parlamentar_id=p['parlamentar_id']).voto
if voto:
if mostrar_voto:
response['presentes'][i]['voto'] = voto
else:
response['presentes'][i]['voto'] = 'Voto Informado'
except ObjectDoesNotExist:
# logger.error("Votos do parlamentar (id={}) não encontrados. Retornado vazio."
# .format(p['parlamentar_id']))
response['presentes'][i]['voto'] = ''
elif leitura:
response.update({
'numero_votos_sim': 0,
'numero_votos_nao': 0,
'numero_abstencoes': 0,
'registro': True,
'total_votos': 0,
'tipo_resultado': 'Matéria lida.',
})
else:
total = (registro.numero_votos_sim +
registro.numero_votos_nao +
registro.numero_abstencoes)
if materia.tipo_votacao == 2:
votos_parlamentares = VotoParlamentar.objects.filter(
votacao_id=registro.id).order_by(
'parlamentar__nome_parlamentar')
for i, p in enumerate(response.get('presentes', [])):
try:
logger.debug("Tentando obter votos do parlamentar (id={}).".format(p['parlamentar_id']))
response['presentes'][i]['voto'] = votos_parlamentares.get(
parlamentar_id=p['parlamentar_id']).voto
except ObjectDoesNotExist:
logger.error(
"Votos do parlamentar (id={}) não encontrados. Retornado None.".format(p['parlamentar_id']))
response['presentes'][i]['voto'] = None
response.update({
'numero_votos_sim': registro.numero_votos_sim,
'numero_votos_nao': registro.numero_votos_nao,
'numero_abstencoes': registro.numero_abstencoes,
'registro': True,
'total_votos': total,
'tipo_resultado': registro.tipo_resultado_votacao.nome,
})
return response
def filiacao_data(parlamentar, data_inicio, data_fim=None):
from sapl.parlamentares.models import Filiacao
filiacoes_parlamentar = Filiacao.objects.filter(
parlamentar=parlamentar)
filiacoes = filiacoes_parlamentar.filter(Q(
data__lte=data_inicio,
data_desfiliacao__isnull=True) | Q(
data__lte=data_inicio,
data_desfiliacao__gte=data_inicio))
if data_fim:
filiacoes = filiacoes | filiacoes_parlamentar.filter(
data__gte=data_inicio,
data__lte=data_fim)
return ' | '.join([f.partido.sigla for f in filiacoes])
def get_presentes(pk: int, materia: AbstractOrdemDia):
if type(materia) == OrdemDia:
presentes = PresencaOrdemDia.objects.filter(
sessao_plenaria_id=pk)
else:
presentes = SessaoPlenariaPresenca.objects.filter(
sessao_plenaria_id=pk)
sessao = SessaoPlenaria.objects.get(id=pk)
num_presentes = len(presentes)
data_sessao = sessao.data_inicio
oradores = OradorExpediente.objects.filter(
sessao_plenaria_id=pk).order_by('numero_ordem')
oradores_list = []
for o in oradores:
oradores_list.append(
{
'nome': o.parlamentar.nome_parlamentar,
'numero': o.numero_ordem
})
presentes_list = []
for p in presentes:
legislatura = sessao.legislatura
# Recupera os mandatos daquele parlamentar
mandatos = p.parlamentar.mandato_set.filter(legislatura=legislatura)
if p.parlamentar.ativo and mandatos:
filiacao = filiacao_data(p.parlamentar, data_sessao, data_sessao)
if not filiacao:
partido = 'Sem Registro'
else:
partido = filiacao
presentes_list.append(
{'id': p.id,
'parlamentar_id': p.parlamentar.id,
'nome': p.parlamentar.nome_parlamentar,
'partido': partido,
'voto': ''
})
elif not p.parlamentar.ativo or not mandatos:
num_presentes += -1
response = {}
if materia:
if materia.tipo_votacao == 1:
tipo_votacao = 'Simbólica'
elif materia.tipo_votacao == 2:
tipo_votacao = 'Nominal'
elif materia.tipo_votacao == 3:
tipo_votacao = 'Secreta'
elif materia.tipo_votacao == 4:
tipo_votacao = 'Leitura'
response.update({
'tipo_resultado': materia.resultado,
'observacao_materia': html.unescape(materia.observacao),
'tipo_votacao': tipo_votacao,
'materia_legislativa_texto': str(materia.materia),
'materia_legislativa_ementa': str(materia.materia.ementa)
})
# presentes_list = sort_lista_chave(presentes_list, 'nome')
response.update({
'presentes': presentes_list,
'num_presentes': num_presentes,
'oradores': oradores_list,
'msg_painel': str('Votação aberta!'),
})
return response
class PainelConsumer(AsyncJsonWebsocketConsumer):
# def __init__(self):
@ -313,12 +102,15 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
self.controller_id = controller_id
self.group = f"controller_{controller_id}"
print(self.group)
await self.channel_layer.group_add(self.group, self.channel_name)
await self.accept()
# await self.send_json({"type": "data",
# "text": "Connection established!"})
print("SENDING DATA DO CONSUMER ")
print(get_dados_painel(controller_id))
await self.send_json(get_dados_painel(controller_id))
async def disconnect(self, code):
@ -328,7 +120,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
# Called by server via channel_layer.group_send
async def notify(self, event):
# event: {"type": "notify", "data": {...}}
await self.send_json(event["data"])
await self.send_json(event)
async def receive(self, text_data=None, bytes_data=None):
data = json.loads(text_data or "{}")
@ -337,7 +129,7 @@ class PainelConsumer(AsyncJsonWebsocketConsumer):
print("PING")
await self.send_json({"type": "pong", "ts": time.time()})
return
await self.send_json({"type": "data",
await self.send_json({"type": "echo",
"text": f"Echo: {data}"})
@database_sync_to_async

3
sapl/painel/urls.py

@ -3,7 +3,7 @@ from django.conf.urls import url
from .apps import AppConfig
from .views import (cronometro_painel, get_dados_painel, painel_mensagem_view,
painel_parlamentar_view, painel_view, painel_votacao_view,
switch_painel, verifica_painel, votante_view, websocket_view)
switch_painel, verifica_painel, votante_view, websocket_view, painel_controller_view)
from django.urls import path
@ -29,4 +29,5 @@ urlpatterns = [
# url(r'^painel', websocket_view, name='painel_websocket'),
path("painel/v2", websocket_view, name='painel_websocket'),
path("painel/controller", painel_controller_view, name='painel_controller'),
]

36
sapl/painel/views.py

@ -647,13 +647,37 @@ def websocket_view(request):
return render(request, "painel/painel_v2.html", context)
def push_to_me(request):
def painel_controller_view(request):
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
layer = get_channel_layer()
group = f"user_{request.user.pk}"
async_to_sync(layer.group_send)(
group, {"type": "notify", "data": {"text": "server says hi!"}}
)
print("called") # LOG!
command = request.GET["stopwatch"]
print("command", command)
if command:
layer = get_channel_layer()
controller_id = 4984 # TODO: recover from template call
group = f"controller_{controller_id}"
print(group)
async_to_sync(layer.group_send)(
group, {"type": "notify", "stopwatch": command}
)
# await self.channel_layer.group_add(self.group, self.channel_name)
return JsonResponse({"ok": True})
# def push_to_me(request):
# from asgiref.sync import async_to_sync
# from channels.layers import get_channel_layer
#
# layer = get_channel_layer()
# group = f"user_{request.user.pk}"
# async_to_sync(layer.group_send)(
# group, {"type": "notify", "data": {"text": "server says hi!"}}
# )
# return JsonResponse({"ok": True})

90
sapl/sessao/models.py

@ -1,6 +1,7 @@
from operator import xor
from django.core.exceptions import ValidationError
from django.contrib.postgres.fields.jsonb import JSONField
from django.db import models
from django.db.models import Q, F
from django.utils import timezone, formats
@ -1052,13 +1053,94 @@ class Correspondencia(models.Model):
return _('Correspondência: {}').format(self.documento)
class OradoresView(models.Model):
materia = models.ForeignKey(MateriaLegislativa, on_delete=models.DO_NOTHING)
##
## Painel V2
##
class SessaoPresencaView(models.Model):
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.DO_NOTHING,
verbose_name=_('Sessão Plenária'))
parlamentar = models.ForeignKey(Parlamentar,
on_delete=models.DO_NOTHING,
verbose_name=_('Parlamentar'))
etapa_sessao = models.CharField(max_length=15)
nome_parlamentar = models.CharField(max_length=50,
verbose_name=_('Nome Parlamentar'))
filiacao = models.CharField(max_length=50,
verbose_name=_('Filiacao'))
class Meta:
managed = False
db_table = "sessao_presenca_view"
ordering = ('-id',)
ordering = ('sessao_plenaria_id', 'etapa_sessao', 'nome_parlamentar')
def __str__(self):
return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.parlamentar}"
class SessaoOradorView(models.Model):
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.DO_NOTHING,
verbose_name=_('Sessão Plenária'))
parlamentar = models.ForeignKey(Parlamentar,
on_delete=models.DO_NOTHING,
verbose_name=_('Parlamentar'))
etapa_sessao = models.CharField(max_length=15)
nome_parlamentar = models.CharField(max_length=50,
verbose_name=_('Nome Parlamentar'))
ordem_pronunciamento = models.PositiveIntegerField(verbose_name=_('Ordem Pronunciamento'))
nome_parlamentar = models.CharField(max_length=50, verbose_name=_('Nome Parlamentar'))
filiacao = models.CharField(max_length=20, verbose_name=_('Sigla'))
class Meta:
managed = False
db_table = "sessao_orador_view"
ordering = ('sessao_plenaria_id', 'etapa_sessao', 'ordem_pronunciamento', 'nome_parlamentar')
def __str__(self):
return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.ordem_pronunciamento} - {self.parlamentar}"
class SessaoMateriaVotacaoView(models.Model):
sessao_plenaria = models.ForeignKey(SessaoPlenaria,
on_delete=models.DO_NOTHING,
verbose_name=_('Sessão Plenária'))
materia = models.ForeignKey(MateriaLegislativa,
on_delete=models.DO_NOTHING,
verbose_name=_('Matéria Legislativa'))
etapa_sessao = models.CharField(max_length=15)
numero_ordem = models.PositiveIntegerField(verbose_name=_('Número de Ordem'))
total_votos = JSONField(null=True, verbose_name=_('Total Votos'))
votos_parlamentares = JSONField(null=True, verbose_name=_('Votos Parlamentares'))
votacao_aberta = models.BooleanField(default=False)
tipo_votacao = models.PositiveIntegerField(verbose_name=_('Tipo Votação'))
tipo_votacao_descricao = models.CharField(max_length=15)
resultado = models.CharField(
max_length=256,
blank=True,
verbose_name=_('Resultado'))
resultado_votacao = models.CharField(
max_length=256,
blank=True,
verbose_name=_('Resultado Votação'))
class Meta:
managed = False
db_table = "sessao_materias_votacoes_view"
ordering = ('sessao_plenaria_id', 'etapa_sessao', 'numero_ordem')
def __str__(self):
return '{}/{}'.format(self.materia, self.tramitacao)
return f"{self.sessao_plenaria} - {self.etapa_sessao} - {self.tipo_votacao_descricao} - {self.total_votos} - {self.votos_parlamentares}"

583
sapl/templates/painel/painel_v2.html

@ -3,8 +3,10 @@
{% load render_bundle from webpack_loader %}
{% load webpack_static from webpack_loader %}
<!DOCTYPE HTML>
<!--[if IE 8]> <html class="no-js lt-ie9" lang="pt-br"> <![endif]-->
<!--[if IE 8]>
<html class="no-js lt-ie9" lang="pt-br"> <![endif]-->
<!--[if gt IE 8]><!-->
<html lang="pt-br">
<!--<![endif]-->
@ -13,12 +15,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- TODO: does it need this head_title here? -->
<title>{% block head_title %}{% trans 'SAPL - Sistema de Apoio ao Processo Legislativo' %}{% endblock %}</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% block webpack_loader_css %}
{% render_chunk_vendors 'css' %}
{% render_bundle 'global' 'css' %}
{% render_bundle 'painel' 'css' %}
{% endblock webpack_loader_css %}
{% block webpack_loader_css %}
{% render_chunk_vendors 'css' %}
{% render_bundle 'global' 'css' %}
{% render_bundle 'painel' 'css' %}
{% endblock webpack_loader_css %}
<style type="text/css">
@ -31,336 +34,214 @@
list-style-type: none;
}
}
</style>
</head>
<body class="painel-principal">
<audio type="hidden" id="audio" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title"></h1>
</div>
<div class="row ">
<div class="col text-center">
<span id="sessao_plenaria_data" class="text-value"></span>
<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>-->
<audio type="hidden" id="alarm" src="{% webpack_static 'audio/ring.mp3' %}"></audio>
<div class="d-flex justify-content-center">
<h1 id="sessao_plenaria" class="title text-title"></h1>
</div>
<div class="row ">
<div class="col text-center">
<span id="sessao_plenaria_data" class="text-value"></span>
</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>
</div>
{% endblock %}
<div class="row justify-content-center">
<div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/>
</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="">
<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>
</div>
</div>
<div class="col text-center">
<span id="sessao_plenaria_hora_inicio" class="text-value"></span>
</div>
</div>
<div class="row justify-content-center">
<div class="col-1">
<img src="" id="logo-painel" class="logo-painel" alt=""/>
<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>
</div>
<div class="col-md-6 text-left painel">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Cronômetros</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', 1)">
A+
</button>
</div>
<div class="text-value" id="box_cronometros">
Discurso: <span id="cronometro_discurso"></span><br>
Aparte: <span id="cronometro_aparte"></span><br>
Questão de Ordem: <span id="cronometro_ordem"></span><br>
Considerações Finais: <span id="cronometro_consideracoes"></span>
</div>
</div>
<div class="col-md-6 text-left painel" id="resultado_votacao_div">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Resultado</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', 1)">
A+
</button>
</div>
<div id="box_votacao">
<span id="votacao" class="text-value"></span>
<span id="resultado_votacao" lass="text-title"></span>
</div>
</div>
<div class="col-md-6 text-center painel" id="obs_materia_div">
<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>
<br>
<span id="observacao_materia" class="text-value"></span>
</div>
<div class="col-md-6 text-center painel" id="tema_solene_div" style="display: none">
<h2 class="text-subtitle">Tema da Sessão Solene</h2>
<span id="sessao_solene_tema" class="text-value"></span>
</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="">
<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>
</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>
</div>
<div class="col-md-6 text-left painel">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Cronômetros</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_cronometros', 1)">
A+
</button>
</div>
<div class="text-value" id="box_cronometros">
Discurso: <span id="cronometro_discurso"></span><br>
Aparte: <span id="cronometro_aparte"></span><br>
Questão de Ordem: <span id="cronometro_ordem"></span><br>
Considerações Finais: <span id="cronometro_consideracoes"></span>
</div>
</div>
<div class="col-md-6 text-left painel" id="resultado_votacao_div">
<div class="d-flex align-items-left justify-content-left mb-2">
<h2 class="text-subtitle mb-0">Resultado</h2>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', -1)">
A-
</button>
<button class="btn btn-sm btn-secondary ms-2" onclick="changeFontSize('box_votacao', 1)">
A+
</button>
</div>
<div id="box_votacao">
<span id="votacao" class="text-value"></span>
<span id="resultado_votacao" lass="text-title"></span>
</div>
</div>
<div class="col-md-6 text-center painel" id="obs_materia_div">
<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>
<br>
<span id="observacao_materia" class="text-value"></span>
</div>
<div class="col-md-6 text-center painel" id="tema_solene_div" style="display: none">
<h2 class="text-subtitle">Tema da Sessão Solene</h2>
<span id="sessao_solene_tema" class="text-value"></span>
</div>
</div>
</div>
</div>
</div>
</body>
{% block webpack_loader_js %}
{% render_chunk_vendors 'js' %}
{% render_bundle 'global' 'js' %}
{% render_bundle 'painel' 'js' %}
{% endblock webpack_loader_js %}
{% block webpack_loader_chunks_js %}
{% endblock webpack_loader_chunks_js %}
<script>
</div>
</div>
</div>
</body>
{% block webpack_loader_js %}
{% render_chunk_vendors 'js' %}
{% render_bundle 'global' 'js' %}
{% render_bundle 'painel' 'js' %}
{% endblock webpack_loader_js %}
{% 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, timer;
let ws, backoff = 500, connTimer;
const url = `${proto}://${location.host}/ws/painel/${controllerId}/`
function update_view(data) {
$("#sessao_plenaria").text(data["sessao_plenaria"])
$("#sessao_plenaria_data").text("Data Início: " + data["sessao_plenaria_data"])
$("#sessao_plenaria_hora_inicio").text("Hora Início: " + data["sessao_plenaria_hora_inicio"])
$("#sessao_solene_tema").text(data["tema_solene"])
if (data["status_painel"] == false) {
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 (data["sessao_solene"]){
if (sessao.sessao_solene){
$("#resultado_votacao_div").hide();
$("#obs_materia_div").hide();
$('#tema_solene_div').show();
}
if (data["brasao"] != null)
$("#logo-painel").attr("src", data["brasao"]);
if (sessao.brasao != null)
$("#logo-painel").attr("src", sessao.brasao);
// PARLAMENTARES
var presentes_list = data.presentes;
var presentes = $("#parlamentares");
var votacao = $("#votacao");
var oradores = $("#orador")
$("#votacao").text('');
presentes.children().remove();
votacao.children().remove();
oradores.children().remove();
var oradores_list = data["oradores"];
var presentes_list = data["presentes"];
if (data["status_painel"] == true) {
mostrar_voto = data["mostrar_voto"];
presentes.append('<table id="parlamentares_list">');
$.each(presentes_list, function (index, parlamentar) {
if (parlamentar.voto == 'Voto Informado' && mostrar_voto == false){
$('#parlamentares_list').append('<tr><td style="padding-right:20px; color:yellow" >' +
parlamentar.nome +
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.partido + '</td> <td style="padding-right:20px; color:yellow">'
+ '</td></tr>')
}
else{
$('#parlamentares_list').append(show_voto(parlamentar))
}
});
presentes.append('</table>')
if (data["oradores"].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.numero + 'º &nbsp' +
orador.nome +'</td></tr>')
});
oradores.append('</table>');
}
else {
$('#aparecer_oradores').hide();
}
}
else{
presentes.append('<span style="color:white" id="parlamentares_list">');
$('#parlamentares_list').append(
'<center>A listagem de parlamentares só aparecerá quando o painel estiver aberto.</center>')
presentes.append('</span>');
oradores.append('<span style="color:white" id="oradores_list">');
$('#oradores_list').append(
'<center>A listagem de oradores só aparecerá quando o painel estiver aberto.</center>')
oradores.append('</span>');
votacao.append('<span id="votacao">');
$("#votacao").append('<center>A votação só aparecerá quando o painel estiver aberto</center>');
votacao.append('</span>');
}
if(data["status_painel"]){
if (data['materia_legislativa_texto']){
var votacao = $("#votacao");
votacao.append("<li>Sim: " + data["numero_votos_sim"] + "</li>");
votacao.append("<li>Não: " + data["numero_votos_nao"] + "</li>");
votacao.append("<li>Abstenções: " + data["numero_abstencoes"] + "</li>");
votacao.append("<li>Presentes: " + data["num_presentes"] + "</li>");
votacao.append("<li>Total votos: " + data["total_votos"] + "</li>");
}
else{
$("#votacao").append('<center>Não há votação, pois não há nenhuma matéria aberta ou já votada.</center>');
}
}
var discurso_current = data["cronometro_discurso"];
if (!discurso_previous){
discurso_previous = ''
}
if (discurso_current != discurso_previous) {
$('#cronometro_discurso').runner(discurso_current);
discurso_previous = discurso_current;
}
var aparte_current = data["cronometro_aparte"];
if (!aparte_previous){
aparte_previous = ''
}
if (aparte_current != aparte_previous) {
$('#cronometro_aparte').runner(aparte_current);
aparte_previous = aparte_current;
}
var ordem_current = data["cronometro_ordem"];
if (!ordem_previous){
ordem_previous = ''
}
if (ordem_current != ordem_previous) {
$('#cronometro_ordem').runner(ordem_current);
ordem_previous = ordem_current;
}
var consideracoes_current = data["cronometro_consideracoes"];
if (!consideracoes_previous){
consideracoes_previous = ''
}
parlamentar.filiacao + '</td> <td style="padding-right:20px; color:yellow">'
+ '</td></tr>');
});
if (consideracoes_current != consideracoes_previous) {
$('#cronometro_consideracoes').runner(consideracoes_current);
consideracoes_previous = consideracoes_current;
}
if($('#cronometro_discurso').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
if($('#cronometro_aparte').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
// VOTOS
var votos = data.votacao;
var votacao = $("#votacao");
// retornar o total no JSON
let total_votos = votos["sim"] + votos["não"] + votos["abstencoes"]
if($('#cronometro_ordem').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
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>");
if($('#cronometro_consideracoes').runner('info').formattedTime == "00:00:30") {
audioAlertFinish.play();
}
// ORADORES
var oradores_list = data.oradores;
var oradores = $("#orador")
oradores.children().remove();
if(data['sessao_finalizada']){
$("#obs_materia_div").hide();
$("#resultado_votacao_div").hide();
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 if (data['materia_legislativa_texto']){
if (data["status_painel"] == true){
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]);
$("#materia_legislativa_ementa").text(data["materia_legislativa_ementa"]);
}
else{
$("#materia_legislativa_texto").text('A Matéria em votação só aparecerá quando o painel estiver aberto');
}
}
else{
$("#materia_legislativa_texto").text('Não há nenhuma matéria votada ou para votação.');
else {
$('#aparecer_oradores').hide();
}
if (data['observacao_materia'] && data["status_painel"] == true){
var texto = data['observacao_materia'];
if(texto.length > 151) {
$("#observacao_materia").text(texto.substr(0, 145).concat('(...)'));
}
else{
$("#observacao_materia").text(texto);
}
}
else{
$("#observacao_materia").text('');
}
if (data['tipo_resultado'] && data['status_painel'] == true){
if(data['tipo_votacao'] != 'Leitura' && !data['sessao_finalizada'] && !data["sessao_solene"]){
$("#resultado_votacao").css("color", "#45919D");
$("#mat_em_votacao").text("Matéria em Votação");
$("#resultado_votacao_div").show();
}
else{
$("#resultado_votacao_div").hide();
$("#mat_em_votacao").text("Matéria em Leitura");
}
console.log(data["tipo_resultado"], data['tipo_votacao']);
$("#resultado_votacao").text(data["tipo_resultado"]);
var resultado_votacao_upper = $("#resultado_votacao").text().toUpperCase();
console.log(resultado_votacao_upper, data['tipo_resultado']);
if (resultado_votacao_upper.search("APROV") != -1){
$("#resultado_votacao").css("color", "#7CFC00");
$("#mat_em_votacao").text("Matéria Votada");
}
else if (resultado_votacao_upper.search("REJEIT") != -1){
$("#resultado_votacao").css("color", "red");
$("#mat_em_votacao").text("Matéria Votada");
}
else if (resultado_votacao_upper.search("LIDA") != -1){
$("#mat_em_votacao").text("Matéria Lida");
}
}
else{
$("#resultado_votacao").text('');
if(data['tipo_votacao'] != 'Leitura')
$("#mat_em_votacao").text("Matéria em Votação");
else{
$("#mat_em_votacao").text("Matéria em Leitura");
}
}
// Matéria
$("#materia_legislativa_texto").text(data["materia_legislativa_texto"]);
$("#materia_legislativa_ementa").text(data["materia_legislativa_ementa"]);
}
@ -373,7 +254,7 @@
ws.onopen = function () {
console.log("✅ WebSocket connected");
// Optionally send an initial message to the server
ws.send(JSON.stringify({ type: "hello", message: "Client connected" }));
ws.send(JSON.stringify({ type: "echo", message: "Client connected" }));
// Ping keep-alive
pingTimer = setInterval(() => {
@ -390,9 +271,18 @@
if (data.type === "data") {
console.log("📩 Message from server:", data);
update_view(data);
//if (data.text) {
// document.getElementById("output").textContent = data.text;
//}
}
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);
@ -404,10 +294,10 @@
console.log("❌ WebSocket closed:", event);
clearInterval(pingTimer);
clearTimeout(timer);
clearTimeout(connTimer);
// retry with capped exponential backoff
timer = setTimeout(connect, Math.min(backoff, 10000));
connTimer = setTimeout(connect, Math.min(backoff, 10000));
backoff *= 2;
};
@ -417,7 +307,78 @@
};
}
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>
})
</script>
</html>

12
vue.config.js

@ -16,6 +16,18 @@ dotenv.config({
module.exports = {
runtimeCompiler: true,
configureWebpack: {
resolve: {
alias: {
// This ensures that imports of 'vue' use the build with the template compiler
'vue$': 'vue/dist/vue.esm.js'
}
}
},
// Optional but recommended if Django serves the final HTML:
// disable source maps in production builds to avoid exposing source
productionSourceMap: false,
publicPath:
process.env.NODE_ENV === 'production'
? '/static/sapl/frontend'

Loading…
Cancel
Save