chore: initialize repository baseline

Import the existing Electron + React + TypeScript app as the version-control
baseline before the scanner rework (C# input/capture sidecar, resolution-anchored
layout profiles, OCR preprocessing, eval harness, rescan-merge, GOOD interop).

Housekeeping in this commit:
- Remove orphaned temp_inputhelper_block.ts (duplicate of the input-helper script).
- Ignore .claude/scheduled_tasks.lock local session state.
- Add .gitattributes to normalize line endings (LF in repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-07-05 20:31:01 +02:00
commit e76d88e0c7
147 changed files with 26220 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
param(
[string]$ProjectRoot
)
# Mit -NoExit gestartet: dieses Fenster bleibt immer offen (siehe dev-admin.cmd),
# aber ein sauberer try/catch mit klarer Meldung ist trotzdem besser als ein
# roher Stacktrace, wenn z.B. der Dev-Port schon belegt ist.
$ErrorActionPreference = "Stop"
try {
$project = (Resolve-Path -LiteralPath $ProjectRoot).Path
Write-Host "Projekt: $project"
# A UAC-elevated process gets its environment rebuilt fresh from the
# registry; it does NOT inherit PATH edits that only exist in the calling
# (non-elevated) shell session (e.g. a version manager that only patched
# the current terminal). If node/npm resolve normally but not here, that
# is almost always the cause - fail with a clear message instead of a
# cryptic "npm is not recognized" a few lines down.
$npmCommand = Get-Command npm.cmd -ErrorAction SilentlyContinue
if (-not $npmCommand) { $npmCommand = Get-Command npm -ErrorAction SilentlyContinue }
$nodeCommand = Get-Command node.exe -ErrorAction SilentlyContinue
if (-not $nodeCommand) { $nodeCommand = Get-Command node -ErrorAction SilentlyContinue }
if (-not $npmCommand -or -not $nodeCommand) {
Write-Host ""
Write-Host "FEHLER: npm/node wurden in diesem administrativen Fenster nicht gefunden." -ForegroundColor Red
Write-Host "npm gefunden: $([bool]$npmCommand) node gefunden: $([bool]$nodeCommand)" -ForegroundColor Red
Write-Host "Ein 'Als Administrator ausfuehren'-Prozess bekommt seine Umgebung frisch aus der Registry -" -ForegroundColor Red
Write-Host "PATH-Aenderungen, die nur in deiner normalen (nicht-elevierten) Sitzung gelten (z.B. nvm/Volta" -ForegroundColor Red
Write-Host "ohne dauerhafte Registry-Eintragung), sind hier nicht sichtbar, obwohl 'npm run dev' normal funktioniert." -ForegroundColor Red
Write-Host "PATH in diesem Fenster:" -ForegroundColor DarkGray
Write-Host $env:PATH -ForegroundColor DarkGray
Write-Host ""
Write-Host "Loesung: node/npm dauerhaft im PATH eintragen (System-Umgebungsvariablen, nicht nur die Sitzung)," -ForegroundColor Yellow
Write-Host "z.B. ueber die Windows-Systemsteuerung 'Umgebungsvariablen bearbeiten' oder 'setx PATH ...'." -ForegroundColor Yellow
throw "npm/node nicht im administrativen PATH gefunden."
}
# Kill stale instances of THIS project BEFORE checking the port: a leftover
# electron/vite/node/input-helper cluster from a previous run (e.g. a window
# force-closed via Task Manager, which skips main.ts's graceful will-quit
# cleanup and orphans the spawned input-helper.ps1 child) is the most common
# reason a fresh start silently talks to an old instance instead of
# replacing it. Shared with "npm run dev" itself via the predev hook, so
# both start paths get the same guarantee.
& (Join-Path $PSScriptRoot "kill-stale-instances.ps1")
$devPort = 5173
$portInUse = $null
try {
$portInUse = Get-NetTCPConnection -LocalPort $devPort -State Listen -ErrorAction Stop | Select-Object -First 1
} catch {
# Get-NetTCPConnection kann auf manchen Systemen fehlen; das ist kein Fehler.
$portInUse = $null
}
if ($portInUse) {
$portOwner = Get-Process -Id $portInUse.OwningProcess -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "WARNUNG: Port $devPort ist noch belegt (Prozess: $($portOwner.ProcessName), PID $($portInUse.OwningProcess)) - auch nach dem Beenden alter Instanzen dieses Projekts." -ForegroundColor Yellow
Write-Host "Das ist wahrscheinlich ein unabhaengiger Prozess (z.B. ein anderes Projekt auf Port $devPort)." -ForegroundColor Yellow
Write-Host "'npm run dev' weicht dann auf einen anderen Port aus oder verbindet sich mit dem falschen Server." -ForegroundColor Yellow
Write-Host "Falls noetig: Prozess PID $($portInUse.OwningProcess) manuell beenden und dieses Fenster neu starten." -ForegroundColor Yellow
Write-Host ""
}
Set-Location -LiteralPath $project
Write-Host "Starte npm run dev (Administrator)..."
npm run dev
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host ""
Write-Host "npm run dev wurde mit Fehlercode $exitCode beendet." -ForegroundColor Red
Write-Host "Die Ausgabe oben zeigt die genaue Ursache (z.B. belegter Port, fehlende Abhaengigkeiten)." -ForegroundColor Red
} else {
Write-Host ""
Write-Host "npm run dev wurde beendet (Fenster schliessen oder Strg+C fuer Neustart)."
}
} catch {
Write-Host ""
Write-Host "Fehler beim Start: $($_.Exception.Message)" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
}
Write-Host ""
Write-Host "Dieses Fenster bleibt offen (siehe Meldungen oben). Schliessen mit Enter oder dem Fenster-X."
+233
View File
@@ -0,0 +1,233 @@
const fs = require('node:fs');
const path = require('node:path');
const genshin = require('genshin-db');
const opts = { matchCategories: true, verboseCategories: true, resultLanguage: 'English', queryLanguages: ['English'] };
function asArray(value) {
return Array.isArray(value) ? value : [];
}
const characters = asArray(genshin.characters('names', opts))
.map((entry) => ({
id: entry.id,
name: entry.name,
rarity: entry.rarity,
element: entry.elementText,
weapon: entry.weaponText,
}))
.filter((entry) => entry.name)
.sort((a, b) => a.name.localeCompare(b.name));
const artifacts = asArray(genshin.artifacts('names', opts))
.map((entry) => ({
id: entry.id,
name: entry.name,
rarityList: entry.rarityList ?? [],
pieces: [entry.flower, entry.plume, entry.sands, entry.goblet, entry.circlet]
.filter(Boolean)
.map((piece) => ({ name: piece.name, relicType: piece.relicType })),
}))
.filter((entry) => entry.name)
.sort((a, b) => a.name.localeCompare(b.name));
const artifactPieces = artifacts
.flatMap((set) =>
set.pieces.map((piece) => ({
name: piece.name,
setName: set.name,
slot: slotFromRelicType(piece.relicType),
relicType: piece.relicType,
})),
)
.sort((a, b) => a.name.localeCompare(b.name));
const slotByPiece = Object.fromEntries(artifactPieces.map((piece) => [piece.name, piece.slot]));
const setByPiece = Object.fromEntries(artifactPieces.map((piece) => [piece.name, piece.setName]));
const mainStats = [
'Elemental Mastery',
'Energy Recharge',
'CRIT Rate',
'CRIT DMG',
'Healing Bonus',
'ATK%',
'HP%',
'DEF%',
'ATK',
'HP',
'DEF',
'Hydro DMG Bonus',
'Pyro DMG Bonus',
'Electro DMG Bonus',
'Cryo DMG Bonus',
'Dendro DMG Bonus',
'Anemo DMG Bonus',
'Geo DMG Bonus',
'Physical DMG Bonus',
];
const substats = ['CRIT DMG', 'CRIT Rate', 'Energy Recharge', 'Elemental Mastery', 'ATK', 'ATK%', 'HP', 'HP%', 'DEF', 'DEF%'];
const mainStatsBySlot = {
'Flower of Life': ['HP'],
'Plume of Death': ['ATK'],
'Sands of Eon': ['HP%', 'ATK%', 'DEF%', 'Energy Recharge', 'Elemental Mastery'],
'Goblet of Eonothem': [
'HP%',
'ATK%',
'DEF%',
'Elemental Mastery',
'Hydro DMG Bonus',
'Pyro DMG Bonus',
'Electro DMG Bonus',
'Cryo DMG Bonus',
'Dendro DMG Bonus',
'Anemo DMG Bonus',
'Geo DMG Bonus',
'Physical DMG Bonus',
],
'Circlet of Logos': ['HP%', 'ATK%', 'DEF%', 'Elemental Mastery', 'CRIT Rate', 'CRIT DMG', 'Healing Bonus'],
};
const mainStatValueReferences = {
'Flower of Life': [{ stat: 'HP', base: 717, max: 4780 }],
'Plume of Death': [{ stat: 'ATK', base: 47, max: 311 }],
'Sands of Eon': [
{ stat: 'HP%', base: 7.0, max: 46.6 },
{ stat: 'ATK%', base: 7.0, max: 46.6 },
{ stat: 'DEF%', base: 8.7, max: 58.3 },
{ stat: 'Energy Recharge', base: 7.8, max: 51.8 },
{ stat: 'Elemental Mastery', base: 28, max: 187 },
],
'Goblet of Eonothem': [
{ stat: 'HP%', base: 7.0, max: 46.6 },
{ stat: 'ATK%', base: 7.0, max: 46.6 },
{ stat: 'DEF%', base: 8.7, max: 58.3 },
{ stat: 'Elemental Mastery', base: 28, max: 187 },
{ stat: 'Hydro DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Pyro DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Electro DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Cryo DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Dendro DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Anemo DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Geo DMG Bonus', base: 7.0, max: 46.6 },
{ stat: 'Physical DMG Bonus', base: 8.7, max: 58.3 },
],
'Circlet of Logos': [
{ stat: 'HP%', base: 7.0, max: 46.6 },
{ stat: 'ATK%', base: 7.0, max: 46.6 },
{ stat: 'DEF%', base: 8.7, max: 58.3 },
{ stat: 'Elemental Mastery', base: 28, max: 187 },
{ stat: 'CRIT Rate', base: 4.7, max: 31.1 },
{ stat: 'CRIT DMG', base: 9.3, max: 62.2 },
{ stat: 'Healing Bonus', base: 5.4, max: 35.9 },
],
};
const data = {
schemaVersion: 2,
generatedAt: new Date().toISOString(),
source: {
package: 'genshin-db',
version: require('genshin-db/package.json').version,
resultLanguage: opts.resultLanguage,
},
sourceVersions: {
genshinDb: require('genshin-db/package.json').version,
resultLanguage: opts.resultLanguage,
},
sourceVersion: `genshin-db@${require('genshin-db/package.json').version}`,
characters,
artifactSets: artifacts,
artifactPieces,
slotByPiece,
setByPiece,
slots: ['Flower of Life', 'Plume of Death', 'Sands of Eon', 'Goblet of Eonothem', 'Circlet of Logos'],
mainStats,
mainStatsBySlot,
mainStatValueReferences,
substats,
stats: {
main: mainStats,
mainBySlot: mainStatsBySlot,
sub: substats,
},
aliases: {
stats: {
'Crit Damage': 'CRIT DMG',
'Critical Damage': 'CRIT DMG',
'Crit Rate': 'CRIT Rate',
'Critical Rate': 'CRIT Rate',
'Energy Recharge %': 'Energy Recharge',
'Elemental Master': 'Elemental Mastery',
'Elemental Masterie': 'Elemental Mastery',
'Heal Bonus': 'Healing Bonus',
},
textReplacements: {
'CIT DMG': 'CRIT DMG',
'CRIT DMG+I': 'CRIT DMG+1',
'CRIT Rate+Z': 'CRIT Rate+2',
'Energv Recharge': 'Energy Recharge',
'Elemental Masterv': 'Elemental Mastery',
'Equipped;': 'Equipped:',
},
slotAliases: {
'Sands of Eon Vi': 'Sands of Eon',
'Sands of Eon V': 'Sands of Eon',
'Sands of Eon 2': 'Sands of Eon',
'Flower of Life 2': 'Flower of Life',
'Flower of Lif': 'Flower of Life',
'Goblet of Eonotherm': 'Goblet of Eonothem',
'Goblet of Eonothemn': 'Goblet of Eonothem',
'Circlet of Logas': 'Circlet of Logos',
'Circlet of Loges': 'Circlet of Logos',
},
setAliases: {
'Viridescent Venere': 'Viridescent Venerer',
'Maiden Beloved:': 'Maiden Beloved',
'Gladiators Finale': "Gladiator's Finale",
},
pieceAliases: {
'A Note in Springs Leich': "A Note in Spring's Leich",
'Viridescent Vencrers Vessel': "Viridescent Venerer's Vessel",
'Holy Crown of the Believer ': 'Holy Crown of the Believer',
},
characterAliases: {
'Citlall': 'Citlali',
'Sandrone ': 'Sandrone',
'Qiqi ': 'Qiqi',
},
},
uiProfiles: {
artifactDetailEn: {
language: 'English',
supportedResolutions: ['1920x1080', '2560x1440', '3840x2160'],
detailPanel: { x: 0.5, y: 0.05, width: 0.45, height: 0.9 },
note: 'Relative profile used as scanner contract; runtime crops may tune offsets from review samples.',
},
},
};
const outPath = path.join(process.cwd(), 'src', 'data', 'genshinGameData.json');
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, JSON.stringify(data, null, 2) + '\n');
console.log(`generated ${outPath}`);
console.log(`${characters.length} characters, ${artifacts.length} artifact sets`);
console.log(`${artifactPieces.length} artifact pieces, ${mainStats.length} main stats, ${substats.length} substats`);
function slotFromRelicType(relicType) {
switch (relicType) {
case 'EQUIP_BRACER':
return 'Flower of Life';
case 'EQUIP_NECKLACE':
return 'Plume of Death';
case 'EQUIP_SHOES':
return 'Sands of Eon';
case 'EQUIP_RING':
return 'Goblet of Eonothem';
case 'EQUIP_DRESS':
return 'Circlet of Logos';
default:
return '';
}
}
+40
View File
@@ -0,0 +1,40 @@
# Beendet alle laufenden Prozesse dieses Projekts (Electron, Node/Vite, und
# einen verwaisten lokalen Input-Helper-Kindprozess), bevor eine neue Instanz
# startet.
#
# Warum das noetig ist: Ein normales Schliessen der App (X-Button) raeumt
# sauber auf (main.ts: app.on("will-quit") beendet den gespawnten
# input-helper.ps1-Kindprozess). Wird die App aber gewaltsam beendet - z.B.
# ueber Task-Manager "Task beenden" oder ein Skript, das Stop-Process -Force
# auf den Electron-Prozess anwendet - greift dieser Cleanup-Handler NICHT,
# und der Kindprozess (PowerShell mit dem Input-Helper) bleibt als Waise
# aktiv, obwohl das Hauptfenster laengst weg ist. Dieses Skript faengt genau
# das ab: es wird vor JEDEM Start (npm run dev und npm run dev:admin, ueber
# den "predev"-Hook) ausgefuehrt und raeumt vorherige Instanzen kompromisslos
# weg, egal wie sie beendet wurden.
$ErrorActionPreference = "Stop"
$project = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
$electronPath = Join-Path $project "node_modules\electron\dist\electron.exe"
$killed = 0
Get-CimInstance Win32_Process |
Where-Object {
($_.Name -eq "electron.exe" -and $_.ExecutablePath -eq $electronPath) -or
($_.Name -eq "node.exe" -and $_.CommandLine -like "*$project*") -or
($_.Name -eq "powershell.exe" -and $_.CommandLine -like "*input-helper.ps1*")
} |
ForEach-Object {
Write-Host "Beende alte Instanz: $($_.Name) (PID $($_.ProcessId))"
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
$killed++
}
if ($killed -gt 0) {
# Windows braucht einen Moment, um Ports/Handles wirklich freizugeben.
Start-Sleep -Milliseconds 500
Write-Host "$killed alte Prozess(e) beendet."
} else {
Write-Host "Keine alten Instanzen gefunden."
}