Docs: Update MIGRATION_PLAN.md to v0.4.0 with new features (Company Explorer)

This commit is contained in:
2026-01-09 10:18:45 +00:00
parent 1f34534474
commit 1b5a8e3b96
5 changed files with 179 additions and 33 deletions

View File

@@ -1,4 +1,4 @@
# Migrations-Plan: Legacy GSheets -> Company Explorer (Robotics Edition)
# Migrations-Plan: Legacy GSheets -> Company Explorer (Robotics Edition v0.4.0)
**Kontext:** Neuanfang für die Branche **Robotik & Facility Management**.
**Ziel:** Ablösung von Google Sheets/CLI durch eine Web-App ("Company Explorer") mit SQLite-Backend.
@@ -77,4 +77,46 @@ Wir kapseln das neue Projekt vollständig ab ("Fork & Clean").
2. **Setup:** Init `company-explorer` (Backend + Frontend Skeleton).
3. **Foundation:** DB-Schema + "List Matcher" (Deduplizierung ist Prio A für Operations).
4. **Enrichment:** Implementierung des Scrapers + Signal Detector (Robotics).
5. **UI:** React Interface für die Daten.
5. **UI:** React Interface für die Daten.
## 7. Historie & Fixes (Jan 2026)
* **[UPGRADE] v0.4.0: Export & Manual Impressum**
* **JSON Export:** Erweiterung der Detailansicht um einen "Export JSON"-Button, der alle Unternehmensdaten (inkl. Anreicherungen und Signale) herunterlädt.
* **Zeitstempel:** Anzeige des Erstellungsdatums für jeden Anreicherungsdatensatz (Wikipedia, AI Dossier, Impressum) in der Detailansicht.
* **Manuelle Impressum-URL:** Möglichkeit zur manuellen Eingabe einer Impressum-URL in der Detailansicht, um die Extraktion von Firmendaten zu erzwingen.
* **Frontend-Fix:** Behebung eines Build-Fehlers (`Unexpected token`) in `Inspector.tsx` durch Entfernung eines duplizierten JSX-Blocks.
* **[UPGRADE] v2.6.2: Report Completeness & Edit Mode**
* **Edit Hard Facts:** Neue Funktion in Phase 1 ("Edit Raw Data") erlaubt die manuelle Korrektur der extrahierten technischen JSON-Daten.
* **Report-Update:** Phase 5 Prompt wurde angepasst, um explizit die Ergebnisse aus Phase 2 (ICPs & Data Proxies) im finalen Report aufzuführen.
* **Backend-Fix:** Korrektur eines Fehlers beim Speichern von JSON-Daten, der auftrat, wenn Datenbank-Inhalte als Strings vorlagen.
* **[UPGRADE] v2.6.1: Stability & UI Improvements**
* **White Screen Fix:** Robuste Absicherung des Frontends gegen `undefined`-Werte beim Laden älterer Sitzungen (`optional chaining`).
* **Session Browser:** Komplettes Redesign der Sitzungsübersicht zu einer übersichtlichen Listenansicht mit Icons (Reinigung/Service/Transport/Security).
* **URL-Anzeige:** Die Quell-URL wird nun als dedizierter Link angezeigt und das Projekt automatisch basierend auf dem erkannten Produktnamen umbenannt.
* **[UPGRADE] v2.6: Rich Session Browser**
* **Neues UI:** Die textbasierte Liste für "Letzte Sitzungen" wurde durch eine dedizierte, kartenbasierte UI (`SessionBrowser.tsx`) ersetzt.
* **Angereicherte Daten:** Jede Sitzungskarte zeigt nun den Produktnamen, die Produktkategorie (mit Icon), eine Kurzbeschreibung und einen Thumbnail-Platzhalter an.
* **Backend-Anpassung:** Die Datenbankabfrage (`gtm_db_manager.py`) wurde erweitert, um diese Metadaten direkt aus der JSON-Spalte zu extrahieren und an das Frontend zu liefern.
* **Verbesserte UX:** Deutlich verbesserte Übersichtlichkeit und schnellere Identifikation von vergangenen Analysen.
* **[UPGRADE] v2.5: Hard Fact Extraction**
* **Phase 1 Erweiterung:** Implementierung eines sekundären Extraktions-Schritts für "Hard Facts" (Specs).
* **Strukturiertes Daten-Schema:** Integration von `templates/json_struktur_roboplanet.txt`.
* **Normalisierung:** Automatische Standardisierung von Einheiten (Minuten, cm, kg, m²/h).
* **Frontend Update:** Neue UI-Komponente zur Anzeige der technischen Daten (Core Data, Layer, Extended Features).
* **Sidebar & Header:** Update auf "ROBOPLANET v2.5".
* **[UPGRADE] v2.4:**
* Dokumentation der Kern-Engine (`helpers.py`) mit Dual SDK & Hybrid Image Generation.
* Aktualisierung der Architektur-Übersicht und Komponenten-Beschreibungen.
* Versionierung an den aktuellen Code-Stand (`v2.4.0`) angepasst.
* **[UPGRADE] v2.3:**
* Einführung der Session History (Datenbank-basiert).
* Implementierung von Markdown-Cleaning (Stripping von Code-Blocks).
* Prompt-Optimierung für tabellarische Markdown-Ausgaben in Phase 5.
* Markdown-File Import Feature.

