Skip to content

Commit 45eb857

Browse files
tanviet12claude
andcommitted
fix: add APP_URL setting in UI to fix localhost link in Telegram/Email (#43)
- Settings > General: add "URL ứng dụng" field with validation - Backend: save app_url to app_settings, strip trailing slash - Dispatcher: getBaseURL reads DB setting > env > fallback localhost Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 66e043e commit 45eb857

3 files changed

Lines changed: 33 additions & 3 deletions

File tree

backend/api/handlers/settings.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package handlers
33
import (
44
"fmt"
55
"net/http"
6+
"strings"
67
"time"
78

89
"github.com/gin-gonic/gin"
@@ -157,6 +158,7 @@ func SaveGeneralSettings(c *gin.Context) {
157158
Timezone string `json:"timezone"`
158159
Language string `json:"language"`
159160
ExchangeRate float64 `json:"exchange_rate_vnd"`
161+
AppURL string `json:"app_url"`
160162
}
161163
if err := c.ShouldBindJSON(&req); err != nil {
162164
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "details": err.Error()})
@@ -182,6 +184,10 @@ func SaveGeneralSettings(c *gin.Context) {
182184
upsertSetting(tenantID, "exchange_rate_vnd", fmt.Sprintf("%.0f", req.ExchangeRate), nil)
183185
}
184186

187+
// Strip trailing slash from app URL
188+
appURL := strings.TrimRight(req.AppURL, "/")
189+
upsertSetting(tenantID, "app_url", appURL, nil)
190+
185191
c.JSON(http.StatusOK, gin.H{"message": "saved"})
186192
}
187193

backend/notifications/dispatcher.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func (d *Dispatcher) SendJobResults(ctx context.Context, job models.Job, run mod
8080

8181
// Use custom template if configured
8282
body := defaultBody
83-
link := fmt.Sprintf("%s/%s/jobs/%s", d.getBaseURL(), job.TenantID, job.ID)
83+
link := fmt.Sprintf("%s/%s/jobs/%s", d.getBaseURL(job.TenantID), job.TenantID, job.ID)
8484
if output.Template == "custom" && output.CustomTemplate != "" {
8585
body = d.renderCustomTemplate(output.CustomTemplate, job.Name, total, passed, failed, issues, defaultBody, link)
8686
}
@@ -197,10 +197,17 @@ func trimSpace(s string) string {
197197
return s[start:end]
198198
}
199199

200-
func (d *Dispatcher) getBaseURL() string {
200+
func (d *Dispatcher) getBaseURL(tenantID string) string {
201+
// Priority 1: tenant setting from DB
202+
var setting models.AppSetting
203+
if err := db.DB.Where("tenant_id = ? AND setting_key = ?", tenantID, "app_url").First(&setting).Error; err == nil && setting.ValuePlain != "" {
204+
return setting.ValuePlain
205+
}
206+
// Priority 2: environment variable
201207
if u := os.Getenv("APP_URL"); u != "" {
202208
return u
203209
}
210+
// Priority 3: fallback
204211
return "http://localhost:8080"
205212
}
206213

frontend/src/views/Settings.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@
118118
class="mb-3"
119119
/>
120120

121+
<v-text-field
122+
v-model="generalSettings.appUrl"
123+
label="URL ứng dụng"
124+
placeholder="https://cqa.yourdomain.com"
125+
hint="Cấu hình URL để hệ thống gửi link chính xác qua Telegram và Email"
126+
persistent-hint
127+
:rules="appUrlRules"
128+
class="mb-3"
129+
/>
130+
121131
<v-btn color="primary" :loading="savingGeneral" @click="saveGeneral">{{ $t('save_settings') }}</v-btn>
122132
</v-card>
123133
</v-col>
@@ -165,7 +175,12 @@ const geminiModels = [
165175
]
166176
167177
const aiSettings = reactive({ provider: 'claude', model: 'claude-sonnet-4-6', apiKey: '', batchMode: true, batchSize: 5 })
168-
const generalSettings = reactive({ companyName: '', timezone: 'Asia/Ho_Chi_Minh', language: 'vi', exchangeRate: 26000 })
178+
const generalSettings = reactive({ companyName: '', timezone: 'Asia/Ho_Chi_Minh', language: 'vi', exchangeRate: 26000, appUrl: '' })
179+
180+
const appUrlRules = [
181+
(v: string) => !v || /^https?:\/\/.+/.test(v) || 'URL phải bắt đầu bằng http:// hoặc https://',
182+
(v: string) => !v || !v.endsWith('/') || 'URL không nên có dấu / ở cuối',
183+
]
169184
170185
const modelOptions = computed(() => {
171186
return aiSettings.provider === 'claude' ? claudeModels : geminiModels
@@ -185,6 +200,7 @@ async function loadSettings() {
185200
if (data.settings.ai_batch_mode) aiSettings.batchMode = data.settings.ai_batch_mode === 'true'
186201
if (data.settings.ai_batch_size) aiSettings.batchSize = parseInt(data.settings.ai_batch_size) || 5
187202
if (data.settings.exchange_rate_vnd) generalSettings.exchangeRate = parseFloat(data.settings.exchange_rate_vnd) || 26000
203+
if (data.settings.app_url) generalSettings.appUrl = data.settings.app_url
188204
if (data.tenant) {
189205
generalSettings.companyName = data.tenant.name || ''
190206
generalSettings.timezone = data.tenant.timezone || 'Asia/Ho_Chi_Minh'
@@ -254,6 +270,7 @@ async function saveGeneral() {
254270
timezone: generalSettings.timezone,
255271
language: generalSettings.language,
256272
exchange_rate_vnd: generalSettings.exchangeRate,
273+
app_url: generalSettings.appUrl,
257274
})
258275
showSnack(t('success'), 'success')
259276
} catch (err: any) {

0 commit comments

Comments
 (0)