fix(admin): dominio trocado ao criar, apelidos escondidos e quota a zero

Criar "lucas@cienciaembarcada.com.br" criava "lucas@lucasrguerra.dev.br". O
formulario partia o endereco e deitava fora a metade do dominio, e nao enviava
domain_id nenhum -- o servidor caia entao no dominio do admin com sessao
iniciada. Passa a ser parte local mais um dominio escolhido de uma lista: a
metade que decide o endereco deixa de poder ser descartada em silencio.

Apelidos de outros dominios estavam escondidos, e a culpa era minha: procurava
so no dominio onde a morada do proprio utilizador vive. Uma caixa recebe
postmaster, abuse, dmarc e tlsrpt de varios dominios ao mesmo tempo, por isso
o painel mostrava quatro onde entregavam oito. Ha um teste que falha com a
consulta antiga.

Os apelidos passam a poder ser criados e removidos dentro do modal. O dominio
sai do endereco escrito, para nao repetir a armadilha do formulario de cima.

USER, DOMAIN_ADMIN e SUPER_ADMIN eram constantes impressas no ecra, logo
ficavam em ingles em qualquer idioma. Um papel desconhecido continua a ser
mostrado tal como esta guardado, porque quem o ve precisa de saber o que a
base de dados tem.

A quota estava certa e mal mostrada. used_bytes vale 140771 -- os 137,5 KB
reais -- mas a pagina arredondava para megabytes inteiros, portanto qualquer
coisa abaixo de 512 KB lia-se "0 / 2 MB" e a barra ficava vazia, calculada
sobre os megabytes ja arredondados. Passa a formatar a partir dos bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Opus 5 committed 2026-08-24 11:02:02 -03:00
1 parent f801894f53
commit bb12e603a3
9 files changed
+308 -35

No files matched your search

@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";
import { formatBytes, usageRatio } from "./formatBytes";
describe("formatBytes", () => {
// The case from the console: 140771 bytes rendered as "0 / 2 MB".
it("shows a mailbox smaller than a megabyte as itself", () => {
expect(formatBytes(140771)).toBe("137.5 KB");
});
it("climbs through the units", () => {
expect(formatBytes(0)).toBe("0 B");
expect(formatBytes(512)).toBe("512 B");
expect(formatBytes(1024)).toBe("1.0 KB");
expect(formatBytes(2097152)).toBe("2.0 MB");
expect(formatBytes(5_368_709_120)).toBe("5.0 GB");
});
it("keeps one decimal at every size, so the reading stays comparable", () => {
expect(formatBytes(157_286_400)).toBe("150.0 MB");
});
it.each([-1, Number.NaN, Number.POSITIVE_INFINITY])("survives %s", (value) => {
expect(typeof formatBytes(value)).toBe("string");
});
});
describe("usageRatio", () => {
it("measures from the raw bytes, not from rounded megabytes", () => {
// Rounding first gave 0/2 = 0%, and the bar stayed empty on a mailbox
// that was really 6.7% full.
expect(usageRatio(140771, 2097152)).toBeCloseTo(0.0671, 3);
});
// Zero means unlimited; dividing by it reported every unlimited mailbox as
// over its limit.
it("never calls an unlimited mailbox full", () => {
expect(usageRatio(5_000_000, 0)).toBe(0);
});
it("stops at full rather than overflowing the bar", () => {
expect(usageRatio(3_000_000, 2_000_000)).toBe(1);
});
});
+38
View File
@@ -0,0 +1,38 @@
/**
* A byte count as a person reads it.
*
* The admin list rounded bytes to whole megabytes, so a mailbox holding
* 137.5 KB displayed as "0 / 2 MB" and its usage bar sat empty - the figure
* was right in the database and lost on the way to the screen.
*/
export function formatBytes(bytes: number): string {
const value = Number(bytes);
if (!Number.isFinite(value) || value <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let n = value;
let unit = 0;
while (n >= 1024 && unit < units.length - 1) {
n /= 1024;
unit++;
}
// Bytes are whole things; above that always one decimal, which is what
// tells 137.5 KB from 137 KB. Dropping it on larger numbers would have hid
// exactly the digit that made the reading recognisable.
const rendered = unit === 0 ? String(Math.round(n)) : n.toFixed(1);
return `${rendered} ${units[unit]}`;
}
/**
* How full a mailbox is, as a percentage, computed from the raw byte counts.
*
* A quota of zero means unlimited, so it is never full - dividing by it would
* report every unlimited mailbox as over its limit.
*/
export function usageRatio(usedBytes: number, quotaBytes: number): number {
const used = Number(usedBytes);
const quota = Number(quotaBytes);
if (!Number.isFinite(used) || !Number.isFinite(quota) || quota <= 0 || used <= 0) return 0;
return Math.min(used / quota, 1);
}
+18
View File
@@ -0,0 +1,18 @@
/**
* A role as a person reads it.
*
* The console printed the stored constants - USER, DOMAIN_ADMIN, SUPER_ADMIN -
* straight onto the screen, so those three words stayed in English whatever
* the interface was set to.
*/
export function roleLabel(t: (key: string) => string, role: string): string {
const known: Record<string, string> = {
USER: "roles.user",
DOMAIN_ADMIN: "roles.domainAdmin",
SUPER_ADMIN: "roles.superAdmin",
};
const key = known[String(role).toUpperCase()];
// An unknown role is shown as stored rather than as a missing-translation
// marker: whoever sees it needs to know what the database actually holds.
return key ? t(key) : String(role);
}