Skip to content

Commit cff9403

Browse files
haexhubhaex-space
andauthored
chore: fix typescript strict-mode errors (#10)
`nuxt typecheck` reported 89 errors against the strict server tsconfig (noUncheckedIndexedAccess + missing @types/node). Production builds and dev server worked despite this — the build step uses a looser config — but the editor was a sea of red. - Add @types/node so Buffer / process / node:* imports resolve - Add types/assets.d.ts shim for Leaflet *.png URL imports - Guard regex-match index access (m?.[1] / m?.[1] ?? '') across all parsers (boe + zvg-portal) — every parser had the same `if (m) m[1]` pattern that strict mode rejects - Narrow useFetch generics to `T | null` in pages/index.vue so the `default: () => null` initialiser type-checks; pin `applyFilters` with explicit `<GeoAuction>` so .lat/.lng access doesn't widen back to Auction - Type `courts` computed as `string[]` (Set was inferred as unknown) - Guard fotos[0]/items[idx]/data[0] against possibly-undefined index reads in geocode + zvg-portal/index + zvg-portal/detail Co-authored-by: haex <mail@haex.space>
1 parent 7b3a862 commit cff9403

12 files changed

Lines changed: 104 additions & 79 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"devDependencies": {
3535
"@nuxt/icon": "^2.2.1",
3636
"@types/leaflet": "^1.9.21",
37+
"@types/node": "^25.6.0",
3738
"@vitejs/plugin-vue": "^5.2.4",
3839
"@vueuse/core": "^14.3.0",
3940
"typescript": "~5.6"

pages/index.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const queryParams = computed(() => ({
4545
}))
4646
4747
// Lazy fetch so SSR doesn't block on a cold multi-region crawl.
48-
const { data, pending, error, refresh } = useLazyFetch<CrawlResult>('/api/auctions', {
48+
const { data, pending, error, refresh } = useLazyFetch<CrawlResult | null>('/api/auctions', {
4949
query: queryParams,
5050
default: () => null,
5151
})
@@ -66,7 +66,7 @@ const {
6666
error: geoError,
6767
execute: loadGeo,
6868
refresh: refreshGeo,
69-
} = useFetch<GeoCrawlResult>('/api/auctions-geo', {
69+
} = useFetch<GeoCrawlResult | null>('/api/auctions-geo', {
7070
query: {
7171
country: selectedCountry,
7272
region: selectedRegion,
@@ -169,7 +169,7 @@ const headerLabel = computed(() => {
169169
: selectedCountryLabel.value
170170
})
171171
172-
const courts = computed(() => {
172+
const courts = computed<string[]>(() => {
173173
if (!data.value) return []
174174
return [...new Set(data.value.auctions.map((a) => a.amtsgericht).filter(Boolean))].sort()
175175
})
@@ -226,7 +226,7 @@ const filtered = computed<Auction[]>(() => {
226226
227227
const filteredGeo = computed<GeoAuction[]>(() => {
228228
if (!geoData.value) return []
229-
return applyFilters(geoData.value.auctions).filter((a) => a.lat != null && a.lng != null)
229+
return applyFilters<GeoAuction>(geoData.value.auctions).filter((a) => a.lat != null && a.lng != null)
230230
})
231231
232232
const totals = computed(() => {

pnpm-lock.yaml

Lines changed: 66 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/crawlers/boe/detail.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ function extractTablePairs(html: string): Map<string, string> {
3838
const pairs = new Map<string, string>()
3939
const re = /<th[^>]*>([\s\S]*?)<\/th>\s*<td[^>]*>([\s\S]*?)<\/td>/g
4040
for (const m of html.matchAll(re)) {
41-
const k = clean(m[1].replace(/<[^>]+>/g, ''))
42-
const v = clean(m[2].replace(/<[^>]+>/g, ' '))
41+
const k = clean((m[1] ?? '').replace(/<[^>]+>/g, ''))
42+
const v = clean((m[2] ?? '').replace(/<[^>]+>/g, ' '))
4343
if (k) pairs.set(k.toLowerCase(), v)
4444
}
4545
return pairs

server/crawlers/boe/list.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function parseListingHtml(
5151

5252
// "Resultados 1 a 77 de 77" — pull the total if present.
5353
const totalMatch = html.match(/Resultados[\s\S]{0,80}?de\s+(\d+)/i)
54-
const totalReported = totalMatch ? parseInt(totalMatch[1], 10) : null
54+
const totalReported = totalMatch?.[1] ? parseInt(totalMatch[1], 10) : null
5555

5656
$('li.resultado-busqueda').each((_i, el) => {
5757
const $el = $(el)
@@ -73,12 +73,12 @@ export function parseListingHtml(
7373

7474
// "Conclusión prevista: 25/05/2026 a las 18:00:00"
7575
const terminMatch = estadoLine.match(/Conclusión[^:]*:\s*([^\]]+)/i)
76-
const terminText = terminMatch ? terminMatch[1].trim() : null
76+
const terminText = terminMatch?.[1]?.trim() ?? null
7777
const terminIso = terminText ? parseSpanishDateTime(terminText) : null
7878

7979
// Estado prefix before the bracket: "Estado: Celebrándose" → "Celebrándose"
8080
const estadoMatch = estadoLine.match(/Estado:\s*([^\-\[]+)/i)
81-
const estadoLabel = estadoMatch ? estadoMatch[1].trim() : null
81+
const estadoLabel = estadoMatch?.[1]?.trim() ?? null
8282

8383
// Description and address are merged in the listing. Take the whole
8484
// string as `objekt` for now and let detail enrichment split it later.
@@ -88,7 +88,7 @@ export function parseListingHtml(
8888
let adresse: string | null = null
8989
if (descLine) {
9090
const cp = descLine.match(/(\d{5}\s+[^,]+?)(?:\s*\(|$)/)
91-
if (cp) adresse = `${cp[1].trim()}, España`
91+
if (cp?.[1]) adresse = `${cp[1].trim()}, España`
9292
}
9393

9494
const detailUrlUpstream = `${BOE_BASE}/detalleSubasta.php?idSub=${encodeURIComponent(idSub)}`

server/crawlers/boe/text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function clean(s: string): string {
2020
/** Parses Spanish-formatted Euro amounts like `88.029,53 €` → 88029.53. */
2121
export function parseEuroEs(text: string): number | null {
2222
const m = decodeEntities(text).replace(/\s|&nbsp;/g, '').match(/([\d.]+,\d{2})/)
23-
if (!m) return null
23+
if (!m?.[1]) return null
2424
const n = m[1].replace(/\./g, '').replace(',', '.')
2525
const num = parseFloat(n)
2626
return Number.isFinite(num) ? num : null
@@ -32,7 +32,7 @@ export function parseEuroEs(text: string): number | null {
3232
export function parseSpanishDateTime(text: string): string | null {
3333
const m = text.match(/(\d{1,2})[\/.](\d{1,2})[\/.](\d{4})(?:\s*(?:a las|,)?\s*(\d{1,2}):(\d{2})(?::(\d{2}))?)?/)
3434
if (!m) return null
35-
const [, d, mo, y, hh, mm, ss] = m
35+
const [, d = '', mo = '', y = '', hh, mm, ss] = m
3636
const day = d.padStart(2, '0')
3737
const mon = mo.padStart(2, '0')
3838
const time = hh ? `T${hh.padStart(2, '0')}:${mm}:${ss ?? '00'}` : 'T00:00:00'

server/crawlers/zvg-portal/detail.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export async function fetchDetailPage(zvgId: string, landAbk: string): Promise<D
5050
if (!anchor.length) return
5151
const href = anchor.attr('href') || ''
5252
const fileIdMatch = href.match(/file_id=(\d+)/)
53-
if (!fileIdMatch) return
54-
const fileId = fileIdMatch[1]
53+
const fileId = fileIdMatch?.[1]
54+
if (!fileId) return
5555
if (seenFileIds.has(fileId)) return
5656
seenFileIds.add(fileId)
5757

@@ -76,7 +76,7 @@ export async function fetchDetailPage(zvgId: string, landAbk: string): Promise<D
7676
// Beschreibung is in the same column as Objekt/Lage. Extract the full block.
7777
let beschreibung: string | null = null
7878
const beschrMatch = html.match(/Beschreibung[^<]*<\/td>\s*<td[^>]*>([\s\S]*?)<\/td>/i)
79-
if (beschrMatch) {
79+
if (beschrMatch?.[1]) {
8080
const text = beschrMatch[1].replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, ' ')
8181
const cleaned = decodeEntities(text).replace(/[ \t]+/g, ' ').replace(/\n[ \t]+/g, '\n').trim()
8282
if (cleaned.length > 0) beschreibung = cleaned
@@ -96,7 +96,7 @@ export async function enrichInBatches<T extends { zvgId: string }>(
9696
while (cursor < items.length) {
9797
const idx = cursor++
9898
const item = items[idx]
99-
if (!/^\d+$/.test(item.zvgId)) continue
99+
if (!item || !/^\d+$/.test(item.zvgId)) continue
100100
try {
101101
const info = await fetchDetailPage(item.zvgId, landAbk)
102102
enricher(item, info)

server/crawlers/zvg-portal/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ async function crawl(opts: CrawlOptions): Promise<CrawlResult> {
4040
auction.beschreibung = info.beschreibung
4141
const fotos = info.attachments.filter((a) => a.kind === 'foto')
4242
auction.fotoCount = fotos.length
43-
if (fotos.length > 0) {
44-
auction.thumbnailUrl = `/api/zvg-thumb?file_id=${fotos[0].fileId}&zvg_id=${auction.zvgId}&land_abk=${landAbk}`
43+
const firstFoto = fotos[0]
44+
if (firstFoto) {
45+
auction.thumbnailUrl = `/api/zvg-thumb?file_id=${firstFoto.fileId}&zvg_id=${auction.zvgId}&land_abk=${landAbk}`
4546
}
4647
})
4748
}

server/crawlers/zvg-portal/list.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface ParseResult {
1010

1111
export function parseAuctionsHtml(html: string, landAbk: string, platformId: string): ParseResult {
1212
const totalMatch = html.match(/Insgesamt\s+(\d+)/)
13-
const totalReported = totalMatch ? parseInt(totalMatch[1], 10) : null
13+
const totalReported = totalMatch?.[1] ? parseInt(totalMatch[1], 10) : null
1414

1515
// Each entry block is delimited by the comment markers + a final <hr>.
1616
// Strategy: split the raw HTML at "<!--Aktenzeichen--->" and parse each chunk.
@@ -27,7 +27,7 @@ export function parseAuctionsHtml(html: string, landAbk: string, platformId: str
2727
let terminIso: string | null = null
2828
let aufgehoben = false
2929
const terminMatch = chunk.match(/<TR>\s*<TD[^>]*>\s*Termin\s*<\/[Tt][Dd]>([\s\S]*?)<\/[Tt][Rr]>/i)
30-
if (terminMatch) {
30+
if (terminMatch?.[1]) {
3131
const inner = terminMatch[1].replace(/<[^>]+>/g, ' ')
3232
const decoded = clean(inner)
3333
aufgehoben = /aufgehoben/i.test(decoded)
@@ -45,15 +45,15 @@ export function parseAuctionsHtml(html: string, landAbk: string, platformId: str
4545
aktenzeichen = aktenzeichenA.text().replace(/\(Detailansicht\)/i, '').trim()
4646
} else {
4747
const azMatch = chunk.match(/<nobr>\s*(\d+\s+K\s+\d+\/\d+)\s*(?:&nbsp;)?\s*(?:\(Detailansicht\))?\s*<\/nobr>/i)
48-
if (azMatch) aktenzeichen = azMatch[1].trim()
48+
if (azMatch?.[1]) aktenzeichen = azMatch[1].trim()
4949
}
5050
aktenzeichen = clean(aktenzeichen)
5151
if (!aktenzeichen && !zvgIdMatch) continue
5252

5353
const zvgId = zvgIdMatch || `az:${aktenzeichen}`
5454

5555
const updateMatch = chunk.match(/letzte Aktualisierung\s+([\d-]+\s+[\d:]+)/)
56-
const letzteAktualisierungIso = updateMatch ? parseGermanTimestamp(updateMatch[1]) : null
56+
const letzteAktualisierungIso = updateMatch?.[1] ? parseGermanTimestamp(updateMatch[1]) : null
5757

5858
let amtsgericht = ''
5959
// Region name comes from landAbk (canonical), not from the HTML.
@@ -68,7 +68,7 @@ export function parseAuctionsHtml(html: string, landAbk: string, platformId: str
6868
// themselves contain " in " (e.g. "Landau in der Pfalz"), so split on the
6969
// LAST occurrence.
7070
const amtMatch = chunk.match(/<!--Amtsgericht--->[\s\S]*?<b>\s*([^<]+?)\s*<\/b>/i)
71-
if (amtMatch) {
71+
if (amtMatch?.[1]) {
7272
const inner = clean(amtMatch[1])
7373
const sep = inner.lastIndexOf(' in ')
7474
amtsgericht = sep >= 0 ? inner.slice(0, sep) : inner
@@ -77,15 +77,15 @@ export function parseAuctionsHtml(html: string, landAbk: string, platformId: str
7777
let objekt: string | null = null
7878
let adresse: string | null = null
7979
const lageMatch = chunk.match(/<b>([^<]+?)<!--Lage--->\s*:?\s*<\/b>\s*([^<\n]+)/)
80-
if (lageMatch) {
80+
if (lageMatch?.[1] && lageMatch[2]) {
8181
objekt = clean(lageMatch[1])
8282
adresse = clean(lageMatch[2])
8383
}
8484

8585
let verkehrswertEur: number | null = null
8686
let verkehrswertText: string | null = null
8787
const vwMatch = chunk.match(/Verkehrswert in[\s\S]*?<b>([\s\S]*?)<\/b>/)
88-
if (vwMatch) {
88+
if (vwMatch?.[1]) {
8989
const inner = vwMatch[1].replace(/<[^>]+>/g, ' ')
9090
verkehrswertText = clean(inner) || null
9191
verkehrswertEur = parseEuro(inner)
@@ -94,11 +94,11 @@ export function parseAuctionsHtml(html: string, landAbk: string, platformId: str
9494
let pdfUrl: string | null = null
9595
let pdfUrlUpstream: string | null = null
9696
const pdfMatch = chunk.match(/href="([^"]*showAnhang[^"]*)"/i)
97-
if (pdfMatch) {
97+
if (pdfMatch?.[1]) {
9898
const raw = pdfMatch[1].trim().replace(/\s+$/, '')
9999
pdfUrlUpstream = `${ZVG_BASE}/${raw.replace(/^\/+/, '')}`
100100
const fileIdMatch = raw.match(/file_id=(\d+)/)
101-
if (fileIdMatch && zvgIdMatch) {
101+
if (fileIdMatch?.[1] && zvgIdMatch) {
102102
pdfUrl = `/api/zvg-proxy?button=showAnhang&land_abk=${landAbk}&file_id=${fileIdMatch[1]}&zvg_id=${zvgIdMatch}`
103103
}
104104
}

server/crawlers/zvg-portal/text.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export function parseGermanDateTime(text: string): string | null {
88
// "Donnerstag, 07. Mai 2026, 10:00 Uhr"
99
const m = text.match(/(\d{1,2})\.\s*(\p{L}+)\s+(\d{4}),?\s*(\d{1,2}):(\d{2})/u)
1010
if (!m) return null
11-
const [, d, monthName, y, hh, mm] = m
11+
const [, d = '', monthName, y, hh = '', mm] = m
12+
if (!monthName) return null
1213
const month = MONTH_DE[monthName]
1314
if (!month) return null
1415
return `${y}-${month}-${d.padStart(2, '0')}T${hh.padStart(2, '0')}:${mm}:00`
@@ -25,15 +26,15 @@ export function parseGermanTimestamp(text: string): string | null {
2526
export function parseEuro(text: string): number | null {
2627
// "214.000,00 Euro" or "800.000,00" or "16.100,00&nbsp;"
2728
const m = text.replace(/\s|&nbsp;/g, '').match(/([\d.]+,\d{2})/)
28-
if (!m) return null
29+
if (!m?.[1]) return null
2930
const n = m[1].replace(/\./g, '').replace(',', '.')
3031
const num = parseFloat(n)
3132
return Number.isFinite(num) ? num : null
3233
}
3334

3435
export function parseFileSize(text: string): number | null {
3536
const m = text.match(/([\d.,]+)\s*(kB|MB|KB|B)/i)
36-
if (!m) return null
37+
if (!m?.[1] || !m[2]) return null
3738
// The portal mixes formats: "1.234,56" (German thousands+decimal) and "188.25" (English decimal).
3839
// Heuristic: a comma marks the decimal; otherwise a single dot followed by 1-2 digits is the decimal.
3940
let s = m[1]

0 commit comments

Comments
 (0)