chore(cert): fix small bugs with review

- shortError now truncates by rune count instead of bytes, so non-ASCII
  error messages (e.g. localized ACME / DNS provider errors) cannot be
  split mid-rune. TestShortError gains a CJK case asserting valid UTF-8.
- Cert.last_attempt_at is typed string | null on the frontend to reflect
  that the *time.Time pointer serializes as null for legacy / pre-attempt
  rows.
- Drop redundant ?. on refModal / refObtainCertLive in the three click
  handlers. The refs are bound to components rendered alongside their
  trigger button, so they are guaranteed to be mounted by the time the
  handler fires.
This commit is contained in:
Hintay
2026-05-23 06:45:45 +09:00
parent 2b5a403588
commit e005d2437f
6 changed files with 32 additions and 9 deletions
+6 -3
View File
@@ -226,14 +226,17 @@ func markCertSuccess(id uint64, sslCertificatePath, sslCertificateKeyPath string
// shortError trims and truncates an error for UI display in last_error.
// Returns "" for nil so a successful retry can clear the prior error.
// Truncation is rune-aware so non-ASCII error messages (e.g. localized
// ACME or DNS provider errors) cannot be split mid-rune.
func shortError(err error) string {
if err == nil {
return ""
}
msg := strings.TrimSpace(err.Error())
const max = 500
if len(msg) > max {
msg = msg[:max] + "…"
const maxRunes = 500
runes := []rune(msg)
if len(runes) > maxRunes {
msg = string(runes[:maxRunes]) + "…"
}
return msg
}
+14
View File
@@ -1,8 +1,10 @@
package certificate
import (
"strings"
"testing"
"time"
"unicode/utf8"
"github.com/0xJacky/Nginx-UI/internal/cert"
"github.com/0xJacky/Nginx-UI/model"
@@ -142,13 +144,25 @@ func TestMarkCertSuccessClearsLastError(t *testing.T) {
func TestShortError(t *testing.T) {
assert.Equal(t, "", shortError(nil))
assert.Equal(t, "hello", shortError(errString(" hello ")))
long := make([]byte, 600)
for i := range long {
long[i] = 'a'
}
got := shortError(errString(string(long)))
// 500 ASCII runes + the literal "…" suffix.
assert.Equal(t, 500+len("…"), len(got))
assert.Equal(t, "…", got[len(got)-len("…"):])
// Multi-byte runes must not be split: each CJK char is 3 bytes,
// 600 chars = 1800 bytes, which would corrupt the boundary if we
// sliced by bytes. After rune-aware truncation we expect exactly
// 500 CJK chars + the "…" suffix, and the result must be valid UTF-8.
multi := strings.Repeat("中", 600)
gotMulti := shortError(errString(multi))
assert.True(t, utf8.ValidString(gotMulti), "truncated message must be valid UTF-8")
assert.Equal(t, 500+1 /* ellipsis rune */, utf8.RuneCountInString(gotMulti))
assert.Equal(t, "…", gotMulti[len(gotMulti)-len("…"):])
}
type errString string
+1 -1
View File
@@ -34,7 +34,7 @@ export interface Cert extends ModelBase {
revoke_old: boolean
status: CertStatusType
last_error: string
last_attempt_at: string
last_attempt_at: string | null
}
export interface CertificateInfo {
@@ -90,8 +90,10 @@ function issueCert() {
step.value = 1
modalVisible.value = true
refObtainCertLive.value
?.issue_cert(computedMainDomain.value, computedDomains.value, data.value.key_type)
// ObtainCertLive is mounted in the same modal via force-render, so the
// ref is guaranteed to be available by the time this function runs.
refObtainCertLive.value!
.issue_cert(computedMainDomain.value, computedDomains.value, data.value.key_type)
.then(() => {
message.success($gettext('Issued successfully'))
emit('issued')
@@ -20,7 +20,9 @@ async function issueCert() {
await certStore.save()
message.success($gettext('Save successfully'))
refModal.value?.start().then(() => {
// refModal is mounted alongside this button via force-render, so it
// is guaranteed to be available by the time @click fires.
refModal.value!.start().then(() => {
message.success($gettext('Renew successfully'))
emit('renewed')
})
@@ -25,8 +25,10 @@ const issueOptions = computed<AutoCertOptions>(() => ({
}))
function openAndRetry() {
refModal.value
?.start()
// refModal is bound to a component rendered alongside this button,
// so it is guaranteed to be mounted by the time @click fires.
refModal.value!
.start()
.then(() => {
message.success($gettext('Certificate issued successfully'))
emit('retried')