Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@ Versioning follows [SemVer](https://semver.org/): **MAJOR.MINOR.PATCH**

---

## [1.36.1] — 2026-06-11

Responsividade no celular. A base já era mobile-first (Bootstrap 5.3, navbar
colapsável, formulários em grid), mas as **listas eram tabelas largas** que rolavam
na horizontal — ruins de ler no telefone — e algumas barras de ação/cabeçalho
estouravam a tela. Só markup/CSS; o desktop não muda.

### Changed
- **Listas viram cartões empilhados no celular (≤576px).** Toda tabela de itens
(Spools, Filamentos, Fila de Etiquetas, Estoque Baixo, Relatórios por
Material/Local, Histórico de Pesagens, Carretéis Vazios, Busca, Usuários, Backups,
além dos históricos nos detalhes) agora vira um cartão por linha com `rótulo: valor`
— **sem rolagem horizontal**. No desktop continua tabela. Implementado com um único
bloco `@media (max-width:576px)` sobre a classe `.sc-stack` + `data-label` nas
células; o `<thead>` é ocultado no celular.
- **Barras de ferramentas e de ações quebram em linha** (`flex-wrap`) nas páginas de
lista e no detalhe do spool, em vez de espremer/transbordar em telas estreitas.
- **Cartões de login e 2FA** passam de largura fixa (`360px`) para
`max-width:360px` com margem lateral, não colando mais nas bordas em telas de
320–375px.
- **Toasts e alertas** ganham `max-width:calc(100vw - 2rem)` para não vazar a
largura da viewport no celular.

## [1.36.0] — 2026-06-10

Endurecimento de segurança a partir de um teste externo (caixa-preta). Vários
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.36.0
1.36.1
45 changes: 45 additions & 0 deletions static/spool.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,51 @@ h1, h2, h3, h4, h5, h6 { letter-spacing: -0.02em; }
border-top-right-radius: 10px;
}

