sync: Windmill-State übernehmen + neue Reporting-Flows

- Dateien nach Windmill-Naming-Konvention umbenannt (ssh-key_aus_db_testen,
  flow-fehler_per_nextcloud_talk_melden, bitwarden_(fallback))
- testpause-Schritt aus flow.yaml entfernt (Debugging abgeschlossen)
- Neue Flows: f/Reporting/exchange_logins, f/Reporting/run_sql_events
- mail_to_talk: Dateinamen nach Windmill-Konvention synchronisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Serfling
2026-05-07 13:13:35 +02:00
parent 4e19c41cd2
commit d22ef502ed
40 changed files with 1077 additions and 89 deletions
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,30 @@
export async function main(results_array: any[], event_name: string = 'all_create', subject_prefix: string = '') {
const total = Array.isArray(results_array) ? results_array.length : 0
const failed = Array.isArray(results_array) ? results_array.filter((r: any) => r && r.success === false).length : 0
const succeeded = total - failed
const subject = `${subject_prefix ? subject_prefix + ' ' : ''}Ergebnisse für Event ${event_name}: ${succeeded}/${total} erfolgreich`
const lines: string[] = []
lines.push(`Event: ${event_name}`)
lines.push(`Erfolgreich: ${succeeded}/${total}`)
lines.push('')
for (let i = 0; i < total; i++) {
const r: any = results_array[i]
if (!r) continue
lines.push(`CALL ${i + 1}: ${r.call}`)
if (r.success) {
let snippet = ''
try {
snippet = JSON.stringify(r.result)
if (snippet.length > 1000) snippet = snippet.slice(0, 1000) + '...'
} catch { snippet = '[unserialisierbar]' }
lines.push(' Status: OK')
lines.push(` Result: ${snippet}`)
} else {
lines.push(' Status: FEHLER')
lines.push(` Error: ${r.error}`)
}
lines.push('')
}
const body = lines.join('\n')
return { subject, body }
}
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,18 @@
export async function main(event_definition: string) {
if (!event_definition || typeof event_definition !== 'string') return []
// Some MySQL store event body as full CREATE EVENT ... DO ...; we only need inside DO ...
let def = event_definition
const doMatch = def.match(/\bDO\s+(BEGIN[\s\S]*END|[^;]+);?/i)
if (doMatch) {
def = doMatch[1]
}
let body = def
.replace(/DELIMITER\s+[^\n]+/gi, ' ')
.replace(/\bBEGIN\b/gi, ' ')
.replace(/\bEND\b/gi, ' ')
.replace(/\s+/g, ' ')
const regex = /CALL\s+[^;]+;/gi
const matches = body.match(regex) || []
return matches.map((s) => s.trim())
}
@@ -0,0 +1,5 @@
{
"dependencies": {}
}
//bun.lock
<empty>
@@ -0,0 +1,21 @@
export async function main(rows: any) {
// Normalize possible hub script return shapes
let arr: any[] = []
if (Array.isArray(rows)) arr = rows
else if (rows && Array.isArray(rows.rows)) arr = rows.rows
else if (rows && Array.isArray(rows.result)) arr = rows.result
else if (rows && Array.isArray(rows.data)) arr = rows.data
let def = ''
try {
if (arr.length > 0) {
const r = arr[0]
def = r.EVENT_DEFINITION || r.event_definition || ''
if (!def && r.EVENT_BODY) {
// Some MySQL variants expose body differently; just in case
def = r.EVENT_BODY
}
}
} catch {}
return { event_definition: def, row_count: arr.length }
}
+105
View File
@@ -0,0 +1,105 @@
summary: Run SQL Events for Reporting
description: ''
value:
modules:
- id: get_event_def_rows
summary: Liest EVENT_DEFINITION für das MySQL-Event (Zeilen vom Hubskript)
value:
type: script
input_transforms:
mysql_conn:
type: static
value: $res:u/sebastianserfling/fascinating_mysql
query:
type: javascript
expr: "`SELECT EVENT_DEFINITION FROM information_schema.EVENTS WHERE EVENT_NAME
= '${flow_input.event_name ?? 'all_create'}' ORDER BY
(EVENT_SCHEMA = DATABASE()) DESC LIMIT 1`"
path: hub/17540/mysql/execute_query
- id: extract_event_def
summary: Extrahiert EVENT_DEFINITION-String aus den Zeilen
value:
type: rawscript
content: '!inline extrahiert_event_definition-string_aus_den_zeilen.ts'
input_transforms:
rows:
type: javascript
expr: results.get_event_def_rows
lock: '!inline extrahiert_event_definition-string_aus_den_zeilen.lock'
language: bun
- id: parse_calls
summary: Extrahiert alle CALL-Statements aus dem EVENT_DEFINITION-Body
value:
type: rawscript
content: '!inline
extrahiert_alle_call-statements_aus_dem_event_definition-body.ts'
input_transforms:
event_definition:
type: javascript
expr: results.extract_event_def.event_definition
lock: '!inline
extrahiert_alle_call-statements_aus_dem_event_definition-body.lock'
language: bun
- id: loop_calls
summary: Führt jede CALL einzeln aus und sendet bei Fehler Pushover
value:
type: forloopflow
modules:
- id: run_call
summary: Führt eine einzelne CALL aus, sendet bei Fehler Pushover und liefert
Ergebnis
value:
type: rawscript
content: '!inline
führt_eine_einzelne_call_aus,_sendet_bei_fehler_pushover_und_liefert_ergebnis.ts'
input_transforms:
call_sql:
type: javascript
expr: flow_input.iter.value
database_resource_path:
type: static
value: $res:u/sebastianserfling/fascinating_mysql
pushover_token:
type: javascript
expr: flow_input.pushover_token
pushover_user:
type: javascript
expr: flow_input.pushover_user
lock: '!inline
führt_eine_einzelne_call_aus,_sendet_bei_fehler_pushover_und_liefert_ergebnis.lock'
language: bun
iterator:
type: javascript
expr: results.parse_calls || []
parallel: false
skip_failures: true
squash: false
- id: compose_email
summary: Erstellt Betreff und Body mit allen Ergebnissen der CALLs
value:
type: rawscript
content: '!inline erstellt_betreff_und_body_mit_allen_ergebnissen_der_calls.ts'
input_transforms:
event_name:
type: javascript
expr: flow_input.event_name ?? 'all_create'
results_array:
type: javascript
expr: results.loop_calls
subject_prefix:
type: javascript
expr: flow_input.email_subject_prefix ?? ''
lock: '!inline erstellt_betreff_und_body_mit_allen_ergebnissen_der_calls.lock'
language: bun
groups: []
schema:
$schema: https://json-schema.org/draft/2020-12/schema
type: object
order:
- event_name
properties:
event_name:
type: string
description: Name des MySQL Events
default: all_create
required: []
@@ -0,0 +1,19 @@
{
"dependencies": {
"windmill-client": "latest"
}
}
//bun.lock
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"windmill-client": "latest",
},
},
},
"packages": {
"windmill-client": ["windmill-client@1.614.0", "", {}, "sha512-skpKI9mJXhUdwc9gZbounVclw22HTpXlhCAkGnkEAqys3xkRzA7v8/NY2Xt2WQrWV7GIN4ZGkOAgv2rk1r36hg=="],
}
}
@@ -0,0 +1,34 @@
import * as wmill from 'windmill-client'
export async function main(database_resource_path: string, call_sql: string, pushover_token?: string, pushover_user?: string) {
let success = false
let result: any = null
let errorMsg = ''
try {
// Pass the resource path string directly so the hub script can resolve it itself
const rows = await wmill.runScriptByPath('hub/17540/mysql/execute_query', {
mysql_conn: database_resource_path,
query: call_sql
})
success = true
result = rows
} catch (err: any) {
errorMsg = err?.message || String(err)
if (pushover_token && pushover_user) {
try {
const body = new URLSearchParams({
token: String(pushover_token),
user: String(pushover_user),
title: 'MySQL CALL fehlgeschlagen',
message: `Fehler bei CALL: ${call_sql}\nFehlermeldung: ${errorMsg}`
})
await fetch('https://api.pushover.net/1/messages.json', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
})
} catch {}
}
}
return { call: call_sql, success, result, error: success ? null : errorMsg }
}