chore: migrate frontend linting from standard to neostandard

Replace the ESLint 8-based standard package with neostandard and a
flat config so frontend installs no longer pull deprecated ESLint 8
transitives. Keep vue recommended rules and auto-fix indent drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jamesread
2026-09-10 17:28:09 +01:00
co-authored by Cursor
parent 88f9acebd5
commit fa2702094b
10 changed files with 1428 additions and 1082 deletions
-25
View File
@@ -1,25 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:vue/recommended",
"standard"
],
"parser": "vue-eslint-parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"parser": {
"js": "espree"
}
},
"rules": {
"vue/multi-word-component-names": "off",
"vue/require-default-prop": "off",
"vue/no-v-html": "warn",
"no-tabs": "off",
"no-mixed-spaces-and-tabs": "off"
}
}
+31
View File
@@ -0,0 +1,31 @@
import neostandard from 'neostandard'
import pluginVue from 'eslint-plugin-vue'
export default [
{
ignores: [
'dist/**',
'node_modules/**',
'resources/scripts/gen/**'
]
},
...neostandard({
env: ['browser'],
noJsx: true
}),
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.{js,mjs,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'@stylistic/no-tabs': 'off',
'@stylistic/no-mixed-spaces-and-tabs': 'off',
'vue/multi-word-component-names': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-html': 'warn'
}
}
]
+1310 -971
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -5,7 +5,9 @@
"repository": "https://github.com/OliveTin/OliveTin",
"source": "index.html",
"devDependencies": {
"eslint": "^9.39.5",
"eslint-plugin-vue": "^10.11.0",
"neostandard": "^0.13.0",
"process": "^0.11.10",
"stylelint": "^17.15.0",
"stylelint-config-standard": "^40.0.0"
@@ -35,7 +37,6 @@
"@xterm/xterm": "^6.0.0",
"iconify-icon": "^3.0.2",
"picocrank": "^1.29.0",
"standard": "^17.1.2",
"unplugin-vue-components": "^32.1.0",
"vite": "^8.2.2",
"vue": "^3.5.42",
+23 -23
View File
@@ -273,9 +273,9 @@ function constructFromJson (json) {
if (bindingId.value) {
rateLimits[bindingId.value] = rateLimitExpires.value
setBindingExecutionState(
bindingId.value,
!!json.hasRunningInstance,
!!json.hasQueuedInstance
bindingId.value,
!!json.hasRunningInstance,
!!json.hasQueuedInstance
)
}
updateRateLimitStatus()
@@ -310,8 +310,8 @@ function updateRateLimitStatus () {
isRateLimited.value = false
rateLimitMessage.value = ''
if (rateLimitInterval.value) {
clearInterval(rateLimitInterval.value)
rateLimitInterval.value = null
clearInterval(rateLimitInterval.value)
rateLimitInterval.value = null
}
return
}
@@ -325,8 +325,8 @@ function updateRateLimitStatus () {
rateLimitMessage.value = ''
rateLimitExpires.value = 0
if (rateLimitInterval.value) {
clearInterval(rateLimitInterval.value)
rateLimitInterval.value = null
clearInterval(rateLimitInterval.value)
rateLimitInterval.value = null
}
} else {
// Still rate limited
@@ -336,9 +336,9 @@ function updateRateLimitStatus () {
// Set up interval to update every second
if (!rateLimitInterval.value) {
rateLimitInterval.value = setInterval(() => {
rateLimitInterval.value = setInterval(() => {
updateRateLimitStatus()
}, 1000)
}, 1000)
}
}
}
@@ -360,12 +360,12 @@ async function handleClick () {
const bindingId = props.actionData.bindingId
const prefilled = props.prefilledArguments || {}
if (Object.keys(prefilled).length > 0) {
router.push({
router.push({
path: `/actionBinding/${bindingId}/argumentForm`,
state: { prefilledArguments: prefilled }
})
})
} else {
router.push(`/actionBinding/${bindingId}/argumentForm`)
router.push(`/actionBinding/${bindingId}/argumentForm`)
}
} else {
await startAction()
@@ -439,7 +439,7 @@ async function startAction (actionArgs) {
stopButtonResultWatch = watch(
() => buttonResults[startActionArgs.uniqueTrackingId],
(newResult, oldResult) => {
onLogEntryChanged(newResult)
onLogEntryChanged(newResult)
}
)
@@ -450,11 +450,11 @@ async function startAction (actionArgs) {
const trackingId = response.executionTrackingId || startActionArgs.uniqueTrackingId
if (popupOnStart.value && popupOnStart.value.includes('execution-dialog')) {
router.push(`/logs/${trackingId}`)
router.push(`/logs/${trackingId}`)
}
if (!connectionState.connected) {
await pollExecutionUntilDone(trackingId)
await pollExecutionUntilDone(trackingId)
}
} catch (err) {
stopWatchingButtonResult()
@@ -554,14 +554,14 @@ onMounted(() => {
watch(
rateLimits,
() => {
const id = bindingId.value
if (id && rateLimits[id] !== undefined) {
const id = bindingId.value
if (id && rateLimits[id] !== undefined) {
const newExpires = rateLimits[id]
if (newExpires !== rateLimitExpires.value) {
rateLimitExpires.value = newExpires
updateRateLimitStatus()
rateLimitExpires.value = newExpires
updateRateLimitStatus()
}
}
}
},
{ deep: true }
)
@@ -570,9 +570,9 @@ onMounted(() => {
watch(
() => pendingBindingFlash[bindingId.value],
(pending) => {
if (pending) {
if (pending) {
consumeAndFlashPendingResult()
}
}
},
{ immediate: true }
)
@@ -593,7 +593,7 @@ watch(
(newData) => {
updateFromJson(newData)
if (newData?.icon !== undefined) {
glyph.value = newData.icon ?? ''
glyph.value = newData.icon ?? ''
}
},
{ deep: true }
@@ -1,38 +1,38 @@
const fallbackNamedHtmlEntities = {
amp: '&',
apos: "'",
darr: '\u2193',
gt: '>',
laquo: '\u00ab',
larr: '\u2190',
nbsp: '\u00a0',
quot: '"',
raquo: '\u00bb',
rarr: '\u2192',
uarr: '\u2191',
amp: '&',
apos: "'",
darr: '\u2193',
gt: '>',
laquo: '\u00ab',
larr: '\u2190',
nbsp: '\u00a0',
quot: '"',
raquo: '\u00bb',
rarr: '\u2192',
uarr: '\u2191',
}
export function decodeHtmlEntities(text) {
if (typeof document !== 'undefined') {
const textarea = document.createElement('textarea')
textarea.innerHTML = text
export function decodeHtmlEntities (text) {
if (typeof document !== 'undefined') {
const textarea = document.createElement('textarea')
textarea.innerHTML = text
return textarea.value
}
return textarea.value
}
return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
const codePoint = Number.parseInt(hex, 16)
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
}).replace(/&#(\d+);?/g, (_, decimal) => {
const codePoint = Number.parseInt(decimal, 10)
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
}).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
return fallbackNamedHtmlEntities[name] ?? entity
})
return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => {
const codePoint = Number.parseInt(hex, 16)
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
}).replace(/&#(\d+);?/g, (_, decimal) => {
const codePoint = Number.parseInt(decimal, 10)
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : ''
}).replace(/&([a-zA-Z][a-zA-Z0-9]+);?/g, (entity, name) => {
return fallbackNamedHtmlEntities[name] ?? entity
})
}
export function glyphLooksLikeHtml(text) {
const trimmedText = text.trim()
export function glyphLooksLikeHtml (text) {
const trimmedText = text.trim()
return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
return trimmedText.startsWith('<') || /<img\b/i.test(text) || /\/custom-webui\//i.test(text)
}
@@ -3,18 +3,18 @@ import assert from 'node:assert/strict'
import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs'
test('decodeHtmlEntities decodes named entity icons as plain glyph text', () => {
assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
assert.equal(decodeHtmlEntities('&laquo;'), '\u00ab')
assert.equal(decodeHtmlEntities('&rarr;'), '\u2192')
assert.equal(decodeHtmlEntities('&laquo; next &rarr;'), '\u00ab next \u2192')
})
test('decoded named entity icons are not treated as HTML markup', () => {
const decodedGlyph = decodeHtmlEntities('&rarr;')
const decodedGlyph = decodeHtmlEntities('&rarr;')
assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
assert.equal(glyphLooksLikeHtml(decodedGlyph), false)
})
test('decodeHtmlEntities keeps existing numeric entity icon support', () => {
assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
assert.equal(decodeHtmlEntities('&#x1f4a9;'), '\ud83d\udca9')
assert.equal(decodeHtmlEntities('&#128190;'), '\ud83d\udcbe')
})
@@ -13,7 +13,7 @@ const choices = [
{ title: 'Photos', value: 'photos' }
]
test('parseChecklistValue parses JSON-encoded values', () => {
test('parseChecklistValue parses JSON-encoded values', () => {
assert.deepEqual(parseChecklistValue('["documents","photos"]'), ['documents', 'photos'])
assert.deepEqual(parseChecklistValue('["kitchen,bedroom","hallway"]'), ['kitchen,bedroom', 'hallway'])
assert.deepEqual(parseChecklistValue(''), [])
@@ -4,31 +4,31 @@ import assert from 'node:assert/strict'
import { getInitialArgumentValue, readPrefilledArgumentsFromNavigation } from './prefilledArguments.js'
test('readPrefilledArgumentsFromNavigation returns navigation state values', () => {
assert.deepEqual(
readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
{ ansible_host: '10.0.0.1' }
)
assert.deepEqual(
readPrefilledArgumentsFromNavigation({ prefilledArguments: { ansible_host: '10.0.0.1' } }),
{ ansible_host: '10.0.0.1' }
)
})
test('readPrefilledArgumentsFromNavigation returns empty object when state is absent', () => {
assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
assert.deepEqual(readPrefilledArgumentsFromNavigation({}), {})
assert.deepEqual(readPrefilledArgumentsFromNavigation(undefined), {})
})
test('getInitialArgumentValue prefers navigation state over query params', () => {
assert.equal(
getInitialArgumentValue(
'ansible_host',
{ ansible_host: '10.0.0.1' },
'?ansible_host=10.0.0.2'
),
'10.0.0.1'
)
assert.equal(
getInitialArgumentValue(
'ansible_host',
{ ansible_host: '10.0.0.1' },
'?ansible_host=10.0.0.2'
),
'10.0.0.1'
)
})
test('getInitialArgumentValue falls back to query params when state is absent', () => {
assert.equal(
getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
'10.0.0.2'
)
assert.equal(
getInitialArgumentValue('ansible_host', {}, '?ansible_host=10.0.0.2'),
'10.0.0.2'
)
})
@@ -355,10 +355,10 @@ async function fetchExecutionResult (executionTrackingIdParam) {
} catch (err) {
// Check if it's a "not found" error (404 or similar)
if (err.status === 404 || err.code === 'NotFound' || err.message?.includes('not found')) {
notFound.value = true
errorMessage.value = err.message || 'The execution could not be found in the system.'
notFound.value = true
errorMessage.value = err.message || 'The execution could not be found in the system.'
} else {
renderError(err)
renderError(err)
}
throw err
}
@@ -434,7 +434,7 @@ async function renderExecutionResult (res) {
if (terminal) {
await terminal.reset()
await terminal.write(res.logEntry.output, () => {
terminal.fit()
terminal.fit()
})
}
}
@@ -488,11 +488,11 @@ onMounted(() => {
watch(
() => buttonResults[props.executionTrackingId],
(newResult, oldResult) => {
if (newResult) {
if (newResult) {
renderExecutionResult({
logEntry: newResult
logEntry: newResult
})
}
}
}
)
})