Docs: Update MIGRATION_PLAN.md to v0.4.0 with new features (Company Explorer)
This commit is contained in:
@@ -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**.
|
**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.
|
**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).
|
2. **Setup:** Init `company-explorer` (Backend + Frontend Skeleton).
|
||||||
3. **Foundation:** DB-Schema + "List Matcher" (Deduplizierung ist Prio A für Operations).
|
3. **Foundation:** DB-Schema + "List Matcher" (Deduplizierung ist Prio A für Operations).
|
||||||
4. **Enrichment:** Implementierung des Scrapers + Signal Detector (Robotics).
|
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.
|
||||||
@@ -261,6 +261,45 @@ def override_website_url(company_id: int, url: str = Query(...), db: Session = D
|
|||||||
db.commit()
|
db.commit()
|
||||||
return {"status": "updated", "website": url}
|
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):
|
def run_discovery_task(company_id: int):
|
||||||
# New Session for Background Task
|
# New Session for Background Task
|
||||||
from .database import SessionLocal
|
from .database import SessionLocal
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ try:
|
|||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
# App Info
|
# App Info
|
||||||
APP_NAME: str = "Company Explorer"
|
APP_NAME: str = "Company Explorer"
|
||||||
VERSION: str = "0.3.0"
|
VERSION: str = "0.4.0"
|
||||||
DEBUG: bool = True
|
DEBUG: bool = True
|
||||||
|
|
||||||
# Database (Store in App dir for simplicity)
|
# Database (Store in App dir for simplicity)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-white tracking-tight">Company Explorer</h1>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
|
|||||||
const [wikiUrlInput, setWikiUrlInput] = useState("")
|
const [wikiUrlInput, setWikiUrlInput] = useState("")
|
||||||
const [isEditingWebsite, setIsEditingWebsite] = useState(false)
|
const [isEditingWebsite, setIsEditingWebsite] = useState(false)
|
||||||
const [websiteInput, setWebsiteInput] = useState("")
|
const [websiteInput, setWebsiteInput] = useState("")
|
||||||
|
const [isEditingImpressum, setIsEditingImpressum] = useState(false)
|
||||||
|
const [impressumUrlInput, setImpressumUrlInput] = useState("")
|
||||||
|
|
||||||
const fetchData = (silent = false) => {
|
const fetchData = (silent = false) => {
|
||||||
if (!companyId) return
|
if (!companyId) return
|
||||||
@@ -84,6 +86,7 @@ export function Inspector({ companyId, onClose, apiBase }: InspectorProps) {
|
|||||||
fetchData()
|
fetchData()
|
||||||
setIsEditingWiki(false)
|
setIsEditingWiki(false)
|
||||||
setIsEditingWebsite(false)
|
setIsEditingWebsite(false)
|
||||||
|
setIsEditingImpressum(false)
|
||||||
setIsProcessing(false) // Reset on ID change
|
setIsProcessing(false) // Reset on ID change
|
||||||
}, [companyId])
|
}, [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
|
if (!companyId) return null
|
||||||
|
|
||||||
const wikiEntry = data?.enrichment_data?.find(e => e.source_type === 'wikipedia')
|
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">
|
<div className="p-6 space-y-8">
|
||||||
|
|
||||||
{/* Impressum / Legal Data (NEW) */}
|
{/* 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="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 justify-between mb-1">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="p-1 bg-slate-800 rounded text-slate-400">
|
||||||
<div className="p-1 bg-slate-800 rounded text-slate-400">
|
<Briefcase className="h-3 w-3" />
|
||||||
<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>
|
||||||
|
<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 && (
|
{scrapeDate && (
|
||||||
<div className="text-[10px] text-slate-600 flex items-center gap-1">
|
<div className="text-[10px] text-slate-600 flex items-center gap-1">
|
||||||
<Clock className="h-3 w-3" /> {new Date(scrapeDate).toLocaleDateString()}
|
<Clock className="h-3 w-3" /> {new Date(scrapeDate).toLocaleDateString()}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<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>
|
</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) */}
|
{/* AI Analysis Dossier (NEW) */}
|
||||||
{aiAnalysis && (
|
{aiAnalysis && (
|
||||||
|
|||||||
Reference in New Issue
Block a user