Skip to content

Commit d80b749

Browse files
committed
feature(translations): add dynamic seconds placeholder
1 parent 2fe0d73 commit d80b749

File tree

4 files changed

+58
-43
lines changed

4 files changed

+58
-43
lines changed

public/queue-page.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@
142142
</div>
143143

144144
<div class="countdown-message">
145-
[[ printf .Translations.RefreshTime .QueueData.RefreshInterval ]]
145+
[[ printf .Translations.RefreshTime .QueueData.RefreshInterval | safeHtml ]]
146146
</div>
147147

148148
<div class="debug-info">

public/translations.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"introduction": "Our services are currently experiencing high demand. Your patience is appreciated. You will be automatically redirected when it's your turn.",
66
"yourPosition": "Your Position",
77
"eta": "Estimated Wait Time",
8-
"refreshTime": "This page will automatically refresh in %d seconds",
8+
"refreshTime": "This page will automatically refresh in <span id=\"countdown\">%d</span> seconds",
99
"mins": "min(s)"
1010
},
1111
"fr": {
@@ -14,7 +14,7 @@
1414
"introduction": "Nos services rencontrent actuellement une forte demande. Merci pour votre patience. Vous serez automatiquement redirigé lorsque ce sera votre tour.",
1515
"yourPosition": "Votre position",
1616
"eta": "Temps d’attente estimé",
17-
"refreshTime": "Cette page se rafraîchira automatiquement dans %d secondes",
17+
"refreshTime": "Cette page se rafraîchira automatiquement dans <span id=\"countdown\">%d</span> secondes",
1818
"mins": "min(s)"
1919
},
2020
"es": {
@@ -23,7 +23,7 @@
2323
"introduction": "Nuestros servicios están experimentando una alta demanda. Agradecemos tu paciencia. Serás redirigido automáticamente cuando sea tu turno.",
2424
"yourPosition": "Tu posición",
2525
"eta": "Tiempo de espera estimado",
26-
"refreshTime": "Esta página se actualizará automáticamente en %d segundos",
26+
"refreshTime": "Esta página se actualizará automáticamente en <span id=\"countdown\">%d</span> segundos",
2727
"mins": "min(s)"
2828
},
2929
"de": {
@@ -32,7 +32,7 @@
3232
"introduction": "Unsere Dienste verzeichnen derzeit eine hohe Nachfrage. Vielen Dank für Ihre Geduld. Sie werden automatisch weitergeleitet, wenn Sie an der Reihe sind.",
3333
"yourPosition": "Ihre Position",
3434
"eta": "Geschätzte Wartezeit",
35-
"refreshTime": "Diese Seite wird in %d Sekunden automatisch aktualisiert",
35+
"refreshTime": "Diese Seite wird in <span id=\"countdown\">%d</span> Sekunden automatisch aktualisiert",
3636
"mins": "min(s)"
3737
},
3838
"pt": {
@@ -41,7 +41,7 @@
4141
"introduction": "Nossos serviços estão com alta demanda no momento. Agradecemos sua paciência. Você será redirecionado automaticamente quando for a sua vez.",
4242
"yourPosition": "Sua posição",
4343
"eta": "Tempo de espera estimado",
44-
"refreshTime": "Esta página será atualizada automaticamente em %d segundos",
44+
"refreshTime": "Esta página será atualizada automaticamente em <span id=\"countdown\">%d</span> segundos",
4545
"mins": "min(s)"
4646
},
4747
"it": {
@@ -50,7 +50,7 @@
5050
"introduction": "I nostri servizi stanno riscontrando un'alta domanda. Ti ringraziamo per la pazienza. Verrai reindirizzato automaticamente quando sarà il tuo turno.",
5151
"yourPosition": "La tua posizione",
5252
"eta": "Tempo di attesa stimato",
53-
"refreshTime": "Questa pagina si aggiornerà automaticamente tra %d secondi",
53+
"refreshTime": "Questa pagina si aggiornerà automaticamente tra <span id=\"countdown\">%d</span> secondi",
5454
"mins": "min(s)"
5555
}
5656
}

queue-manager.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,11 @@ func (qm *QueueManager) loadTemplate() error {
311311
return fmt.Errorf("error reading template file '%s': %w", qm.config.QueuePageFile, err)
312312
}
313313

