chore: coderabbit suggestions

This commit is contained in:
jamesread 2026-07-19 00:03:12 +01:00
parent c72d180fae
commit c5f0387045
17 changed files with 161 additions and 54 deletions

View File

@ -38,28 +38,28 @@ repos:
entry: make service-codestyle entry: make service-codestyle
language: system language: system
pass_filenames: false pass_filenames: false
files: ^(service/|proto/|lang/) files: ^(service/|proto/|lang/|Makefile)
- id: frontend-codestyle - id: frontend-codestyle
name: frontend-codestyle name: frontend-codestyle
entry: make frontend-codestyle entry: make frontend-codestyle
language: system language: system
pass_filenames: false pass_filenames: false
files: ^frontend/ files: ^(frontend/|Makefile)
- id: service-unittests - id: service-unittests
name: service-unittests name: service-unittests
entry: make service-unittests entry: make service-unittests
language: system language: system
pass_filenames: false pass_filenames: false
files: ^(service/|proto/|lang/) files: ^(service/|proto/|lang/|Makefile)
- id: service-build - id: service-build
name: service-build name: service-build
entry: make service entry: make service
language: system language: system
pass_filenames: false pass_filenames: false
files: ^(service/|proto/|lang/) files: ^(service/|proto/|lang/|Makefile)
- id: it - id: it
name: integration-tests name: integration-tests

View File

@ -33,7 +33,7 @@
"stylelint-config-standard": "^40.0.0" "stylelint-config-standard": "^40.0.0"
}, },
"engines": { "engines": {
"node": ">=22.0.0" "node": "^20.19.0 || >=22.12.0"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {

View File

@ -41,6 +41,6 @@
"vue-router": "^5.1.0" "vue-router": "^5.1.0"
}, },
"engines": { "engines": {
"node": ">=22.0.0" "node": "^20.19.0 || >=22.12.0"
} }
} }

View File

