fix(gmail): stop hiding why it failed, twice over
Release / Build and publish image (push) Failing after 10m6s
Release / Build and publish image (push) Failing after 10m6s
Reported from use: "Fetch now" said "something went wrong" and the error log was empty. Both halves were the same bug wearing two hats. `handleGmailSyncNow` answered 502 with the code `internal_error`, which the interface maps to a generic sentence — so Google's own words, which are the whole diagnosis, were replaced by one that says nothing. And `writeError` records nothing; only `s.internal` does. So there was no message to read and nowhere to look it up afterwards. `s.upstream()` is the answer: 502 because this is somebody else's service saying no rather than this server breaking, a code of its own so the interface can quote what was said, and a row in the error log — which is the entire reason that log exists. A Gmail sync failing quietly for a week is otherwise invisible: the background job writes a line the next restart erases. The same connection also left the mailbox unnamed. The address is learned at connect time and that call can fail; a warning went to the log and an empty string to the database, so the page said "an unknown account" and kept saying it. The only way back was to disconnect and reconnect, which nobody would guess. The status endpoint asks again when it finds none, so a wrong answer lasts until the next look rather than until somebody reconnects. Ten Danish strings were hiding in `confirm()` and `app.toast()` calls, including every destructive confirmation in the app — the worst possible set to leave in one language, since it is what somebody reads in the half-second before agreeing to lose something. The i18n guard reads markup and attributes, and a string argument lives in the script block. It reads those now, and found four my own grep missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
95378a1199
commit
c4e480a0c4
@@ -55,6 +55,9 @@ const MESSAGES = {
|
||||
ai_not_configured: 'error.aiNotConfigured',
|
||||
totp_not_enabled: 'error.totpNotEnabled',
|
||||
inbox_protected: 'error.inboxProtected',
|
||||
// The fallback for when Gmail refused and said nothing usable. When it did say
|
||||
// something, `humanMessage` quotes it instead — see below.
|
||||
gmail_failed: 'error.gmailRefused',
|
||||
last_admin: 'error.lastAdmin'
|
||||
};
|
||||
|
||||
@@ -67,6 +70,15 @@ const MESSAGES = {
|
||||
*/
|
||||
export function humanMessage(err) {
|
||||
if (!(err instanceof ApiError)) return t(MESSAGES.offline);
|
||||
|
||||
// Gmail's own words are the diagnosis — "invalid_grant", "insufficient
|
||||
// authentication scopes" — and a generic sentence in their place throws away
|
||||
// the only thing that says what to do. Wrapped so it reads as a quotation
|
||||
// rather than as a stray English string in a Danish interface.
|
||||
if (err.code === 'gmail_failed' && err.message) {
|
||||
return t('error.gmailSaid', { what: err.message });
|
||||
}
|
||||
|
||||
const key = MESSAGES[err.code];
|
||||
if (key) return t(key);
|
||||
return err.message ?? t(MESSAGES.internal_error);
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
);
|
||||
} catch {
|
||||
app.replace(id, previous);
|
||||
app.toast('Kunne ikke flytte opgaven.');
|
||||
app.toast(t('task.moveFailed'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
}
|
||||
|
||||
async function removeComment(comment) {
|
||||
if (!confirm('Slet kommentaren?')) return;
|
||||
if (!confirm(t('detail.deleteComment'))) return;
|
||||
const previous = comments;
|
||||
comments = comments.filter((c) => c.id !== comment.id);
|
||||
try {
|
||||
|
||||
@@ -646,5 +646,24 @@ export const da = {
|
||||
'update.waiting': 'Venter på at serveren kommer op igen…',
|
||||
'update.stillDown': 'Serveren er ikke kommet op endnu. Kig i panelet.',
|
||||
'update.notConfigured':
|
||||
'Denne instans kan ikke genstarte sig selv: {missing} er ikke sat. En container kan ikke udskifte sit eget image — panelet gør arbejdet, og det her er kun måden at bede om det.'
|
||||
'Denne instans kan ikke genstarte sig selv: {missing} er ikke sat. En container kan ikke udskifte sit eget image — panelet gør arbejdet, og det her er kun måden at bede om det.',
|
||||
|
||||
'error.gmailSaid': 'Gmail svarede: {what}',
|
||||
|
||||
'error.gmailRefused': 'Gmail afviste kaldet. Prøv at forbinde postkassen igen.',
|
||||
|
||||
'int.newAddressQuestion': 'Ny adresse? Mail til den gamle bliver afvist.',
|
||||
'int.copied': 'Kopieret.',
|
||||
'int.copyFailed': 'Kunne ikke kopiere — markér teksten i stedet.',
|
||||
|
||||
'detail.deleteComment': 'Slet kommentaren?',
|
||||
'int.disconnectQuestion': 'Afbryd forbindelsen til Gmail? Tokens bliver glemt.',
|
||||
'int.newLinkQuestion': 'Nyt feed-link? Alle eksisterende abonnementer holder op med at virke.',
|
||||
'data.deleteTemplate': 'Slet skabelonen "{name}"?',
|
||||
'tokens.revokeQuestion': 'Tilbagekald "{name}"? Alt, der bruger den, holder op med at virke.',
|
||||
'account.signOutThisDevice': 'Det er denne enhed. Du bliver logget ud.',
|
||||
|
||||
'project.deleteQuestion': 'Slet "{name}" med alle dets opgaver?',
|
||||
'project.deleteRecoverable':
|
||||
'Det havner i papirkurven og kan hentes tilbage under Indstillinger → Data og skabeloner i {days} dage.'
|
||||
};
|
||||
|
||||
@@ -641,5 +641,24 @@ export const en = {
|
||||
'update.waiting': 'Waiting for the server to come back…',
|
||||
'update.stillDown': 'The server has not come back yet. Have a look in the panel.',
|
||||
'update.notConfigured':
|
||||
'This instance cannot restart itself: {missing} are not set. A container cannot replace its own image — the panel does the work, and this is only the way to ask.'
|
||||
'This instance cannot restart itself: {missing} are not set. A container cannot replace its own image — the panel does the work, and this is only the way to ask.',
|
||||
|
||||
'error.gmailSaid': 'Gmail said: {what}',
|
||||
|
||||
'error.gmailRefused': 'Gmail refused the call. Try connecting the mailbox again.',
|
||||
|
||||
'int.newAddressQuestion': 'A new address? Mail to the old one is refused.',
|
||||
'int.copied': 'Copied.',
|
||||
'int.copyFailed': 'Could not copy — select the text instead.',
|
||||
|
||||
'detail.deleteComment': 'Delete the comment?',
|
||||
'int.disconnectQuestion': 'Disconnect Gmail? The tokens are forgotten.',
|
||||
'int.newLinkQuestion': 'A new feed link? Every existing subscription stops working.',
|
||||
'data.deleteTemplate': 'Delete the template "{name}"?',
|
||||
'tokens.revokeQuestion': 'Revoke "{name}"? Everything using it stops working.',
|
||||
'account.signOutThisDevice': 'That is this device. You will be signed out.',
|
||||
|
||||
'project.deleteQuestion': 'Delete "{name}" with all its tasks?',
|
||||
'project.deleteRecoverable':
|
||||
'It goes to the trash and can be restored under Settings → Data and templates for {days} days.'
|
||||
};
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
|
||||
async function endSession(session) {
|
||||
if (session.current) {
|
||||
if (!confirm('Det er denne enhed. Du bliver logget ud.')) return;
|
||||
if (!confirm(t('account.signOutThisDevice'))) return;
|
||||
await api.endSession(session.id);
|
||||
// A full reload rather than clearing the store by hand: the session is
|
||||
// gone, so every open socket and every cached answer in this tab is now
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
}
|
||||
|
||||
async function deleteTemplate(template) {
|
||||
if (!confirm(`Slet skabelonen "${template.name}"?`)) return;
|
||||
if (!confirm(t('data.deleteTemplate', { name: template.name }))) return;
|
||||
|
||||
const previous = templates;
|
||||
templates = templates.filter((t) => t.id !== template.id);
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
}
|
||||
|
||||
async function disconnectGmail() {
|
||||
if (!confirm('Afbryd forbindelsen til Gmail? Tokens bliver glemt.')) return;
|
||||
if (!confirm(t('int.disconnectQuestion'))) return;
|
||||
try {
|
||||
await api.disconnectGmail();
|
||||
gmail = await api.getGmail();
|
||||
@@ -153,7 +153,7 @@
|
||||
});
|
||||
|
||||
async function rotateFeed() {
|
||||
if (!confirm('Nyt feed-link? Alle eksisterende abonnementer holder op med at virke.')) return;
|
||||
if (!confirm(t('int.newLinkQuestion'))) return;
|
||||
try {
|
||||
feedURL = (await api.rotateFeed()).url;
|
||||
} catch (e) {
|
||||
@@ -162,7 +162,7 @@
|
||||
}
|
||||
|
||||
async function rotateMail() {
|
||||
if (!confirm('Ny adresse? Mail til den gamle bliver afvist.')) return;
|
||||
if (!confirm(t('int.newAddressQuestion'))) return;
|
||||
try {
|
||||
const r = await api.rotateMailAddress();
|
||||
mailAddress = r.address;
|
||||
@@ -175,9 +175,9 @@
|
||||
async function copy(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
app.toast('Kopieret.');
|
||||
app.toast(t('int.copied'));
|
||||
} catch {
|
||||
app.toast('Kunne ikke kopiere — markér teksten i stedet.');
|
||||
app.toast(t('int.copyFailed'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
} catch {
|
||||
// Insecure context, or permission refused. The token is on screen and
|
||||
// selectable, so this is a convenience that failed, not the feature.
|
||||
app.toast('Kunne ikke kopiere — markér teksten i stedet.');
|
||||
app.toast(t('int.copyFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,12 +69,12 @@
|
||||
await navigator.clipboard.writeText(connectorURL);
|
||||
connectorCopied = true;
|
||||
} catch {
|
||||
app.toast('Kunne ikke kopiere — markér teksten i stedet.');
|
||||
app.toast(t('int.copyFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
async function revoke(token) {
|
||||
if (!confirm(`Tilbagekald "${token.name}"? Alt, der bruger den, holder op med at virke.`))
|
||||
if (!confirm(t('tokens.revokeQuestion', { name: token.name })))
|
||||
return;
|
||||
|
||||
const previous = tokens;
|
||||
|
||||
@@ -188,9 +188,9 @@
|
||||
async function remove() {
|
||||
const days = 30;
|
||||
const confirmed = confirm(
|
||||
`Slet "${project.name}" med alle dets opgaver?\n\n` +
|
||||
`Det havner i papirkurven og kan hentes tilbage under ` +
|
||||
`Indstillinger → Data og skabeloner i ${days} dage.`
|
||||
t('project.deleteQuestion', { name: projectName(project) }) +
|
||||
'\n\n' +
|
||||
t('project.deleteRecoverable', { days })
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user