314-
newTpl, parseErr := template.New("QueuePage").Delims("[[", "]]").Parse(string(content))
314+
newTpl, parseErr := template.New("QueuePage").Delims("[[", "]]").Funcs(template.FuncMap{
315+
"safeHtml": func(s string) template.HTML {
316+
return template.HTML(s)
317+
},
318+
}).Parse(string(content))
315319
if parseErr != nil {
316320
qm.tpl = nil
317321
return fmt.Errorf("error parsing template '%s': %w", qm.config.QueuePageFile, parseErr)
@@ -744,7 +748,7 @@ func (qm *QueueManager) prepareQueuePageData(positionInQueue int) QueuePageData
744748
// serveFallbackTemplate provides a basic, hardcoded HTML queue page.
745749
func (qm *QueueManager) serveFallbackTemplate(rw http.ResponseWriter, data QueueTemplateData) {
746750
// Minified and slightly improved fallback HTML
747-
fallbackHTML := `<!DOCTYPE html><html><head><title>Service Queue</title><meta http-equiv="refresh" content="[[.RefreshInterval]]"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:Arial,sans-serif;text-align:center;margin:20px;padding:0;background-color:#f4f4f4;color:#333;} .container{max-width:600px;margin:40px auto;padding:20px;background-color:white;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.1);} h1{color:#2c3e50;margin-bottom:15px;} p{line-height:1.6;} .progress-container{width:100%;background-color:#e9ecef;border-radius:5px;margin:25px 0;overflow:hidden;} .progress-bar{height:24px;width:[[.ProgressPercentage]]%;background-color:#3498db;text-align:center;line-height:24px;color:white;font-weight:bold;transition:width .3s ease;} .info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:15px;margin:20px 0;} .info-box{background-color:#f8f9fa;padding:15px;border-radius:5px;border-left:4px solid #3498db;} .info-box strong{display:block;margin-bottom:5px;color:#2c3e50;} .debug{font-size:0.85em;color:#7f8c8d;margin-top:20px;padding:10px;background-color:#ecf0f1;border-radius:4px;text-align:left;display:[[if .DebugInfo]]block[[else]]none[[end]];}</style></head><body><div class="container"><h1>You're in the Queue</h1><p>Our service is currently experiencing high demand. Please wait, and this page will refresh automatically.</p><div class="progress-container"><div class="progress-bar">[[.ProgressPercentage]]%</div></div><div class="info-grid"><div class="info-box"><strong>Your Position</strong>[[.Position]] / [[.QueueSize]]</div><div class="info-box"><strong>Est. Wait Time</strong>~[[.EstimatedWaitTime]] min(s)</div></div><p>This page will refresh in <span id="countdown">[[.RefreshInterval]]</span> seconds.</p><div class="debug"><strong>Debug Info:</strong> <pre>[[.DebugInfo]]</pre></div></div><script>let s=[[.RefreshInterval]];const e=document.getElementById("countdown");function n(){s--,e.textContent=s,s<=0&&window.location.reload(!0)}e&&setInterval(n,1e3);</script></body></html>`
751+
fallbackHTML := `<!DOCTYPE html><html><head><title>Service Queue</title><meta http-equiv="refresh" content="[[.QueueData.RefreshInterval]]"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>body{font-family:Arial,sans-serif;text-align:center;margin:20px;padding:0;background-color:#f4f4f4;color:#333;} .container{max-width:600px;margin:40px auto;padding:20px;background-color:white;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.1);} h1{color:#2c3e50;margin-bottom:15px;} p{line-height:1.6;} .progress-container{width:100%;background-color:#e9ecef;border-radius:5px;margin:25px 0;overflow:hidden;} .progress-bar{height:24px;width:[[.QueueData.ProgressPercentage]]%;background-color:#3498db;text-align:center;line-height:24px;color:white;font-weight:bold;transition:width .3s ease;} .info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:15px;margin:20px 0;} .info-box{background-color:#f8f9fa;padding:15px;border-radius:5px;border-left:4px solid #3498db;} .info-box strong{display:block;margin-bottom:5px;color:#2c3e50;} .debug{font-size:0.85em;color:#7f8c8d;margin-top:20px;padding:10px;background-color:#ecf0f1;border-radius:4px;text-align:left;display:[[if .QueueData.DebugInfo]]block[[else]]none[[end]];}</style></head><body><div class="container"><h1>You're in the Queue</h1><p>Our service is currently experiencing high demand. Please wait, and this page will refresh automatically.</p><div class="progress-container"><div class="progress-bar">[[.QueueData.ProgressPercentage]]%</div></div><div class="info-grid"><div class="info-box"><strong>Your Position</strong>[[.QueueData.Position]] / [[.QueueData.QueueSize]]</div><div class="info-box"><strong>Est. Wait Time</strong>~[[.QueueData.EstimatedWaitTime]] min(s)</div></div><p>This page will refresh in <span id="countdown">[[.QueueData.RefreshInterval]]</span> seconds.</p><div class="debug"><strong>Debug Info:</strong> <pre>[[.QueueData.DebugInfo]]</pre></div></div><script>let s=[[.QueueData.RefreshInterval]];const e=document.getElementById("countdown");function n(){s--,e.textContent=s,s<=0&&window.location.reload(!0)}e&&setInterval(n,1e3);</script></body></html>`
748752

749753
tmpl, err := template.New("FallbackQueuePage").Delims("[[", "]]").Parse(fallbackHTML)
750754
if err != nil {

queue-manager_test.go

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ package traefik_queue_manager
44
import (
55
"bytes"
66
"context"
7-
87
"io"
98
"log"
109
"net/http"
@@ -192,7 +191,7 @@ func TestGetClientID_NoCookies_IPUserAgentHash(t *testing.T) {
192191
id1, _ := qm.getClientID(rr1, req1)
193192

194193
req2 := httptest.NewRequest("GET", "/", nil)
195-
req2.RemoteAddr = "1.2.3.4:456" // Same IP, different port
194+
req2.RemoteAddr = "1.2.3.4:456" // Same IP, different port
196195
req2.Header.Set("User-Agent", "TestAgent1") // Same User-Agent
197196
rr2 := httptest.NewRecorder()
198197
id2, _ := qm.getClientID(rr2, req2)
@@ -227,7 +226,6 @@ func TestFileExists(t *testing.T) {
227226
f.Close()
228227
defer os.Remove(tempFileName)
229228

230-
231229
if !fileExists(tempFileName) {
232230
t.Errorf("fileExists returned false for an existing file: %s", tempFileName)
233231
}
@@ -236,7 +234,6 @@ func TestFileExists(t *testing.T) {
236234
}
237235
}
238236

239-
240237
func TestServeHTTP_CapacityAvailable(t *testing.T) {
241238
cfg := CreateConfig()
242239
cfg.MaxEntries = 1
@@ -246,7 +243,6 @@ func TestServeHTTP_CapacityAvailable(t *testing.T) {
246243
var logBuf bytes.Buffer
247244
logger := log.New(&logBuf, "", 0)
248245

249-
250246
nextCalled := false
251247
nextHandler := &mockNext{serveHTTPFunc: func(w http.ResponseWriter, r *http.Request) {
252248
nextCalled = true
@@ -263,7 +259,6 @@ func TestServeHTTP_CapacityAvailable(t *testing.T) {
263259
defer qmInst.Stop()
264260
}
265261

266-
267262
req := httptest.NewRequest("GET", "/", nil)
268263
rr := httptest.NewRecorder()
269264

@@ -281,7 +276,7 @@ func TestServeHTTP_CapacityAvailable(t *testing.T) {
281276

282277
func TestServeHTTP_NoCapacity_QueueUser(t *testing.T) {
283278
cfg := CreateConfig()
284-
cfg.MaxEntries = 0 // Force no capacity
279+
cfg.MaxEntries = 0 // Force no capacity
285280
cfg.QueuePageFile, _ = createTempFile(t, "Queue Page: [[.Position]]") // Valid template file
286281
defer os.Remove(cfg.QueuePageFile)
287282
cfg.Debug = true
@@ -305,7 +300,6 @@ func TestServeHTTP_NoCapacity_QueueUser(t *testing.T) {
305300
defer qmInst.Stop()
306301
}
307302

308-
309303
req := httptest.NewRequest("GET", "/", nil)
310304
rr := httptest.NewRecorder()
311305

@@ -322,20 +316,18 @@ func TestServeHTTP_NoCapacity_QueueUser(t *testing.T) {
322316
}
323317
}
324318

325-
326319
func TestServeHTTP_MultipleUsers_QueueAndProceed(t *testing.T) {
327320
cfg := CreateConfig()
328321
cfg.MaxEntries = 1
329322
cfg.InactivityTimeoutSeconds = 1 // Short for testing expiry
330323
cfg.CleanupIntervalSeconds = 1 // Short for testing cleanup
331-
cfg.QueuePageFile, _ = createTempFile(t, "Queue: Pos [[.Position]] Size [[.QueueSize]] Wait [[.EstimatedWaitTime]]")
324+
cfg.QueuePageFile, _ = createTempFile(t, "Queue: Pos [[.QueueData.Position]] Size [[.QueueData.QueueSize]] Wait [[.QueueData.EstimatedWaitTime]]")
332325
defer os.Remove(cfg.QueuePageFile)
333326
cfg.Debug = true
334327

335328
var logBuf bytes.Buffer
336329
logger := log.New(&logBuf, "", 0)
337330

338-
339331
var serviceAccessCount int
340332
var mu sync.Mutex // To protect serviceAccessCount
341333

@@ -405,10 +397,9 @@ func TestServeHTTP_MultipleUsers_QueueAndProceed(t *testing.T) {
405397
// Inactivity is 1s, Cleanup is 1s. Wait a bit longer.
406398
time.Sleep(time.Duration(cfg.InactivityTimeoutSeconds+cfg.CleanupIntervalSeconds+1) * time.Second)
407399

408-
409400
// --- Client 2 again (should now get access) ---
410401
req3 := httptest.NewRequest("GET", "/client2-retry", nil) // New request path for clarity
411-
req3.Header.Set("X-Test-Client-ID", "client2") // Same client ID
402+
req3.Header.Set("X-Test-Client-ID", "client2") // Same client ID
412403
// Add cookie that client 2 would have received from its first (queued) request
413404
cookiesClient2 := rr2.Result().Cookies()
414405
for _, c := range cookiesClient2 {
@@ -436,50 +427,70 @@ func TestServeHTTP_MultipleUsers_QueueAndProceed(t *testing.T) {
436427
mu.Unlock()
437428
}
438429

439-
440430
func TestPrepareQueuePageData(t *testing.T) {
441431
qm := &QueueManager{
442-
config: CreateConfig(),
443-
logger: log.New(io.Discard, "", 0),
444-
queue: make([]Session, 0),
432+
config: CreateConfig(),
433+
logger: log.New(io.Discard, "", 0),
434+
queue: make([]Session, 0),
445435
activeSessionIDs: make(map[string]bool),
446436
}
447437
qm.config.MinWaitTimeMinutes = 1
448438
qm.config.RefreshIntervalSeconds = 15
449439

450440
// Scenario 1: Empty queue, new user (pos 0)
451441
data1 := qm.prepareQueuePageData(0) // pos 0 (first in line)
452-
if data1.Position != 1 { t.Errorf("Expected Position 1, got %d", data1.Position) }
453-
if data1.QueueSize != 0 { t.Errorf("Expected QueueSize 0, got %d", data1.QueueSize) }
454-
if data1.EstimatedWaitTime != 1 { t.Errorf("Expected EstimatedWaitTime %d, got %d", qm.config.MinWaitTimeMinutes, data1.EstimatedWaitTime) } // Min wait time
455-
if data1.ProgressPercentage != 99 {t.Errorf("Expected ProgressPercentage 99, got %d", data1.ProgressPercentage)}
456-
442+
if data1.Position != 1 {
443+
t.Errorf("Expected Position 1, got %d", data1.Position)
444+
}
445+
if data1.QueueSize != 0 {
446+
t.Errorf("Expected QueueSize 0, got %d", data1.QueueSize)
447+
}
448+
if data1.EstimatedWaitTime != 1 {
449+
t.Errorf("Expected EstimatedWaitTime %d, got %d", qm.config.MinWaitTimeMinutes, data1.EstimatedWaitTime)
450+
} // Min wait time
451+
if data1.ProgressPercentage != 99 {
452+
t.Errorf("Expected ProgressPercentage 99, got %d", data1.ProgressPercentage)
453+
}
457454

458455
// Scenario 2: Queue with 5 people, user is 3rd in line (pos 2)
459-
qm.queue = make([]Session, 5) // Simulate 5 people in queue
456+
qm.queue = make([]Session, 5) // Simulate 5 people in queue
460457
data2 := qm.prepareQueuePageData(2) // 0-indexed position 2 (3rd person)
461-
if data2.Position != 3 { t.Errorf("Expected Position 3, got %d", data2.Position) }
462-
if data2.QueueSize != 5 { t.Errorf("Expected QueueSize 5, got %d", data2.QueueSize) }
458+
if data2.Position != 3 {
459+
t.Errorf("Expected Position 3, got %d", data2.Position)
460+
}
461+
if data2.QueueSize != 5 {
462+
t.Errorf("Expected QueueSize 5, got %d", data2.QueueSize)
463+
}
463464
// Wait factor for pos 2 (0-indexed) is 0.3 + (2%5 * 0.08) = 0.3 + 0.16 = 0.46
464465
// Raw wait = 2 * 0.46 = 0.92. Ceil(0.92) = 1. Max(1, MinWaitTime=1) = 1
465-
if data2.EstimatedWaitTime != 1 { t.Errorf("Expected EstimatedWaitTime 1, got %d", data2.EstimatedWaitTime) }
466+
if data2.EstimatedWaitTime != 1 {
467+
t.Errorf("Expected EstimatedWaitTime 1, got %d", data2.EstimatedWaitTime)
468+
}
466469
// Progress: (5 - (2+1)) / 5 * 100 = (5-3)/5 * 100 = 2/5 * 100 = 40%
467-
if data2.ProgressPercentage != 40 { t.Errorf("Expected ProgressPercentage 40, got %d", data2.ProgressPercentage) }
468-
470+
if data2.ProgressPercentage != 40 {
471+
t.Errorf("Expected ProgressPercentage 40, got %d", data2.ProgressPercentage)
472+
}
469473

470474
// Scenario 3: Queue with 1 person, user is 1st (pos 0)
471475
qm.queue = make([]Session, 1)
472476
data3 := qm.prepareQueuePageData(0)
473-
if data3.Position != 1 { t.Errorf("Expected Position 1, got %d", data3.Position) }
474-
if data3.QueueSize != 1 { t.Errorf("Expected QueueSize 1, got %d", data3.QueueSize) }
475-
if data3.EstimatedWaitTime != 1 { t.Errorf("Expected EstimatedWaitTime %d, got %d", qm.config.MinWaitTimeMinutes, data3.EstimatedWaitTime) }
477+
if data3.Position != 1 {
478+
t.Errorf("Expected Position 1, got %d", data3.Position)
479+
}
480+
if data3.QueueSize != 1 {
481+
t.Errorf("Expected QueueSize 1, got %d", data3.QueueSize)
482+
}
483+
if data3.EstimatedWaitTime != 1 {
484+
t.Errorf("Expected EstimatedWaitTime %d, got %d", qm.config.MinWaitTimeMinutes, data3.EstimatedWaitTime)
485+
}
476486
// Progress: (1 - (0+1)) / 1 * 100 = 0. Should be adjusted.
477487
// The logic is: if queueSize == 1 && positionInQueue == 0 -> 50%
478-
if data3.ProgressPercentage != 50 {t.Errorf("Expected ProgressPercentage 50 for single item queue, got %d", data3.ProgressPercentage)}
488+
if data3.ProgressPercentage != 50 {
489+
t.Errorf("Expected ProgressPercentage 50 for single item queue, got %d", data3.ProgressPercentage)
490+
}
479491
}
480492

481-
482493
// Note: Add more comprehensive tests, especially for CleanupExpiredSessions scenarios
483494
// including hard session limits, and more edge cases for ServeHTTP.
484495
// Mocking time (time.Now()) would make these tests more robust and faster,
485-
// but it adds complexity (e.g., using an interface for time or a library).
496+
// but it adds complexity (e.g., using an interface for time or a library).

0 commit comments

Comments
 (0)