@ -247,10 +247,16 @@ function updateFromJson (json) {
if (json.datetimeRateLimitExpires) { if (json.datetimeRateLimitExpires) {
const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T')) const date = new Date(json.datetimeRateLimitExpires.replace(' ', 'T'))
rateLimitExpires.value = date.getTime() / 1000 rateLimitExpires.value = date.getTime() / 1000
if (bindingId.value) {
rateLimits[bindingId.value] = rateLimitExpires.value
}
updateRateLimitStatus() updateRateLimitStatus()
} else if (json.datetimeRateLimitExpires === '') { } else if (json.datetimeRateLimitExpires === '') {
// Explicitly clear if empty string // Explicitly clear if empty string
rateLimitExpires.value = 0 rateLimitExpires.value = 0
if (bindingId.value) {
rateLimits[bindingId.value] = 0
}
updateRateLimitStatus() updateRateLimitStatus()
} }
} }
@ -359,6 +365,15 @@ async function pollExecutionUntilDone (trackingId) {
} }
} }
let stopButtonResultWatch = null
function stopWatchingButtonResult () {
if (stopButtonResultWatch) {
stopButtonResultWatch()
stopButtonResultWatch = null
}
}
async function startAction (actionArgs) { async function startAction (actionArgs) {
buttonClasses.value = [] // Removes old animation classes buttonClasses.value = [] // Removes old animation classes
@ -376,7 +391,8 @@ async function startAction (actionArgs) {
console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId) console.log('Watching buttonResults for', startActionArgs.uniqueTrackingId)
watch( stopWatchingButtonResult()
stopButtonResultWatch = watch(
() => buttonResults[startActionArgs.uniqueTrackingId], () => buttonResults[startActionArgs.uniqueTrackingId],
(newResult, oldResult) => { (newResult, oldResult) => {
onLogEntryChanged(newResult) onLogEntryChanged(newResult)
@ -397,12 +413,18 @@ async function startAction (actionArgs) {
await pollExecutionUntilDone(trackingId) await pollExecutionUntilDone(trackingId)
} }
} catch (err) { } catch (err) {
stopWatchingButtonResult()
console.error('Failed to start action:', err) console.error('Failed to start action:', err)
} }
} }
function onLogEntryChanged (logEntry) { function onLogEntryChanged (logEntry) {
if (!logEntry) {
return
}
if (logEntry.executionFinished) { if (logEntry.executionFinished) {
stopWatchingButtonResult()
onExecutionFinished(logEntry) onExecutionFinished(logEntry)
} else if (logEntry.queued && !logEntry.executionStarted) { } else if (logEntry.queued && !logEntry.executionStarted) {
onExecutionQueued(logEntry) onExecutionQueued(logEntry)

View File

@ -1,6 +1,6 @@
<template> <template>
<section <section
v-if="!dashboard && !initError" v-if="!dashboard && !initError && !loadError"
style="text-align: center; padding: 2em;" style="text-align: center; padding: 2em;"
> >
<HugeiconsIcon <HugeiconsIcon
@ -27,6 +27,19 @@
Please check your configuration and try again. Please check your configuration and try again.
</p> </p>
</section> </section>
<section
v-else-if="loadError"
style="text-align: center; padding: 2em;"
class="bad"
>
<h2 style="color: var(--error);">
Failed to Load Dashboard
</h2>
<p>{{ loadError }}</p>
<p style="color: var(--fg2);">
Please check your configuration and try again.
</p>
</section>
<template v-else-if="dashboard"> <template v-else-if="dashboard">
<section v-if="dashboard.contents.length == 0"> <section v-if="dashboard.contents.length == 0">
<div <div
@ -135,6 +148,7 @@ const router = useRouter()
const dashboard = ref(null) const dashboard = ref(null)
const loadingTime = ref(0) const loadingTime = ref(0)
const initError = ref(null) const initError = ref(null)
const loadError = ref(null)
let loadingTimer = null let loadingTimer = null
let checkInitInterval = null let checkInitInterval = null
let dashboardRequestId = 0 let dashboardRequestId = 0
@ -194,8 +208,9 @@ async function getDashboard () {
const pageTitle = window.initResponse?.pageTitle || 'OliveTin' const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = ret.dashboard.title + ' - ' + pageTitle document.title = ret.dashboard.title + ' - ' + pageTitle
// Clear any previous init error since we successfully loaded // Clear any previous errors since we successfully loaded
initError.value = null initError.value = null
loadError.value = null
// Stop the loading timer once dashboard is loaded // Stop the loading timer once dashboard is loaded
if (loadingTimer) { if (loadingTimer) {
@ -210,9 +225,9 @@ async function getDashboard () {
return return
} }
// On error, provide a safe fallback state
console.error('Failed to load dashboard', e) console.error('Failed to load dashboard', e)
dashboard.value = { title: title || 'Default', contents: [] } dashboard.value = null
loadError.value = e.message || 'Failed to load dashboard'
const pageTitle = window.initResponse?.pageTitle || 'OliveTin' const pageTitle = window.initResponse?.pageTitle || 'OliveTin'
document.title = 'Error - ' + pageTitle document.title = 'Error - ' + pageTitle
@ -221,14 +236,13 @@ async function getDashboard () {
clearInterval(loadingTimer) clearInterval(loadingTimer)
loadingTimer = null loadingTimer = null
} }
// Set attribute even on error so tests can proceed
document.body.setAttribute('loaded-dashboard', title || 'error')
} }
} }
function waitForInitAndLoadDashboard () { function waitForInitAndLoadDashboard () {
document.body.removeAttribute('loaded-dashboard') document.body.removeAttribute('loaded-dashboard')
loadError.value = null
dashboard.value = null
if (loadingTimer) { if (loadingTimer) {
clearInterval(loadingTimer) clearInterval(loadingTimer)

View File

@ -24,6 +24,7 @@ export default {
required: true required: true
} }
}, },
emits: ['show'],
data () { data () {
return { return {
trackingId: '', trackingId: '',
@ -51,11 +52,11 @@ export default {
this.isWaiting = true this.isWaiting = true
}, },
show () { async show () {
this.$emit('show') this.$emit('show')
if (window.executionDialog) { if (window.executionDialog) {
window.executionDialog.reset() await window.executionDialog.reset()
window.executionDialog.show() window.executionDialog.show()
window.executionDialog.fetchExecutionResult(this.trackingId) window.executionDialog.fetchExecutionResult(this.trackingId)
} }
@ -89,7 +90,10 @@ export default {
// For execution button, we don't need to update classes as much // For execution button, we don't need to update classes as much
// since it's a simpler component // since it's a simpler component
if (resultCssClass) { if (resultCssClass) {
this.$el.classList.add(resultCssClass) const button = this.$el.querySelector('button')
if (button) {
button.classList.add(resultCssClass)
}
} }
} }
} }

View File

@ -319,11 +319,13 @@ function getArgumentValue (arg) {
function handleJustificationInput (event) { function handleJustificationInput (event) {
justificationValue.value = event.target.value justificationValue.value = event.target.value
justificationEditedManually.value = true justificationEditedManually.value = true
event.target.setCustomValidity('')
} }
function handleInput (arg, event) { function handleInput (arg, event) {
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value
argValues.value[arg.name] = value argValues.value[arg.name] = value
event.target.setCustomValidity('')
updateUrlWithArg(arg.name, value) updateUrlWithArg(arg.name, value)
updateJustificationFromTemplate() updateJustificationFromTemplate()
} }

View File

@ -117,9 +117,18 @@ async function fetchDiagnostics () {
} }
async function generateServerDiagnostics () { async function generateServerDiagnostics () {
const response = await window.client.serverDiagnostics() loading.value = true
console.log('response', response)
serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n` try {
const response = await window.client.serverDiagnostics()
console.log('response', response)
serverDiagnostics.value = `\`\`\`\n${response.alert}\n\`\`\`\n`
} catch (err) {
console.error('Failed to generate server diagnostics:', err)
serverDiagnostics.value = ''
} finally {
loading.value = false
}
} }
async function copyServerDiagnostics () { async function copyServerDiagnostics () {

View File

@ -24,10 +24,15 @@
@click="loginWithOAuth(provider)" @click="loginWithOAuth(provider)"
> >
<span <span
v-if="provider.icon" v-if="providerIcon(provider)"
class="provider-icon" class="provider-icon"
v-html="provider.icon" >
/> <iconify-icon
v-if="providerIcon(provider).kind === 'iconify'"
:icon="providerIcon(provider).id"
/>
<span v-else>{{ providerIcon(provider).text }}</span>
</span>
<span class="provider-name">Login with {{ provider.title }}</span> <span class="provider-name">Login with {{ provider.title }}</span>
</button> </button>
</div> </div>
@ -96,6 +101,38 @@ const hasOAuth = ref(false)
const hasLocalLogin = ref(false) const hasLocalLogin = ref(false)
const oauthProviders = ref([]) const oauthProviders = ref([])
const trustedProviderIconifyIds = {
github: 'simple-icons:github',
google: 'simple-icons:google'
}
function providerIcon (provider) {
const raw = (provider?.icon || '').trim()
if (!raw) {
return null
}
const iconifyTagMatch = raw.match(/<iconify-icon\b[^>]*\bicon=["']([^"']+)["'][^>]*>/i)
if (iconifyTagMatch) {
return { kind: 'iconify', id: iconifyTagMatch[1] }
}
if (/^[a-z0-9-]+:[a-z0-9-]+$/i.test(raw)) {
return { kind: 'iconify', id: raw }
}
const trustedId = trustedProviderIconifyIds[provider.key]
if (trustedId && (raw.includes('<') || raw === provider.key)) {
return { kind: 'iconify', id: trustedId }
}
if (!raw.includes('<')) {
return { kind: 'text', text: raw }
}
return trustedId ? { kind: 'iconify', id: trustedId } : null
}
function loadLoginOptions () { function loadLoginOptions () {
// Use the init response data that was loaded in App.vue // Use the init response data that was loaded in App.vue
if (window.initResponse) { if (window.initResponse) {

View File

@ -15,11 +15,9 @@ linters:
- staticcheck - staticcheck
- unconvert - unconvert
- unused - unused
settings:
linters-settings: gocyclo:
gocyclo: min-complexity: 5
min-complexity: 5 exclusions:
paths:
issues: - gen
exclude-dirs:
- gen

View File

@ -53,11 +53,11 @@ find-flakey-tests-inf:
go run ./scripts/find-flakey-tests-inf go run ./scripts/find-flakey-tests-inf
go-tools: go-tools:
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
.PHONY: unittests unittests-fast find-flakey-tests find-flakey-tests-inf .PHONY: unittests unittests-fast find-flakey-tests find-flakey-tests-inf
go-tools-all: go-tools-all:
go install "github.com/bufbuild/buf/cmd/buf" go install "github.com/bufbuild/buf/cmd/buf"
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest" go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
go install "google.golang.org/protobuf/cmd/protoc-gen-go" go install "google.golang.org/protobuf/cmd/protoc-gen-go"

View File

@ -349,13 +349,13 @@ func getUserInfo(cfg *config.Config, client *http.Client, provider *config.OAuth
return ret return ret
} }
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
log.Errorf("Failed to get user data: %v", res.StatusCode) log.Errorf("Failed to get user data: %v", res.StatusCode)
return ret return ret
} }
defer func() { _ = res.Body.Close() }()
contents, err := io.ReadAll(res.Body) contents, err := io.ReadAll(res.Body)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
"github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestUserLoadingFromConfig(t *testing.T) { func TestUserLoadingFromConfig(t *testing.T) {
@ -30,13 +31,13 @@ actions:
// Create temporary file // Create temporary file
tmpFile, err := os.CreateTemp("", "test_config_*.yaml") tmpFile, err := os.CreateTemp("", "test_config_*.yaml")
assert.NoError(t, err, "Should create temporary file") require.NoError(t, err, "Should create temporary file")
defer func() { _ = os.Remove(tmpFile.Name()) }() defer func() { _ = os.Remove(tmpFile.Name()) }()
// Write test config to file // Write test config to file
_, err = tmpFile.WriteString(testConfig) _, err = tmpFile.WriteString(testConfig)
assert.NoError(t, err, "Should write test config to file") require.NoError(t, err, "Should write test config to file")
assert.NoError(t, tmpFile.Close()) require.NoError(t, tmpFile.Close())
// Load config using koanf // Load config using koanf
k := koanf.New(".") k := koanf.New(".")
@ -81,12 +82,12 @@ actions:
` `
tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml") tmpFile, err := os.CreateTemp("", "test_config_empty_*.yaml")
assert.NoError(t, err, "Should create temporary file") require.NoError(t, err, "Should create temporary file")
defer func() { _ = os.Remove(tmpFile.Name()) }() defer func() { _ = os.Remove(tmpFile.Name()) }()
_, err = tmpFile.WriteString(testConfig) _, err = tmpFile.WriteString(testConfig)
assert.NoError(t, err, "Should write test config to file") require.NoError(t, err, "Should write test config to file")
assert.NoError(t, tmpFile.Close()) require.NoError(t, tmpFile.Close())
k := koanf.New(".") k := koanf.New(".")
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser()) err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
@ -116,12 +117,12 @@ actions:
` `
tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml") tmpFile, err := os.CreateTemp("", "test_config_disabled_*.yaml")
assert.NoError(t, err, "Should create temporary file") require.NoError(t, err, "Should create temporary file")
defer func() { _ = os.Remove(tmpFile.Name()) }() defer func() { _ = os.Remove(tmpFile.Name()) }()
_, err = tmpFile.WriteString(testConfig) _, err = tmpFile.WriteString(testConfig)
assert.NoError(t, err, "Should write test config to file") require.NoError(t, err, "Should write test config to file")
assert.NoError(t, tmpFile.Close()) require.NoError(t, tmpFile.Close())
k := koanf.New(".") k := koanf.New(".")
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser()) err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())
@ -147,12 +148,12 @@ actions:
` `
tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml") tmpFile, err := os.CreateTemp("", "test_config_no_auth_*.yaml")
assert.NoError(t, err, "Should create temporary file") require.NoError(t, err, "Should create temporary file")
defer func() { _ = os.Remove(tmpFile.Name()) }() defer func() { _ = os.Remove(tmpFile.Name()) }()
_, err = tmpFile.WriteString(testConfig) _, err = tmpFile.WriteString(testConfig)
assert.NoError(t, err, "Should write test config to file") require.NoError(t, err, "Should write test config to file")
assert.NoError(t, tmpFile.Close()) require.NoError(t, tmpFile.Close())
k := koanf.New(".") k := koanf.New(".")
err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser()) err = k.Load(file.Provider(tmpFile.Name()), yaml.Parser())

View File

@ -74,7 +74,11 @@ func watchPath(ctx *watchContext) {
return return
} }
defer func() { _ = watcher.Close() }() defer func() {
if err := watcher.Close(); err != nil {
log.Errorf("Failed to close file watcher: %v", err)
}
}()
done := make(chan bool) done := make(chan bool)

View File

@ -23,7 +23,12 @@ func WriteFile(filename string, out []byte) {
return return
} }
_ = handle.Close() if err := handle.Close(); err != nil {
log.WithFields(log.Fields{
"error": err,
"filename": filename,
}).Errorf("Failed to close %v", filename)
}
} }
err := os.WriteFile(filename, out, 0600) err := os.WriteFile(filename, out, 0600)

View File

@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
log "github.com/sirupsen/logrus"
) )
type RuntimeInfo struct { type RuntimeInfo struct {
@ -85,6 +87,14 @@ func getOsReleasePrettyName() string {
return "" return ""
} }
defer func() {
if closeErr := handle.Close(); closeErr != nil {
log.WithFields(log.Fields{
"error": closeErr,
}).Warn("Failed to close /etc/os-release")
}
}()
scanner := bufio.NewScanner(handle) scanner := bufio.NewScanner(handle)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
@ -96,7 +106,5 @@ func getOsReleasePrettyName() string {
} }
} }
_ = handle.Close()
return "notfound" return "notfound"
} }

View File

@ -202,10 +202,13 @@ func appendFile(path, content string) error {
if err != nil { if err != nil {
return err return err
} }
defer func() { _ = file.Close() }()
_, err = file.WriteString(content) _, writeErr := file.WriteString(content)
return err closeErr := file.Close()
if writeErr != nil {
return writeErr
}
return closeErr
} }
func newTestRunState() *testRunState { func newTestRunState() *testRunState {