View File

@@ -261,6 +261,45 @@ def override_website_url(company_id: int, url: str = Query(...), db: Session = D
db.commit()
return {"status": "updated", "website": url}
@app.post("/api/companies/{company_id}/override/impressum")
def override_impressum_url(company_id: int, url: str = Query(...), db: Session = Depends(get_db)):
"""
Manually sets the Impressum URL for a company and triggers re-extraction.
"""
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
raise HTTPException(404, "Company not found")
logger.info(f"Manual Override for {company.name}: Setting Impressum URL to {url}")
# 1. Scrape Impressum immediately
impressum_data = scraper._scrape_impressum_data(url)
if not impressum_data:
raise HTTPException(status_code=400, detail="Failed to extract data from provided URL")
# 2. Find existing scrape data or create new
existing_scrape = db.query(EnrichmentData).filter(
EnrichmentData.company_id == company.id,
EnrichmentData.source_type == "website_scrape"
).first()
if not existing_scrape:
# Create minimal scrape entry
db.add(EnrichmentData(
company_id=company.id,
source_type="website_scrape",
content={"impressum": impressum_data, "text": "", "title": "Manual Impressum", "url": url}
))
else:
# Update existing
content = dict(existing_scrape.content) if existing_scrape.content else {}
content["impressum"] = impressum_data
existing_scrape.content = content
existing_scrape.updated_at = datetime.utcnow()
db.commit()
return {"status": "updated", "data": impressum_data}
def run_discovery_task(company_id: int):
# New Session for Background Task
from .database import SessionLocal

View File

@@ -9,7 +9,7 @@ try:
class Settings(BaseSettings):
# App Info
APP_NAME: str = "Company Explorer"
VERSION: str = "0.3.0"
VERSION: str = "0.4.0"
DEBUG: bool = True
# Database (Store in App dir for simplicity)

View File

@@ -73,7 +73,7 @@ function App() {
</div>
<div>
<h1 className="text-xl font-bold text-white tracking-tight">Company Explorer</h1>
<p className="text-xs text-blue-400 font-medium">ROBOTICS EDITION <span className="text-slate-600 ml-2">v0.3.0 (Polling & Legal Data)</span></p>
<p className="text-xs text-blue-400 font-medium">ROBOTICS EDITION <span className="text-slate-600 ml-2">v0.4.0 (Overwrites & Export)</span></p>
</div>
</div>

View File

@@ -55,6 +55,8 @@ export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
const [wikiUrlInput, setWikiUrlInput] = useState("")
const [isEditingWebsite, setIsEditingWebsite] = useState(false)
const [websiteInput, setWebsiteInput] = useState("")
const [isEditingImpressum, setIsEditingImpressum] = useState(false)
const [impressumUrlInput, setImpressumUrlInput] = useState("")
const fetchData = (silent = false) => {
if (!companyId) return
@@ -84,6 +86,7 @@ export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
fetchData()
setIsEditingWiki(false)
setIsEditingWebsite(false)
setIsEditingImpressum(false)
setIsProcessing(false) // Reset on ID change
}, [companyId])
@@ -173,6 +176,21 @@ export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
}
}
const handleImpressumOverride = async () => {
if (!companyId) return
setIsProcessing(true)
try {
await axios.post(`${apiBase}/companies/${companyId}/override/impressum?url=${encodeURIComponent(impressumUrlInput)}`)
setIsEditingImpressum(false)
fetchData()
} catch (e) {
alert("Impressum update failed")
console.error(e)
} finally {
setIsProcessing(false)
}
}
if (!companyId) return null
const wikiEntry = data?.enrichment_data?.find(e => e.source_type === 'wikipedia')
@@ -304,43 +322,90 @@ export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
<div className="p-6 space-y-8">
{/* Impressum / Legal Data (NEW) */}
{impressum && (
<div className="bg-slate-950 rounded-lg p-4 border border-slate-800 flex flex-col gap-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div className="p-1 bg-slate-800 rounded text-slate-400">
<Briefcase className="h-3 w-3" />
</div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Official Legal Data</span>
<div className="bg-slate-950 rounded-lg p-4 border border-slate-800 flex flex-col gap-2">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div className="p-1 bg-slate-800 rounded text-slate-400">
<Briefcase className="h-3 w-3" />
</div>
<span className="text-[10px] uppercase font-bold text-slate-500 tracking-wider">Official Legal Data</span>
</div>
<div className="flex items-center gap-2">
{scrapeDate && (
<div className="text-[10px] text-slate-600 flex items-center gap-1">
<Clock className="h-3 w-3" /> {new Date(scrapeDate).toLocaleDateString()}
</div>
)}
{!isEditingImpressum ? (
<button
onClick={() => { setImpressumUrlInput(""); setIsEditingImpressum(true); }}
className="p-1 text-slate-600 hover:text-white transition-colors"
title="Set Impressum URL Manually"
>
<Pencil className="h-3 w-3" />
</button>
) : (
<div className="flex items-center gap-1 animate-in fade-in zoom-in duration-200">
<button
onClick={handleImpressumOverride}
className="p-1 bg-green-900/50 text-green-400 rounded hover:bg-green-900 transition-colors"
>
<Check className="h-3 w-3" />
</button>
<button
onClick={() => setIsEditingImpressum(false)}
className="p-1 text-slate-500 hover:text-red-400 transition-colors"
>
<X className="h-3 w-3" />
</button>
</div>
)}
</div>
<div className="text-sm font-medium text-white">
{impressum.legal_name || "Unknown Legal Name"}
</div>
<div className="flex items-start gap-2 text-xs text-slate-400">
<MapPin className="h-3 w-3 mt-0.5 shrink-0" />
<div>
<div>{impressum.street}</div>
<div>{impressum.zip} {impressum.city}</div>
</div>
</div>
{(impressum.email || impressum.phone) && (
<div className="mt-2 pt-2 border-t border-slate-900 flex flex-wrap gap-4 text-[10px] text-slate-500 font-mono">
{impressum.email && <span>{impressum.email}</span>}
{impressum.phone && <span>{impressum.phone}</span>}
{impressum.vat_id && <span className="text-blue-400/80">VAT: {impressum.vat_id}</span>}
</div>
)}
</div>
)}
{isEditingImpressum && (
<div className="mb-2 animate-in slide-in-from-top-1 duration-200">
<input
type="text"
value={impressumUrlInput}
onChange={e => setImpressumUrlInput(e.target.value)}
placeholder="https://.../impressum"
className="w-full bg-slate-900 border border-slate-700 rounded px-2 py-1 text-xs text-white focus:ring-1 focus:ring-blue-500 outline-none"
autoFocus
/>
</div>
)}
{impressum ? (
<>
<div className="text-sm font-medium text-white">
{impressum.legal_name || "Unknown Legal Name"}
</div>
<div className="flex items-start gap-2 text-xs text-slate-400">
<MapPin className="h-3 w-3 mt-0.5 shrink-0" />
<div>
<div>{impressum.street}</div>
<div>{impressum.zip} {impressum.city}</div>
</div>
</div>
{(impressum.email || impressum.phone) && (
<div className="mt-2 pt-2 border-t border-slate-900 flex flex-wrap gap-4 text-[10px] text-slate-500 font-mono">
{impressum.email && <span>{impressum.email}</span>}
{impressum.phone && <span>{impressum.phone}</span>}
{impressum.vat_id && <span className="text-blue-400/80">VAT: {impressum.vat_id}</span>}
</div>
)}
</>
) : !isEditingImpressum && (
<div className="text-[10px] text-slate-600 italic py-2">
No legal data found. Click pencil to provide direct Impressum link.
</div>
)}
</div>
{/* AI Analysis Dossier (NEW) */}
{aiAnalysis && (