Initial commit

This commit is contained in:
Jonas Karneboge 2026-09-22 18:35:43 +02:00
commit 3cba772836
1341 changed files with 532924 additions and 0 deletions

215
#alt_skripte/get_data.R Normal file
View file

@ -0,0 +1,215 @@
library(formr)
formr_store_keys(host = "https://diagnostik.karneboge.de/api", client_id = "a007099d1ce340b2c701910b01d821ae", client_secret = "ff3f43bdca93c92fa5a854c97fbefc0d67f1900545fc79745a6e6565c1cbc07b", verbose = F)
formr_api_authenticate(host = "https://diagnostik.karneboge.de/api", verbose = F)
#formr_api_surveys(name_pattern = NULL, verbose = TRUE)
daten_scid5spq=formr_api_results(
run_name = "SKID5-SPQ",
"scid5spq",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_bdi2=formr_api_results(
run_name = "BDI2",
"bdi2",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_itq=formr_api_results(
run_name = "ITQ",
"itq",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_vds90=formr_api_results(
run_name = "VDS90",
"vds90",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_scl90r=formr_api_results(
run_name = "SCL90R",
"scl90r",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_pcl5lec5=formr_api_results(
run_name = "PCL5-LEC5",
"plc5_lec5",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_pcl5=formr_api_results(
run_name = "PCL5",
"plc5",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_pg13r=formr_api_results(
run_name = "PG13r",
"pg13r",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_ctq=formr_api_results(
run_name = "CTQ",
"ctq",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_bitc=formr_api_results(
run_name = "BIT-C",
"bit_c",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_acqbsqmi=formr_api_results(
run_name = "ACQ-BSQ-MI",
"acq_bsq_mi",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_lsas=formr_api_results(
run_name = "LSAS",
"lsas",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_desci=formr_api_results(
run_name = "DESC-I",
"desc_i",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_descii=formr_api_results(
run_name = "DESC-II",
"desc_ii",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_cuditr=formr_api_results(
run_name = "CUDIT",
"cudit",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_audit=formr_api_results(
run_name = "AUDIT",
"audit",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_pssi=formr_api_results(
run_name = "PSSI",
"pssi",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_iss20r=formr_api_results(
run_name = "ISS20R",
"iss20r",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_bsl=formr_api_results(
run_name = "BSL",
"bsl",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_desii=formr_api_results(
run_name = "DES-II",
"desii",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_bai=formr_api_results(
run_name = "BAI",
"bai",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_pds5=formr_api_results(
run_name = "PDS-5",
"pds5",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
daten_mkf30=formr_api_results(
run_name = "MKF-30",
"mkf30",
compute_scales = FALSE,
join = TRUE,
remove_test_sessions = FALSE,
verbose = FALSE
)
formr_api_logout(verbose = FALSE)

View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Injiziert den URL-Parameter-Block (Chiffre aus ?chiffre=... vorausfuellen)
# in alle app.R-Dateien neben diesem Skript (bzw. im Root, ausgefuehrt dort).
# Idempotent: bereits gepatchte Dateien werden uebersprungen. Legt vor jeder
# Aenderung ein .bak an.
set -euo pipefail
SNIPPET=' observe({
query = parseQueryString(session$clientData$url_search)
if (!is.null(query$chiffre) && nchar(trimws(query$chiffre)) > 0) {
updateTextInput(session, "chiffre", value = toupper(trimws(query$chiffre)))
}
})'
MARKER='parseQueryString(session$clientData$url_search)'
find . -maxdepth 2 -iname 'app.R' -print0 | while IFS= read -r -d '' f; do
if grep -qF "$MARKER" "$f"; then
echo "SKIP (bereits vorhanden): $f"
continue
fi
if ! grep -qE 'server[[:space:]]*=[[:space:]]*function\(input, output, session\)[[:space:]]*\{' "$f"; then
echo "WARNUNG: Server-Signatur nicht gefunden, uebersprungen: $f"
continue
fi
cp "$f" "$f.bak"
awk -v snippet="$SNIPPET" '
{ print }
/server[ \t]*=[ \t]*function\(input, output, session\)[ \t]*\{/ && !done {
print snippet
done = 1
}
' "$f.bak" > "$f"
echo "OK: $f (Backup: $f.bak)"
done

View file

@ -0,0 +1,267 @@
#!/usr/bin/env python3
"""Injects pseudonym-based matching into every Diagnostik-App Shiny app.R.
Usage (run from the "formr" folder that directly contains all the app
subfolders, next to server.py / get_pseudo.R / pseudonyme.db):
python inject_pseudonym.py # dry run: reports what WOULD happen
python inject_pseudonym.py --apply # actually writes the changes
Finds every "<app-folder>/app.R" itself (no shell globbing needed, so this
works the same from Windows cmd.exe as from bash). Idempotent (marker-based -
safe to rerun), makes a .bak next to each file it actually changes, and
leaves a file completely untouched if any of the required anchors can't be
found unambiguously - such files are reported with a reason instead of being
guessed at.
"""
import re
import shutil
import sys
from pathlib import Path
MARKER = "# --- pseudonym-support-injection v1 ---"
def inject(path, write=True):
path = str(path)
raw = open(path, encoding="utf-8", newline="").read()
uses_crlf = raw.count("\r\n") > raw.count("\n") - raw.count("\r\n")
text = raw.replace("\r\n", "\n")
if MARKER in text:
return "skip: already patched", text
orig = text
warnings = []
notes = []
# 1. UI field: a pseudonym textInput with a small italic hint instead of
# a bulky bold label. If textInput("chiffre", ...) is directly preceded
# by a simple single-purpose `div(style = "...",` wrapper (the common
# shape), clone a sibling div so both fields sit side by side in the
# same flex row instead of stacking. Otherwise fall back to a plain
# inline sibling insert (functional, just possibly stacked) and note it.
label_block = (
'tagList(\n'
'{i} "Pseudonym",\n'
'{i} tags$span(style = "font-weight: normal; font-style: italic; '
'font-size: 0.78em; color: #888; margin-left: 4px; white-space: nowrap;",\n'
'{i} "optional, hat Vorrang vor Chiffre")\n'
'{i} )'
)
m_wrapped = re.search(
r'^([ \t]*)div\(style\s*=\s*"[^"]*"\s*,\s*\n([ \t]*)textInput\("chiffre"',
text, re.MULTILINE,
)
if m_wrapped:
outer_indent, inner_indent = m_wrapped.group(1), m_wrapped.group(2)
label = label_block.format(i=inner_indent)
insertion = (
f'{outer_indent}div(style = "min-width: 360px; white-space: nowrap;",\n'
f'{inner_indent}textInput("pseudonym",\n'
f'{inner_indent} label = {label},\n'
f'{inner_indent} placeholder = "optional", width = "340px")\n'
f'{outer_indent}),\n'
)
text = text[: m_wrapped.start()] + insertion + text[m_wrapped.start() :]
else:
m = re.search(r'^([ \t]*)textInput\("chiffre"', text, re.MULTILINE)
if not m:
warnings.append('UI: textInput("chiffre" not found')
else:
indent = m.group(1)
label = label_block.format(i=indent)
insertion = (
f'{indent}textInput("pseudonym",\n'
f'{indent} label = {label},\n'
f'{indent} placeholder = "optional", width = "340px"),\n'
)
text = text[: m.start()] + insertion + text[m.start() :]
notes.append(
"UI: no simple preceding div(style=...) wrapper found around "
"textInput(\"chiffre\" - pseudonym field inserted inline; layout "
"may need a manual look (may stack instead of sitting side by side)"
)
# 2. URL prefill: new observe() block right after the server function
# signature, independent of/in addition to the existing chiffre one.
m = re.search(r"server\s*=\s*function\(input,\s*output,\s*session\)\s*\{\n", text)
if not m:
warnings.append("server = function(input, output, session) { not found")
else:
block = (
f" {MARKER}\n"
" observe({\n"
" query = parseQueryString(session$clientData$url_search)\n"
" if (!is.null(query$pseudonym) && nchar(trimws(query$pseudonym)) > 0) {\n"
' updateTextInput(session, "pseudonym", value = trimws(query$pseudonym))\n'
" }\n"
" })\n\n"
)
idx = m.end()
text = text[:idx] + block + text[idx:]
# Some apps run their chiffre-empty/format checks inside a dedicated
# top-level helper function (e.g. desii_validiere_chiffre(),
# validiere_chiffre()) defined OUTSIDE server() - such a function has no
# `input` in scope. Blindly rewriting a check there would compile fine
# but throw "object 'input' not found" at runtime on every single click,
# for every patient, whether pseudonym is used or not. So items 3 and 4
# only ever touch occurrences that are reachable from inside server()
# (i.e. at/after the server function's opening brace); anything found
# only earlier in the file is left completely untouched and reported as
# a note instead - pseudonym-only entry (no chiffre at all) just won't
# work for that one app, but the chiffre+pseudonym disambiguation (the
# actual bug this whole thing fixes) is unaffected either way.
m_anchor = re.search(r"server\s*=\s*function\(input,\s*output,\s*session\)\s*\{", text)
reactive_start = m_anchor.start() if m_anchor else None
def patch_reachable(text, target, replacement):
matches = list(re.finditer(re.escape(target), text))
reachable = [
mm for mm in matches
if reactive_start is not None and mm.start() >= reactive_start
]
for mm in reversed(reachable):
text = text[: mm.start()] + replacement + text[mm.end() :]
return text, len(matches), len(reachable)
# 3. Relax the "chiffre leer" gate - phrasing varies (some apps check
# `chiffre`, others read into `chiffre_roh` first).
empty_gate_variants = [
("nchar(chiffre) == 0",
"(nchar(trimws(input$pseudonym)) == 0 && nchar(chiffre) == 0)"),
("nchar(trimws(chiffre_roh)) == 0",
"(nchar(trimws(input$pseudonym)) == 0 && nchar(trimws(chiffre_roh)) == 0)"),
('trimws(chiffre_roh) == ""',
'(nchar(trimws(input$pseudonym)) == 0 && trimws(chiffre_roh) == "")'),
]
any_empty_gate_patched = False
for target, replacement in empty_gate_variants:
text, total, reachable = patch_reachable(text, target, replacement)
if reachable:
any_empty_gate_patched = True
if not any_empty_gate_patched:
notes.append(
"no reachable chiffre-empty gate found (the format-check gate below "
"usually already covers an empty chiffre too, so this is often fine)"
)
# 4. Relax the chiffre-format gate.
target = 'grepl("^[A-Z][0-9]{6}$", chiffre)'
replacement = '(nchar(trimws(input$pseudonym)) > 0 || grepl("^[A-Z][0-9]{6}$", chiffre))'
text, total, reachable = patch_reachable(text, target, replacement)
if reachable == 0:
if total > 0:
notes.append(
"chiffre-format check only exists inside a top-level helper function "
"with no access to the pseudonym input - pseudonym-only entry (no "
"chiffre at all) won't work for this app, but chiffre+pseudonym "
"disambiguation (the main fix) is unaffected"
)
else:
warnings.append(f'gate not found: "{target}"')
# 5. Backfill `chiffre` from the pseudonym right after the pseudonym
# script is sourced (pseudo now exists in .GlobalEnv - guaranteed
# name, set by the one shared get_pseudo.R).
target = "source(PFAD_PSEUDONYM_SKRIPT, local = FALSE)"
occurrences = [mm.start() for mm in re.finditer(re.escape(target), text)]
if len(occurrences) != 1:
warnings.append(
f'expected exactly 1 occurrence of source(PFAD_PSEUDONYM_SKRIPT...), found {len(occurrences)}'
)
else:
pos = occurrences[0]
line_start = text.rfind("\n", 0, pos) + 1
indent = re.match(r"[ \t]*", text[line_start:pos]).group(0)
end = pos + len(target)
backfill = (
f"\n{indent}if (nchar(trimws(input$pseudonym)) > 0) {{\n"
f'{indent} .pw_wert = trimws(input$pseudonym)\n'
f'{indent} .pw_tab = get("pseudo", envir = .GlobalEnv)\n'
f"{indent} .pw_treffer = .pw_tab[.pw_tab$pseudonym == .pw_wert, ]\n"
f"{indent} if (nrow(.pw_treffer) > 0) chiffre = toupper(trimws(.pw_treffer$chiffre[1]))\n"
f"{indent}}}"
)
text = text[:end] + backfill + text[end:]
# 6. Override the final session-id(s) with the exact pseudonym, so an
# ambiguous chiffre (multiple pseudonyms) doesn't silently pick the
# wrong / most-recent entry.
sid_pattern = re.compile(
r"^([ \t]*)([A-Za-z_][A-Za-z0-9_.]*)\s*=\s*"
r"(unique\(as\.character\(\w+\$pseudonym\)\)|unique\(\w+\$pseudonym\)|\w+\$pseudonym\[1\])[ \t]*$",
re.MULTILINE,
)
matches = list(sid_pattern.finditer(text))
if len(matches) == 0:
warnings.append("session-id assignment (X = unique(Y$pseudonym) / Y$pseudonym[1]) not found")
else:
for mm in reversed(matches):
indent, varname = mm.group(1), mm.group(2)
override = f"\n{indent}if (nchar(trimws(input$pseudonym)) > 0) {varname} = trimws(input$pseudonym)"
text = text[: mm.end()] + override + text[mm.end() :]
if warnings:
return "skip: " + "; ".join(warnings), orig
out = text.replace("\n", "\r\n") if uses_crlf else text
if write:
shutil.copy(path, path + ".bak")
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(out)
status = "patched"
if notes:
status += " (note: " + "; ".join(notes) + ")"
return status, out
def main():
args = sys.argv[1:]
apply_changes = "--apply" in args
args = [a for a in args if a != "--apply"]
if args:
paths = [Path(a) for a in args]
else:
paths = sorted(Path(".").glob("*/app.R"))
if not paths:
print('No "*/app.R" files found in the current directory. '
"Run this from the folder that directly contains all the app "
"subfolders (next to server.py / get_pseudo.R / pseudonyme.db), "
"or pass explicit paths.")
sys.exit(1)
print(f"{'DRY RUN (no files will be changed - pass --apply to write)' if not apply_changes else 'APPLYING CHANGES'}")
print(f"{len(paths)} app(s) found.\n")
counts = {"patched": 0, "patched-with-note": 0, "skip-already-patched": 0, "skip": 0}
for p in paths:
try:
status, _ = inject(p, write=apply_changes)
except Exception as e:
status = f"skip: UNEXPECTED ERROR ({type(e).__name__}: {e}) - file left untouched, please report this"
if status.startswith("skip: already patched"):
counts["skip-already-patched"] += 1
elif status.startswith("skip:"):
counts["skip"] += 1
elif "(note:" in status:
counts["patched-with-note"] += 1
else:
counts["patched"] += 1
print(f"{p}: {status}")
print(
f"\n{counts['patched']} clean, {counts['patched-with-note']} patched-with-note, "
f"{counts['skip']} skipped, {counts['skip-already-patched']} already patched."
)
if not apply_changes:
print("This was a dry run - nothing was written. Rerun with --apply once you're happy.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,40 @@
library(formr)
formr_store_keys(host = "https://diagnostik.karneboge.de/api", client_id = "6be1cb43d9964516089238a292b30098", client_secret = "e10c32d15c5ad9ecf44dc17d3d997fd3e7fc4c4153478fec5006904a7056f02e")
formr_api_authenticate(host = "https://diagnostik.karneboge.de/api")
pseudoID="4V8QodhlrGKifPKRdSpSFj-OFC4etlCvaa4GCQ1FAXE_PjDE7UdBCynDVa29guRu"
#SKID 5 SPQ ####
formr_api_create_session(
"SKID5-SPQ",
codes = pseudoID,
testing = FALSE,
verbose = TRUE
)
formr_api_logout(verbose = FALSE)
as.data.frame(
formr_api_sessions(
"SKID5-SPQ",
session_codes = pseudoID,
active = NULL,
testing = NULL,
limit = 1000,
offset = 0,
verbose = TRUE
)
)
#BDI2 ####
formr_api_create_session(
"BDI2",
codes = pseudoID,
testing = FALSE,
verbose = TRUE
)
formr_api_logout(verbose = FALSE)

View file

@ -0,0 +1,66 @@
# Basisordner, in dem alle Shiny App Unterordner liegen
$baseDir = "C:\Users\johnn\Desktop\formr"
# Voller Pfad zu Rscript.exe
$rscriptExe = "C:\Program Files\R\R-4.6.0\bin\x64\Rscript.exe"
# Logdatei für die Ergebnisse
$logFile = Join-Path $baseDir "renv_restore_log.txt"
"Start: $(Get-Date)" | Out-File -FilePath $logFile
# Alle Unterordner finden, die eine renv.lock enthalten
$appFolders = Get-ChildItem -Path $baseDir -Recurse -Filter "renv.lock" |
ForEach-Object { $_.Directory.FullName } |
Sort-Object -Unique
Write-Host "Gefundene Apps mit renv.lock: $($appFolders.Count)"
$erfolgreich = @()
$fehlgeschlagen = @()
foreach ($folder in $appFolders) {
Write-Host "Bearbeite: $folder"
"----" | Out-File -FilePath $logFile -Append
"App: $folder" | Out-File -FilePath $logFile -Append
# R Befehl als Einzeiler, escaped für Rscript -e
$rCommand = "renv::restore(project = '$($folder -replace '\\','/')', prompt = FALSE)"
$output = & $rscriptExe -e $rCommand 2>&1
$output | Out-File -FilePath $logFile -Append
# Grobe Erfolgsprüfung: enthält die Ausgabe Fehlerhinweise?
$hatFehler = $output -match "Error|error in|konnte nicht|es gibt kein Paket"
if ($hatFehler) {
"Status: FEHLER" | Out-File -FilePath $logFile -Append
$fehlgeschlagen += $folder
} else {
"Status: OK" | Out-File -FilePath $logFile -Append
$erfolgreich += $folder
}
}
"Ende: $(Get-Date)" | Out-File -FilePath $logFile -Append
# Zusammenfassung
Write-Host ""
Write-Host "===== Zusammenfassung ====="
Write-Host "Gesamt: $($appFolders.Count)"
Write-Host "Erfolgreich: $($erfolgreich.Count)"
Write-Host "Fehlgeschlagen: $($fehlgeschlagen.Count)"
if ($fehlgeschlagen.Count -gt 0) {
Write-Host ""
Write-Host "Fehlgeschlagene Ordner:"
$fehlgeschlagen | ForEach-Object { Write-Host " - $_" }
}
"===== Zusammenfassung =====" | Out-File -FilePath $logFile -Append
"Gesamt: $($appFolders.Count)" | Out-File -FilePath $logFile -Append
"Erfolgreich: $($erfolgreich.Count)" | Out-File -FilePath $logFile -Append
"Fehlgeschlagen: $($fehlgeschlagen.Count)" | Out-File -FilePath $logFile -Append
$fehlgeschlagen | Out-File -FilePath $logFile -Append
Write-Host ""
Write-Host "Fertig. Log unter: $logFile"

View file

@ -0,0 +1,461 @@
[CmdletBinding()]
param(
[ValidateSet("Status", "Restore", "Snapshot", "SyncToLibrary")]
[string]$Mode = "Status",
[string]$Root = "C:\Diagnostik\formr",
[string]$Rscript = "C:\Program Files\R\R-4.6.0\bin\x64\Rscript.exe",
[switch]$IncludeNestedProjects
)
$ErrorActionPreference = "Stop"
function Write-Log {
param(
[Parameter(Mandatory)]
[AllowEmptyString()]
[string]$Message,
[switch]$Initialize
)
$maxAttempts = 10
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
try {
if ($Initialize) {
Set-Content -LiteralPath $logFile -Value $Message -Encoding UTF8 -ErrorAction Stop
}
else {
Add-Content -LiteralPath $logFile -Value $Message -Encoding UTF8 -ErrorAction Stop
}
return
}
catch [System.IO.IOException] {
if ($attempt -eq $maxAttempts) {
Write-Warning "Logdatei konnte nach $maxAttempts Versuchen nicht beschrieben werden: $logFile"
Write-Warning $_.Exception.Message
return
}
Start-Sleep -Milliseconds (100 * $attempt)
}
}
}
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$logFile = Join-Path $Root "renv_${Mode}_${timestamp}_PID${PID}.log"
if (-not (Test-Path -LiteralPath $Rscript -PathType Leaf)) {
throw "Rscript wurde nicht gefunden: $Rscript"
}
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
throw "Stammverzeichnis wurde nicht gefunden: $Root"
}
function ConvertTo-RString {
param([Parameter(Mandatory)][string]$Value)
return $Value.Replace("\", "/").Replace("'", "\'")
}
function Invoke-RenvProject {
param(
[Parameter(Mandatory)][string]$Project,
[Parameter(Mandatory)][string]$Action
)
$projectR = ConvertTo-RString -Value $Project
$actionCode = switch ($Action) {
"Status" {
@'
# Nur prüfen
'@
}
"Restore" {
@'
renv::restore(project = project, prompt = FALSE)
'@
}
"Snapshot" {
@'
lockfile <- file.path(project, "renv.lock")
backup <- paste0(lockfile, ".bak_", format(Sys.time(), "%Y%m%d_%H%M%S"))
if (file.exists(lockfile)) {
file.copy(lockfile, backup, overwrite = FALSE)
message("Lockfile-Backup: ", backup)
}
renv::snapshot(project = project, prompt = FALSE)
'@
}
"SyncToLibrary" {
@'
initial_status <- renv::status(project = project)
if (isTRUE(initial_status$synchronized)) {
message("Projekt ist bereits synchron; keine Änderung.")
} else {
library_path <- renv::paths$library(project = project)
# Unvollständige Paketordner und Installationsreste entfernen.
if (dir.exists(library_path)) {
entries <- list.dirs(
library_path,
full.names = TRUE,
recursive = FALSE
)
invalid <- entries[
grepl("^00LOCK", basename(entries)) |
!file.exists(file.path(entries, "DESCRIPTION"))
]
invalid <- unique(invalid)
if (length(invalid)) {
message(
"Entferne unvollständige Paketordner: ",
paste(basename(invalid), collapse = ", ")
)
unlink(
invalid,
recursive = TRUE,
force = TRUE
)
}
}
# Direkte Projektabhängigkeiten aus dem Code.
dependencies <- renv::dependencies(
path = project,
progress = FALSE,
errors = "reported"
)
direct_packages <- sort(
unique(stats::na.omit(dependencies$Package))
)
# Zusätzlich alle im Lockfile dokumentierten Pakete berücksichtigen.
lockfile_path <- file.path(project, "renv.lock")
lock_packages <- character()
if (file.exists(lockfile_path)) {
lock <- renv::lockfile_read(lockfile_path)
lock_packages <- names(lock$Packages)
}
base_packages <- rownames(
installed.packages(priority = "base")
)
target_packages <- sort(unique(c(
direct_packages,
lock_packages
)))
target_packages <- setdiff(
target_packages,
base_packages
)
valid_installed_packages <- function() {
if (!dir.exists(library_path)) {
return(character())
}
dirs <- list.dirs(
library_path,
full.names = TRUE,
recursive = FALSE
)
basename(
dirs[file.exists(file.path(dirs, "DESCRIPTION"))]
)
}
missing <- setdiff(
target_packages,
valid_installed_packages()
)
# Fehlende Pakete einzeln installieren. Fehlgeschlagene Pakete werden
# mehrfach versucht, damit zuerst installierte Abhängigkeiten später
# abhängige Pakete ermöglichen.
max_passes <- 3L
failed <- missing
if (length(failed)) {
for (pass in seq_len(max_passes)) {
message(
"Installationsdurchgang ",
pass,
" von ",
max_passes,
": ",
paste(failed, collapse = ", ")
)
next_failed <- character()
for (package in failed) {
message("Installiere Paket: ", package)
ok <- tryCatch(
{
renv::install(
packages = package,
project = project,
rebuild = TRUE,
prompt = FALSE
)
TRUE
},
error = function(error) {
message(
"Installation fehlgeschlagen für ",
package,
": ",
conditionMessage(error)
)
FALSE
}
)
if (!isTRUE(ok)) {
next_failed <- c(next_failed, package)
}
}
failed <- unique(next_failed)
# Pakete können als Abhängigkeiten anderer Installationen
# erfolgreich hinzugekommen sein.
failed <- setdiff(
failed,
valid_installed_packages()
)
if (!length(failed)) {
break
}
}
}
still_missing <- setdiff(
target_packages,
valid_installed_packages()
)
if (length(still_missing)) {
stop(
"Nach mehreren Installationsdurchgängen fehlen weiterhin: ",
paste(still_missing, collapse = ", "),
". Kein Snapshot durchgeführt."
)
}
# Vor dem Snapshot Lockfile sichern.
backup <- paste0(
lockfile_path,
".bak_",
format(Sys.time(), "%Y%m%d_%H%M%S")
)
if (file.exists(lockfile_path)) {
copied <- file.copy(
lockfile_path,
backup,
overwrite = FALSE
)
if (!isTRUE(copied)) {
stop(
"Lockfile-Backup konnte nicht erstellt werden: ",
backup
)
}
message("Lockfile-Backup: ", backup)
}
# Snapshot führt zusätzlich eine transitive Vorabvalidierung durch.
# Bei einem Fehler bleibt das Backup erhalten und das Projekt wird
# nicht als erfolgreich markiert.
message("Aktualisiere renv.lock.")
renv::snapshot(
project = project,
prompt = FALSE
)
}
'@
}
}
$rCode = @"
options(
repos = c(CRAN = "https://cloud.r-project.org"),
renv.config.auto.snapshot = FALSE
)
project <- '$projectR'
message("Projekt: ", project)
message("R-Version: ", R.version.string)
$actionCode
status <- renv::status(project = project)
if (!isTRUE(status`$synchronized)) {
message("ERGEBNIS: NICHT SYNCHRON")
quit(save = "no", status = 10L)
}
message("ERGEBNIS: SYNCHRON")
quit(save = "no", status = 0L)
"@
$tempRFile = Join-Path ([System.IO.Path]::GetTempPath()) (
"renv_{0}_{1}.R" -f ([System.IO.Path]::GetRandomFileName()), $PID
)
try {
# R-Code nicht mit -e übergeben. Unter Windows können dabei Anführungszeichen
# verloren gehen, sodass z. B. C:/Users/... als ungequoteter R-Code ankommt.
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($tempRFile, $rCode, $utf8NoBom)
$stdoutFile = Join-Path ([System.IO.Path]::GetTempPath()) (
"renv_stdout_{0}_{1}.txt" -f ([System.IO.Path]::GetRandomFileName()), $PID
)
$stderrFile = Join-Path ([System.IO.Path]::GetTempPath()) (
"renv_stderr_{0}_{1}.txt" -f ([System.IO.Path]::GetRandomFileName()), $PID
)
Push-Location -LiteralPath $Project
try {
$process = Start-Process `
-FilePath $Rscript `
-ArgumentList @("--no-save", "--no-restore", $tempRFile) `
-WorkingDirectory $Project `
-RedirectStandardOutput $stdoutFile `
-RedirectStandardError $stderrFile `
-NoNewWindow `
-Wait `
-PassThru
$exitCode = $process.ExitCode
$output = @()
if (Test-Path -LiteralPath $stdoutFile) {
$output += Get-Content -LiteralPath $stdoutFile -ErrorAction SilentlyContinue
}
if (Test-Path -LiteralPath $stderrFile) {
$output += Get-Content -LiteralPath $stderrFile -ErrorAction SilentlyContinue
}
}
finally {
Pop-Location
Remove-Item -LiteralPath $stdoutFile -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $stderrFile -Force -ErrorAction SilentlyContinue
}
}
finally {
Remove-Item -LiteralPath $tempRFile -Force -ErrorAction SilentlyContinue
}
foreach ($line in $output) {
$text = [string]$line
Write-Host $text
Write-Log -Message $text
}
return $exitCode
}
$searchDepth = if ($IncludeNestedProjects) { 20 } else { 1 }
$projects = Get-ChildItem -LiteralPath $Root -Directory |
Where-Object {
Test-Path -LiteralPath (Join-Path $_.FullName "renv.lock") -PathType Leaf
}
if ($IncludeNestedProjects) {
$projects = Get-ChildItem -LiteralPath $Root -Directory -Recurse -Depth $searchDepth |
Where-Object {
Test-Path -LiteralPath (Join-Path $_.FullName "renv.lock") -PathType Leaf
}
}
$projects = $projects | Sort-Object FullName -Unique
if (-not $projects) {
throw "Keine Projekte mit renv.lock unter '$Root' gefunden."
}
Write-Log -Message "Start: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Initialize
Write-Log -Message "Modus: $Mode"
Write-Log -Message "Rscript: $Rscript"
Write-Log -Message "Projekte: $($projects.Count)"
$results = foreach ($project in $projects) {
Write-Host ""
Write-Host ("=" * 78)
Write-Host "[$Mode] $($project.FullName)"
Write-Host ("=" * 78)
Write-Log -Message "`r`n[$Mode] $($project.FullName)"
try {
$code = Invoke-RenvProject -Project $project.FullName -Action $Mode
[pscustomobject]@{
Project = $project.Name
Path = $project.FullName
Result = switch ($code) {
0 { "Synchron" }
10 { "Nicht synchron" }
default { "Fehler, Exitcode $code" }
}
ExitCode = $code
}
}
catch {
$message = $_.Exception.Message
Write-Warning $message
Write-Log -Message "FEHLER: $message"
[pscustomobject]@{
Project = $project.Name
Path = $project.FullName
Result = "PowerShell-Fehler"
ExitCode = 99
}
}
}
Write-Host ""
Write-Host "Zusammenfassung"
$results | Format-Table Project, Result, ExitCode -AutoSize
$csvFile = [System.IO.Path]::ChangeExtension($logFile, ".csv")
$results | Export-Csv -LiteralPath $csvFile -NoTypeInformation -Encoding UTF8
Write-Host ""
Write-Host "Log: $logFile"
Write-Host "CSV: $csvFile"
if ($results.ExitCode -contains 99 -or ($results.ExitCode | Where-Object { $_ -notin 0, 10 })) {
exit 1
}
if ($results.ExitCode -contains 10) {
exit 10
}
exit 0

View file

@ -0,0 +1,6 @@
library(formr)
formr_store_keys(host = "https://diagnostik.karneboge.de/api", client_id = "be81e57d26aa5da69e235ff6d04e7cdd", client_secret = "a88f352517f2f49d840e927cb3a92aa4c22031310d092630db9bf57f8fdf25e1")
formr_api_authenticate(host = "https://diagnostik.karneboge.de/api")
formr_api_upload_file("IST-Screening", "./Body_Female.png", verbose = TRUE)
formr_api_files("IST-Screening", verbose = TRUE)