fix: changed colors on qsl info tab

This commit is contained in:
2026-07-14 20:02:52 +02:00
parent 04b6431726
commit ee844564de
2 changed files with 131 additions and 8 deletions
+24 -8
View File
@@ -80,11 +80,23 @@ function StatusCell({ value }: { value?: string }) {
if (v === '') { if (v === '') {
return <span className="block text-center text-[11px] text-muted-foreground"></span>; return <span className="block text-center text-[11px] text-muted-foreground"></span>;
} }
// One colour per state, and each colour means something:
// Yes green — confirmed, the thing you wanted
// Requested blue — in flight, waiting on the other end. Not a problem.
// Modified orange — uploaded, then the QSO changed: it needs re-uploading.
// This is the ONLY state asking for action, so it gets the
// only alarming colour.
// No neutral — nothing done yet. Every freshly logged QSO is "No" on
// every row; painting that orange (as it used to be, in the
// same orange as Requested) made the table shout about a
// non-problem and told you nothing apart.
// Ignore dashed — deliberately excluded, on purpose.
const label = v === 'Y' ? t('qedit.qslYes') : v === 'R' ? t('qedit.qslRequested') : v === 'I' ? t('qedit.qslIgnore') : v === 'M' ? t('qedit.statusModified') : t('qedit.qslNo'); const label = v === 'Y' ? t('qedit.qslYes') : v === 'R' ? t('qedit.qslRequested') : v === 'I' ? t('qedit.qslIgnore') : v === 'M' ? t('qedit.statusModified') : t('qedit.qslNo');
const cls = v === 'Y' ? 'bg-success text-success-foreground' const cls = v === 'Y' ? 'bg-success text-success-foreground border border-success'
: v === 'R' ? 'bg-warning text-warning-foreground' : v === 'R' ? 'bg-info-muted text-info-muted-foreground border border-info-border'
: v === 'I' ? 'bg-muted-foreground text-background' : v === 'M' ? 'bg-warning text-warning-foreground border border-warning'
: 'bg-warning text-warning-foreground'; : v === 'I' ? 'bg-muted text-muted-foreground border border-dashed border-border italic'
: 'bg-muted text-muted-foreground border border-border';
return <span className={cn('block text-center text-[11px] font-semibold rounded px-1 py-0.5', cls)}>{label}</span>; return <span className={cn('block text-center text-[11px] font-semibold rounded px-1 py-0.5', cls)}>{label}</span>;
} }
@@ -571,9 +583,13 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
</p> </p>
</div> </div>
{/* Right: live status grid for every channel */} {/* Right: live status grid for every channel.
<div className="w-72 shrink-0"> Sized by its content, not pinned to a width: a fixed 288px box
<table className="w-full border-separate" style={{ borderSpacing: 4 }}> left the label column too narrow, so "QSL (paper)" wrapped onto
two lines and padded out the whole row. There is spare width to
the right — spend it on the label. */}
<div className="shrink-0">
<table className="border-separate" style={{ borderSpacing: 4 }}>
<thead> <thead>
<tr className="text-[10px] uppercase tracking-wider text-muted-foreground"> <tr className="text-[10px] uppercase tracking-wider text-muted-foreground">
<th className="text-left font-semibold">{t('qedit.thType')}</th> <th className="text-left font-semibold">{t('qedit.thType')}</th>
@@ -584,7 +600,7 @@ export function QSOEditModal({ qso, onSave, onDelete, onClose, countries = [], b
<tbody> <tbody>
{CONFIRMATIONS.map((c) => ( {CONFIRMATIONS.map((c) => (
<tr key={c.key} className="text-xs"> <tr key={c.key} className="text-xs">
<td className="font-medium pr-2 py-0.5">{CONF_LABEL_KEYS[c.key] ? t(CONF_LABEL_KEYS[c.key]) : c.label}</td> <td className="font-medium pr-3 py-0.5 whitespace-nowrap">{CONF_LABEL_KEYS[c.key] ? t(CONF_LABEL_KEYS[c.key]) : c.label}</td>
<td className="w-24"><StatusCell value={val(c.sent)} /></td> <td className="w-24"><StatusCell value={val(c.sent)} /></td>
<td className="w-24">{c.rcvd ? <StatusCell value={val(c.rcvd)} /> : <span className="block text-center text-[11px] text-muted-foreground"></span>}</td> <td className="w-24">{c.rcvd ? <StatusCell value={val(c.rcvd)} /> : <span className="block text-center text-[11px] text-muted-foreground"></span>}</td>
</tr> </tr>
+107
View File
@@ -0,0 +1,107 @@
package award
import (
"encoding/json"
"fmt"
"testing"
"time"
"hamlog/internal/qso"
)
// A synthetic log with the shape a real one has: a spread of bands, modes,
// entities, and the free-text fields the harder awards actually scan.
func benchLog(n int) []qso.QSO {
bands := []string{"160m", "80m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"}
modes := []string{"SSB", "CW", "FT8", "FT4", "RTTY"}
entities := []int{291, 248, 227, 230, 339, 5, 108, 110, 6, 281}
towns := []string{"SERIATE (BG)", "MILANO (MI)", "ROMA (RM)", "TORINO (TO)", "NAPOLI (NA)"}
out := make([]qso.QSO, n)
base := time.Date(2015, 1, 1, 0, 0, 0, 0, time.UTC)
for i := range out {
e := entities[i%len(entities)]
out[i] = qso.QSO{
ID: int64(i + 1),
Callsign: fmt.Sprintf("I%dABC", i%10),
Band: bands[i%len(bands)],
Mode: modes[i%len(modes)],
DXCC: &e,
QSODate: base.Add(time.Duration(i) * time.Hour),
QTH: towns[i%len(towns)],
Address: "Via Roma 12, 24068 " + towns[i%len(towns)] + " Italy",
Grid: "JN45AA",
State: "CA",
LOTWRcvd: "Y",
}
}
return out
}
// WAIP is the worst realistic shape: a predefined list, an exact-match primary
// that always produces a doomed candidate, then a fallback chain that tokenises a
// free-text field. If anything is slow, it is this.
func benchWAIP() (Def, []RefMeta) {
d := Def{
Code: "WAIP", Valid: true, Type: TypeReference,
Field: "address", MatchBy: "code", ExactMatch: true,
OrRules: []OrRule{
{Field: "qth", MatchBy: "description"},
{Field: "qth", MatchBy: "code"},
{Field: "address", MatchBy: "code"},
},
DXCCFilter: []int{248},
Confirm: []string{"lotw", "qsl"},
}
codes := []string{"AG", "AL", "AN", "AO", "AP", "AQ", "AR", "AT", "AV", "BA", "BG", "BI", "BL", "BN", "BO", "BR", "BS", "BT", "BZ", "CA", "CB", "CE", "CH", "CL", "CN", "CO", "CR", "CS", "CT", "CZ", "EN", "FC", "FE", "FG", "FI", "FM", "FR", "GE", "GO", "GR", "IM", "IS", "KR", "LC", "LE", "LI", "LO", "LT", "LU", "MB", "MC", "ME", "MI", "MN", "MO", "MS", "MT", "NA", "NO", "NU", "OR", "PA", "PC", "PD", "PE", "PG", "PI", "PN", "PO", "PR", "PT", "PU", "PV", "PZ", "RA", "RC", "RE", "RG", "RI", "RM", "RN", "RO", "SA", "SI", "SO", "SP", "SR", "SS", "SU", "SV", "TA", "TE", "TN", "TO", "TP", "TR", "TS", "TV", "UD", "VA", "VB", "VC", "VE", "VI", "VR", "VT", "VV"}
metas := make([]RefMeta, 0, len(codes))
for _, c := range codes {
metas = append(metas, RefMeta{Code: c, Name: "Province " + c, Valid: true})
}
return d, metas
}
// One award, whole log — what GetAward(code) does when the Awards panel opens.
func BenchmarkComputeOneAward(b *testing.B) {
d, metas := benchWAIP()
for _, n := range []int{10_000, 50_000, 100_000} {
log := benchLog(n)
b.Run(fmt.Sprintf("WAIP/%dqso", n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
Compute([]Def{d}, log, map[string][]RefMeta{"WAIP": metas}, nil)
}
})
}
}
// Every shipped award at once — GetAwards(), the heaviest thing the app can ask.
func BenchmarkComputeWholeCatalog(b *testing.B) {
defs := Defaults()
metas := map[string][]RefMeta{}
wd, wm := benchWAIP()
defs = append(defs, wd)
metas["WAIP"] = wm
for _, c := range Catalog() {
if raw, ok := CatalogRefs(c.Def.Code); ok {
var refs []struct {
Code string `json:"code"`
Name string `json:"name"`
Valid bool `json:"valid"`
}
if json.Unmarshal(raw, &refs) == nil {
m := make([]RefMeta, 0, len(refs))
for _, r := range refs {
m = append(m, RefMeta{Code: r.Code, Name: r.Name, Valid: r.Valid})
}
metas[c.Def.Code] = m
}
}
}
for _, n := range []int{10_000, 100_000} {
log := benchLog(n)
b.Run(fmt.Sprintf("%dawards/%dqso", len(defs), n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
Compute(defs, log, metas, nil)
}
})
}
}