feat: Verschluesselung waehlbar, dazu zwei Layout-Fehler
Verschluesselung: Bisher gab es nur den Schalter use_ssl – also implizites TLS auf 993 oder gar nichts. STARTTLS auf 143 fehlte ganz, und das bieten viele Anbieter an. Jetzt ein Feld mit drei Werten, der Port zieht bei der Auswahl mit, und "keine" warnt, dass Zugangsdaten im Klartext gehen. Layout: Die Postfach-Karte steht in einer schmalen Spalte; der waagerechte Aufbau brach dort auf ein Wort je Zeile um und die Knoepfe lagen ueber dem Titel. Jetzt gestapelt. Die Einstellungsseite nutzte ein Raster, das die Karten zeilenweise ausrichtet – die zweite Reihe begann erst unter der hoechsten Karte der ersten, weshalb "Backup zurueckladen" allein weit unten stand. Jetzt fliessen die Karten spaltenweise, und die Spalten sind breiter: vier schmale Karten nebeneinander waren auf dem Notebook nicht lesbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+13
-6
@@ -284,13 +284,20 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Einstellungen & Mein Konto: Karten in ein mehrspaltiges Raster (nutzt die
|
||||
Bildschirmbreite besser als das bisherige Untereinander). */
|
||||
/* Einstellungen & Mein Konto: Karten fliessen spaltenweise.
|
||||
Ein Raster richtet die Karten zeilenweise aus – die zweite Reihe beginnt erst
|
||||
unter der HOECHSTEN Karte der ersten, was bei sehr unterschiedlich hohen
|
||||
Karten ein grosses Loch hinterlaesst. Der Spaltenfluss stapelt stattdessen
|
||||
weiter, sobald oben Platz frei wird. Die Spalten sind bewusst breit: vier
|
||||
schmale Karten nebeneinander waren auf dem Notebook nicht mehr lesbar. */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
gap: var(--sp-4);
|
||||
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||
align-items: start;
|
||||
column-width: 380px;
|
||||
column-gap: var(--sp-4);
|
||||
}
|
||||
.settings-grid > * {
|
||||
break-inside: avoid;
|
||||
margin-bottom: var(--sp-4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Nutzer-Chip in der Seitenleisten-Fußzeile ist ein Link auf „Mein Konto". */
|
||||
|
||||
@@ -522,6 +522,12 @@
|
||||
"deleteConfirm": "Postfach „{{name}}“ entfernen? Bereits erfasste Zahlungen bleiben.",
|
||||
"noSenders": "Noch kein Absender zugeordnet – ohne Zuordnung wird nichts übernommen.",
|
||||
"noAccount": "kein Konto",
|
||||
"fromPlaceholder": "Absender enthält, z. B. raiffeisen.ch"
|
||||
"fromPlaceholder": "Absender enthält, z. B. raiffeisen.ch",
|
||||
"security": "Verschlüsselung",
|
||||
"secSsl": "SSL/TLS (üblich, Port 993)",
|
||||
"secStarttls": "STARTTLS (Port 143)",
|
||||
"secNone": "keine",
|
||||
"secNoneWarn": "Ohne Verschlüsselung gehen Benutzername und Passwort im Klartext über das Netz. Nur sinnvoll, wenn der Mailserver auf demselben Rechner läuft.",
|
||||
"addSender": "Absender zuordnen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,6 +522,12 @@
|
||||
"deleteConfirm": "Remove mailbox “{{name}}”? Payments already captured stay.",
|
||||
"noSenders": "No sender mapped yet – without a mapping nothing is taken over.",
|
||||
"noAccount": "no account",
|
||||
"fromPlaceholder": "Sender contains, e.g. raiffeisen.ch"
|
||||
"fromPlaceholder": "Sender contains, e.g. raiffeisen.ch",
|
||||
"security": "Encryption",
|
||||
"secSsl": "SSL/TLS (usual, port 993)",
|
||||
"secStarttls": "STARTTLS (port 143)",
|
||||
"secNone": "none",
|
||||
"secNoneWarn": "Without encryption the username and password travel in clear text. Only sensible when the mail server runs on the same machine.",
|
||||
"addSender": "Map sender"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useConfirm } from "../../confirm";
|
||||
import Icon from "../../components/Icon";
|
||||
|
||||
const LEER = {
|
||||
name: "", host: "", port: 993, use_ssl: true, username: "", password: "",
|
||||
name: "", host: "", port: 993, security: "ssl", username: "", password: "",
|
||||
folder: "INBOX", done_folder: "", mark_seen: true, is_active: true, poll_minutes: 15,
|
||||
};
|
||||
|
||||
@@ -116,32 +116,32 @@ export default function Postfaecher() {
|
||||
<p className="muted small">{t("mailbox.intro")}</p>
|
||||
|
||||
{quellen.map((q) => (
|
||||
<div key={q.id} className="account-row" style={{ alignItems: "flex-start" }}>
|
||||
<div className="named-text grow">
|
||||
<div><strong>{q.name || q.host}</strong></div>
|
||||
<div className="named-sub">
|
||||
{q.username} · {q.folder}
|
||||
{q.last_error
|
||||
? ` · ${t("mailbox.lastError")}: ${q.last_error}`
|
||||
: q.last_poll_at ? ` · ${t("mailbox.lastPoll")}` : ` · ${t("mailbox.neverPolled")}`}
|
||||
</div>
|
||||
<div className="named-sub">
|
||||
{(absender[q.id] || []).length === 0
|
||||
? t("mailbox.noSenders")
|
||||
: (absender[q.id] || []).map((a) => {
|
||||
const konto = konten.find((k) => k.id === a.account_id);
|
||||
return `${a.from_contains || "*"} → ${konto ? konto.name : t("mailbox.noAccount")}`;
|
||||
}).join(" · ")}
|
||||
</div>
|
||||
<SenderForm konten={konten} onAdd={(d) => absenderAnlegen(q, d)} t={t} />
|
||||
<div key={q.id} className="mail-source">
|
||||
<div><strong>{q.name || q.host}</strong></div>
|
||||
<div className="muted small">{q.username} · {q.folder} · {q.security.toUpperCase()}</div>
|
||||
<div className="muted small">
|
||||
{q.last_error
|
||||
? `${t("mailbox.lastError")}: ${q.last_error}`
|
||||
: q.last_poll_at ? t("mailbox.lastPoll") : t("mailbox.neverPolled")}
|
||||
</div>
|
||||
<div className="row-actions">
|
||||
<div className="muted small" style={{ marginTop: "var(--sp-2)" }}>
|
||||
{(absender[q.id] || []).length === 0
|
||||
? t("mailbox.noSenders")
|
||||
: (absender[q.id] || []).map((a) => {
|
||||
const konto = konten.find((k) => k.id === a.account_id);
|
||||
return `${a.from_contains || "*"} → ${konto ? konto.name : t("mailbox.noAccount")}`;
|
||||
}).join(" · ")}
|
||||
</div>
|
||||
<SenderForm konten={konten} onAdd={(d) => absenderAnlegen(q, d)} t={t} />
|
||||
<div className="btn-pair" style={{ marginTop: "var(--sp-2)" }}>
|
||||
<button className="btn sm" disabled={busy} onClick={() => testen(q)}>{t("mailbox.test")}</button>
|
||||
<button className="btn sm primary" disabled={busy} onClick={() => abrufen(q)}>{t("mailbox.fetchNow")}</button>
|
||||
<button className="btn sm ghost" onClick={() => { setForm({ ...LEER, ...q, password: "" }); setOrdner(null); }}>
|
||||
<button className="btn sm ghost" title={t("common.edit")}
|
||||
onClick={() => { setForm({ ...LEER, ...q, password: "" }); setOrdner(null); }}>
|
||||
<Icon name="edit" size={15} />
|
||||
</button>
|
||||
<button className="btn sm danger" onClick={() => loeschen(q)}><Icon name="trash" size={15} /></button>
|
||||
<button className="btn sm danger" title={t("common.delete")}
|
||||
onClick={() => loeschen(q)}><Icon name="trash" size={15} /></button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -162,6 +162,22 @@ export default function Postfaecher() {
|
||||
<input value={form.host} required onChange={(e) => setzen({ host: e.target.value })}
|
||||
placeholder="imap.example.com" />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t("mailbox.security")}</span>
|
||||
<select value={form.security}
|
||||
onChange={(e) => {
|
||||
// Der uebliche Port haengt an der Verschluesselung – gleich
|
||||
// mitziehen, damit die haeufigste Fehlerquelle wegfaellt.
|
||||
const art = e.target.value;
|
||||
const port = art === "ssl" ? 993 : 143;
|
||||
setzen({ security: art, port });
|
||||
}}>
|
||||
<option value="ssl">{t("mailbox.secSsl")}</option>
|
||||
<option value="starttls">{t("mailbox.secStarttls")}</option>
|
||||
<option value="none">{t("mailbox.secNone")}</option>
|
||||
</select>
|
||||
</label>
|
||||
{form.security === "none" && <p className="muted small">{t("mailbox.secNoneWarn")}</p>}
|
||||
<label className="field">
|
||||
<span>{t("mailbox.port")}</span>
|
||||
<input type="number" value={form.port}
|
||||
@@ -216,16 +232,17 @@ function SenderForm({ konten, onAdd, t }) {
|
||||
const [von, setVon] = useState("");
|
||||
const [konto, setKonto] = useState("");
|
||||
return (
|
||||
<div className="field-inline" style={{ marginTop: "var(--sp-2)", marginBottom: 0 }}>
|
||||
<input placeholder={t("mailbox.fromPlaceholder")} value={von}
|
||||
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||
<input style={{ width: "100%" }} placeholder={t("mailbox.fromPlaceholder")} value={von}
|
||||
onChange={(e) => setVon(e.target.value)} />
|
||||
<select value={konto} onChange={(e) => setKonto(e.target.value)}>
|
||||
<select style={{ width: "100%", marginTop: 4 }} value={konto}
|
||||
onChange={(e) => setKonto(e.target.value)}>
|
||||
<option value="">{t("mailbox.noAccount")}</option>
|
||||
{konten.map((k) => <option key={k.id} value={k.id}>{k.name}</option>)}
|
||||
</select>
|
||||
<button type="button" className="btn sm" disabled={!von.trim()}
|
||||
<button type="button" className="btn sm" style={{ marginTop: 4 }} disabled={!von.trim()}
|
||||
onClick={() => { onAdd({ from_contains: von.trim(), account_id: konto ? Number(konto) : null }); setVon(""); }}>
|
||||
<Icon name="plus" size={14} />
|
||||
<Icon name="plus" size={14} /> {t("mailbox.addSender")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1146,3 +1146,7 @@ tr.row-active { background: var(--surface-2); }
|
||||
/* "Foto machen"/"Galerie" sind <label> und erben sonst das margin-bottom des
|
||||
globalen label-Stils – dann saessen sie hoeher als der <button> "Entfernen". */
|
||||
.foto-knoepfe .btn { cursor: pointer; margin: 0; }
|
||||
|
||||
/* Postfach-Eintrag: gestapelt, weil die Einstellungsspalte schmal ist. */
|
||||
.mail-source { padding: var(--sp-3) 0; border-bottom: 1px solid var(--border); }
|
||||
.mail-source:last-of-type { border-bottom: 0; }
|
||||
|
||||
Reference in New Issue
Block a user