/* ── Tabelas viram cartões no celular (.sc-stack) ───────────────────────────
No desktop a tabela é normal. Em telas ≤576px cada <tr> vira um cartão e cada
<td> mostra "rótulo: valor" via data-label. Células sem data-label (donut, ID,
ações) ocupam a linha inteira; donut+ID compartilham a linha de cabeçalho via
.sc-stack-head. */
@media (max-width: 576px) {
table.sc-stack thead { display: none; }
table.sc-stack, table.sc-stack tbody { display: block; width: 100%; }
table.sc-stack tr {
display: block;
border: 1px solid var(--sc-border);
border-radius: 10px;
padding: .5rem .75rem;
margin: .6rem;
}
table.sc-stack td {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
border: none;
padding: .2rem 0;
text-align: right;
}
table.sc-stack td::before {
content: attr(data-label);
font-weight: 600;
color: var(--sc-text-muted);
text-align: left;
white-space: nowrap;
}
/* células sem rótulo: linha inteira, sem "rótulo:" */
table.sc-stack td:not([data-label]) { display: block; text-align: left; }
table.sc-stack td:not([data-label])::before { content: none; }
/* donut + ID na mesma linha de cabeçalho do cartão */
table.sc-stack td.sc-stack-head {
display: inline-flex; width: auto; padding: .1rem .4rem .2rem 0;
vertical-align: middle;
}
/* barra de ações: botões à esquerda, com folga acima */
table.sc-stack td.sc-stack-actions { margin-top: .35rem; }
/* linha "nenhum resultado" (colspan) volta a ser texto simples centralizado */
table.sc-stack td[colspan] { display: block; text-align: center; }
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn {
font-weight: 500;
Expand Down
10 changes: 5 additions & 5 deletions templates/admin/backup.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ <h6 class="text-muted text-uppercase fw-semibold mb-3" style="font-size:.75rem">
<h6 class="text-muted text-uppercase fw-semibold mb-3" style="font-size:.75rem">{{ _('Backups automáticos (diários)') }}</h6>
<p class="mb-3" style="font-size:.85rem">{{ _('O sistema mantém um backup por dia da semana (7 no total), sobrescrevendo o do mesmo dia a cada semana.') }}</p>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<table class="table table-sm align-middle mb-0 sc-stack">
<thead class="table-light">
<tr>
<th>#</th><th>{{ _('Data') }}</th><th class="text-end">{{ _('Tamanho') }}</th><th></th>
Expand All @@ -47,11 +47,11 @@ <h6 class="text-muted text-uppercase fw-semibold mb-3" style="font-size:.75rem">
<tbody>
{% for b in local_backups %}
<tr>
<td class="text-muted">{{ b.slot }}</td>
<td class="text-muted sc-stack-head">{{ b.slot }}</td>
{% if b.exists %}
<td>{{ b.mtime | localdt }}</td>
<td class="text-end text-muted">{{ b.size_kb }} KB</td>
<td class="text-end">
<td data-label="{{ _('Data') }}">{{ b.mtime | localdt }}</td>
<td class="text-end text-muted" data-label="{{ _('Tamanho') }}">{{ b.size_kb }} KB</td>
<td class="text-end sc-stack-actions">
<div class="d-flex justify-content-end gap-1">
<a href="{{ url_for('admin_backup_download_local', slot=b.slot) }}"
class="btn btn-sm btn-outline-secondary" title="{{ _('Baixar') }}">
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/update.html
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ <h4 class="fw-bold mb-4">{{ _('Atualizações') }}</h4>
wrap.style.cssText = 'z-index:1090;pointer-events:none';
wrap.innerHTML =
'<div class="toast align-items-center text-bg-success border-0" role="alert" ' +
'data-bs-autohide="true" data-bs-delay="6000" style="pointer-events:auto;min-width:260px">' +
'data-bs-autohide="true" data-bs-delay="6000" style="pointer-events:auto;min-width:260px;max-width:calc(100vw - 2rem)">' +
'<div class="d-flex"><div class="toast-body fw-semibold">' +
'<i class="bi bi-check-circle-fill me-2"></i>' +
"{{ _('Atualizado para a v{v} com sucesso') }}".replace('{v}', v) +
Expand Down
10 changes: 5 additions & 5 deletions templates/admin/users.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,17 @@ <h4 class="mb-0 fw-bold">{{ _('Gerenciamento de Usuários') }}</h4>
<div class="card overflow-hidden">
<div class="card-header fw-semibold">{{ _('Usuários Cadastrados') }}</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<table class="table table-sm mb-0 sc-stack">
<thead class="table-light">
<tr><th>{{ _('Usuário') }}</th><th>{{ _('Perfil') }}</th><th>{{ _('Criado em') }}</th><th></th></tr>
</thead>
<tbody>
{% for u in users %}
<tr>
<td class="fw-semibold">{{ u.username }} {% if u.id == session.user_id %}<span class="badge bg-secondary">{{ _('você') }}</span>{% endif %}</td>
<td><span class="badge {% if u.role == 'admin' %}bg-dark{% else %}bg-light text-dark border{% endif %}">{{ u.role }}</span></td>
<td class="text-muted small">{{ u.created_at | localdate }}</td>
<td>
<td class="fw-semibold sc-stack-head">{{ u.username }} {% if u.id == session.user_id %}<span class="badge bg-secondary">{{ _('você') }}</span>{% endif %}</td>
<td data-label="{{ _('Perfil') }}"><span class="badge {% if u.role == 'admin' %}bg-dark{% else %}bg-light text-dark border{% endif %}">{{ u.role }}</span></td>
<td class="text-muted small" data-label="{{ _('Criado em') }}">{{ u.created_at | localdate }}</td>
<td class="sc-stack-actions">
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#pwModal{{ u.id }}">{{ _('Senha') }}</button>
{% if u.id != session.user_id %}
<form method="post" action="{{ url_for('admin_users_delete', user_id=u.id) }}" class="d-inline"
Expand Down
4 changes: 2 additions & 2 deletions templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,14 @@
{% for cat, msg in messages %}
{% if cat == 'success' %}
<div class="toast align-items-center text-bg-success border-0 sc-toast" role="alert"
data-bs-autohide="true" data-bs-delay="3000" style="pointer-events:auto;min-width:260px">
data-bs-autohide="true" data-bs-delay="3000" style="pointer-events:auto;min-width:260px;max-width:calc(100vw - 2rem)">
<div class="d-flex">
<div class="toast-body fw-semibold"><i class="bi bi-check-circle-fill me-2"></i>{{ msg }}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
</div>
</div>
{% else %}
<div class="alert alert-{{ cat }} alert-dismissible fade show mb-0" role="alert" style="pointer-events:auto;min-width:260px">
<div class="alert alert-{{ cat }} alert-dismissible fade show mb-0" role="alert" style="pointer-events:auto;min-width:260px;max-width:calc(100vw - 2rem)">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
Expand Down
26 changes: 13 additions & 13 deletions templates/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,23 @@ <h4 class="mb-0 fw-bold">{{ _('Dashboard') }}</h4>
<div class="card mb-4 border-warning overflow-hidden">
<div class="card-header bg-warning fw-bold"><i class="bi bi-exclamation-triangle-fill me-1"></i>{{ _('Estoque Baixo') }}</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<table class="table table-sm mb-0 sc-stack">
<thead class="table-light">
<tr><th>{{ _('Material') }}</th><th>{{ _('Marca / Família') }}</th><th>{{ _('Local') }}</th><th>{{ _('Restante') }}</th><th>%</th><th></th></tr>
</thead>
<tbody>
{% for s in low_stock %}
<tr>
<td>{{ s.material }}</td>
<td>{{ s.brand }} / {{ s.family }}</td>
<td>{{ s.location or '—' }}</td>
<td>{{ "%.0f"|format(s.current_net_g) }}g</td>
<td>
<td class="sc-stack-head fw-semibold">{{ s.material }}</td>
<td data-label="{{ _('Marca / Família') }}">{{ s.brand }} / {{ s.family }}</td>
<td data-label="{{ _('Local') }}">{{ s.location or '—' }}</td>
<td data-label="{{ _('Restante') }}">{{ "%.0f"|format(s.current_net_g) }}g</td>
<td data-label="%">
<div class="progress" style="height:14px;width:80px">
<div class="progress-bar bg-warning" style="width:{{ s.pct_remaining }}%">{{ s.pct_remaining }}%</div>
</div>
</td>
<td><a href="{{ url_for('spools_detail', spool_id=s.id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Ver') }}</a></td>
<td class="sc-stack-actions"><a href="{{ url_for('spools_detail', spool_id=s.id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Ver') }}</a></td>
</tr>
{% endfor %}
</tbody>
Expand All @@ -82,18 +82,18 @@ <h4 class="mb-0 fw-bold">{{ _('Dashboard') }}</h4>
<div class="card overflow-hidden">
<div class="card-header fw-bold"><i class="bi bi-clock-history me-1"></i>{{ _('Pesagens Recentes') }}</div>
<div class="table-responsive">
<table class="table table-sm mb-0">
<table class="table table-sm mb-0 sc-stack">
<thead class="table-light">
<tr><th>{{ _('Quando') }}</th><th>{{ _('Material') }}</th><th>{{ _('Marca / Família') }}</th><th>{{ _('Peso Net') }}</th><th></th></tr>
</thead>
<tbody>
{% for r in stats.recent_weighings %}
<tr>
<td class="text-muted small">{{ r.ts | localdt }}</td>
<td>{{ r.material }}</td>
<td>{{ r.brand }} / {{ r.family }}</td>
<td>{{ "%.0f"|format(r.net_weight_g) }}g</td>
<td><a href="{{ url_for('spools_detail', spool_id=r.spool_id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Ver') }}</a></td>
<td class="text-muted small sc-stack-head">{{ r.ts | localdt }}</td>
<td data-label="{{ _('Material') }}">{{ r.material }}</td>
<td data-label="{{ _('Marca / Família') }}">{{ r.brand }} / {{ r.family }}</td>
<td data-label="{{ _('Peso Net') }}">{{ "%.0f"|format(r.net_weight_g) }}g</td>
<td class="sc-stack-actions"><a href="{{ url_for('spools_detail', spool_id=r.spool_id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Ver') }}</a></td>
</tr>
{% endfor %}
</tbody>
Expand Down
18 changes: 9 additions & 9 deletions templates/filaments/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ <h4 class="mb-0 fw-bold">
<h5 class="fw-semibold mb-3">{{ _('Spools') }} ({{ spools|length }})</h5>
<div class="card">
<div class="table-responsive">
<table class="table table-hover mb-0">
<table class="table table-hover mb-0 sc-stack">
<thead class="table-dark">
<tr>
<th style="width:44px"></th>
Expand All @@ -104,25 +104,25 @@ <h5 class="fw-semibold mb-3">{{ _('Spools') }} ({{ spools|length }})</h5>
<tr class="spool-row {% if not s.active %}table-secondary{% endif %}"
style="cursor:pointer"
data-href="{{ url_for('spools_detail', spool_id=s.id) }}">
<td class="text-center align-middle" style="width:44px;padding:4px">
<td class="text-center align-middle sc-stack-head" style="width:44px;padding:4px">
{{ donut(pct, fill, tip=pct|int|string + _('% disponível')) }}
</td>
<td class="fw-bold">SP-{{ '%04d'|format(s.id) }}</td>
<td>{{ s.model_name or '—' }}</td>
<td>{{ s.location or '—' }}</td>
<td>{{ s.nominal_weight_g|int }}g</td>
<td>
<td class="fw-bold sc-stack-head">SP-{{ '%04d'|format(s.id) }}</td>
<td data-label="{{ _('Carretel Vazio') }}">{{ s.model_name or '—' }}</td>
<td data-label="{{ _('Local') }}">{{ s.location or '—' }}</td>
<td data-label="{{ _('Nominal') }}">{{ s.nominal_weight_g|int }}g</td>
<td data-label="{{ _('Peso Atual') }}">
{% if s.current_net_g is not none %}{{ s.current_net_g|int }}g
{% else %}<span class="text-muted" data-bs-toggle="tooltip" title="{{ _('não pesado') }}">{{ s.nominal_weight_g|int }}g</span>{% endif %}
</td>
<td>
<td data-label="{{ _('Status') }}">
{% if s.active %}
<span class="badge bg-success">{{ _('Ativo') }}</span>
{% else %}
<span class="badge bg-secondary">{{ _('Finalizado') }}</span>
{% endif %}
</td>
<td onclick="event.stopPropagation()">
<td class="sc-stack-actions" onclick="event.stopPropagation()">
<a href="{{ url_for('spools_detail', spool_id=s.id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Ver') }}</a>
{% if can_write and s.active %}
<a href="{{ url_for('spools_weigh', spool_id=s.id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Pesar') }}</a>
Expand Down
22 changes: 11 additions & 11 deletions templates/filaments/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
</span>
{% endmacro %}

<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<h4 class="mb-0 fw-bold">{{ _('Filamentos') }}</h4>
<div class="d-flex gap-2">
<div class="d-flex flex-wrap gap-2">
<input class="form-control form-control-sm" type="search" id="filterInput"
data-filter-for="#filamentTable"
placeholder="{{ _('Filtrar...') }}" value="{{ q or '' }}" style="min-width:160px" autocomplete="off">
Expand All @@ -30,7 +30,7 @@ <h4 class="mb-0 fw-bold">{{ _('Filamentos') }}</h4>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover mb-0" id="filamentTable" data-sortable>
<table class="table table-hover mb-0 sc-stack" id="filamentTable" data-sortable>
<thead class="table-dark">
<tr>
<th style="width:44px"></th>
Expand All @@ -53,34 +53,34 @@ <h4 class="mb-0 fw-bold">{{ _('Filamentos') }}</h4>
{% set fill = f.color_hex if f.color_hex else '#6c757d' %}
<tr class="filament-row" style="cursor:pointer"
data-href="{{ url_for('filaments_detail', filament_id=f.id) }}">
<td class="text-center align-middle" style="width:44px;padding:4px">
<td class="text-center align-middle sc-stack-head" style="width:44px;padding:4px">
{% if f.active_count > 0 %}
{{ donut(pct, fill, tip=pct|int|string + _('% disponível')) }}
{% else %}
{{ donut(0, '#6c757d') }}
{% endif %}
</td>
<td>
<td class="sc-stack-head">
<a href="{{ url_for('spools_list') }}?q={{ f.material }}"
class="badge bg-secondary text-decoration-none"
onclick="event.stopPropagation()">{{ f.material }}</a>
</td>
<td>
<td data-label="{{ _('Marca') }}">
{% if f.brand_logo %}
<img src="{{ url_for('static', filename=f.brand_logo) }}"
height="18" style="object-fit:contain;max-width:48px;vertical-align:middle" alt="" class="me-1">
{% endif %}
<a href="{{ url_for('spools_list') }}?q={{ f.brand }}"
class="text-decoration-none" onclick="event.stopPropagation()">{{ f.brand }}</a>
</td>
<td>
<td data-label="{{ _('Família') }}">
<a href="{{ url_for('spools_list') }}?q={{ f.family }}"
class="text-decoration-none" onclick="event.stopPropagation()">{{ f.family }}</a>
</td>
<td>{{ f.diameter_mm }}mm</td>
<td>{{ f.active_count }}</td>
<td>{{ f.spool_count }}</td>
<td onclick="event.stopPropagation()">
<td data-label="Ø">{{ f.diameter_mm }}mm</td>
<td data-label="{{ _('Spools Ativos') }}">{{ f.active_count }}</td>
<td data-label="{{ _('Total') }}">{{ f.spool_count }}</td>
<td class="sc-stack-actions" onclick="event.stopPropagation()">
<a href="{{ url_for('filaments_detail', filament_id=f.id) }}" class="btn btn-sm btn-outline-secondary">{{ _('Ver') }}</a>
{% if can_write %}
<a href="{{ url_for('spools_new') }}?filament_id={{ f.id }}" class="btn btn-sm btn-outline-secondary" title="{{ _('+ Novo Spool') }}"><i class="bi bi-plus-lg"></i></a>
Expand Down
4 changes: 2 additions & 2 deletions templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
<li><a class="dropdown-item d-flex align-items-center gap-2" href="{{ url_for('set_lang', code='es') }}"><img src="{{ url_for('static', filename='flags/es.svg') }}" alt="" class="sc-flag">ES</a></li>
</ul>
</div>
<div class="d-flex justify-content-center align-items-center" style="min-height:100vh">
<div class="card shadow-sm" style="width:360px">
<div class="d-flex justify-content-center align-items-center px-3" style="min-height:100vh">
<div class="card shadow-sm w-100" style="max-width:360px">
<div class="card-body p-4">
<div class="text-center mb-4">
{% set logo_h = 44 %}{% include '_brand.html' %}
Expand Down
4 changes: 2 additions & 2 deletions templates/login_2fa.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
<li><a class="dropdown-item d-flex align-items-center gap-2" href="{{ url_for('set_lang', code='es') }}"><img src="{{ url_for('static', filename='flags/es.svg') }}" alt="" class="sc-flag">ES</a></li>
</ul>
</div>
<div class="d-flex justify-content-center align-items-center" style="min-height:100vh">
<div class="card shadow-sm" style="width:360px">
<div class="d-flex justify-content-center align-items-center px-3" style="min-height:100vh">
<div class="card shadow-sm w-100" style="max-width:360px">
<div class="card-body p-4">
<div class="text-center mb-4">
{% set logo_h = 44 %}{% include '_brand.html' %}
Expand Down
Loading