vflow2 Documentation

IFC Analysis Tools ← Back to App

API Endpoints

Overview

All endpoints are served by Flask (app.py). The web UI and 3D viewer communicate with the backend exclusively through these JSON APIs.

Cache-or-enqueue contract (heavy GETs)

Every endpoint below that touches IFC geometry (/spaces, /connection_graph, /wall_zones, /roombook, /heizlast) follows a single contract:

A single dispatcher drains queued jobs in FIFO order (one at a time), so a cold open never blocks the Flask request thread and never freezes the browser.

The viewer's fetchOrJob(url) helper in templates/viewer.html encapsulates this flow; every frontend consumer uses it.

Endpoints

GET /

Main analysis page. Lists uploaded files and available tools.

POST /upload

Upload an IFC file. - Body: multipart/form-data with field file - Returns: {"filename": "test-1.ifc"}

DELETE /delete/<filename>

Remove an uploaded file. - Returns: {"ok": true}

GET /uploads/<filename>

Serve raw IFC file (used by web-ifc in the browser).

POST /analyze

Run an analysis tool. - Body: {"filename": "test-1.ifc", "tool": "element_summary"} - Returns: tool result dict ({title, summary, columns, rows, ...})

GET /spaces/<filename>

Space bounding boxes + IFC world bbox for coordinate mapping. - Follows the cache-or-enqueue contract (200 cache hit, 202 + job otherwise). - Returns on 200:

{
  "ifcWorldMin": [0.0, 0.0, -0.18],
  "ifcWorldMax": [6.45, 10.25, 0.0],
  "spaces": [
    {"expressID": 46, "name": "1", "longName": "Raum 1", "globalId": "...",
     "bbox": {"min": [0.3, 5.3, 0.0], "max": [6.3, 10.1, 2.438]}}
  ]
}

GET /connection_graph/<filename>

Connection point graph for all building elements. - Follows the cache-or-enqueue contract. Query params: force=1 (optional). - Returns on 200: {nodes, edges, stats} (see 02_connection_graph.md) - Cached: saves to <stem>.connection_graph.json.

GET /wall_zones/<filename>?mode=normal

DIN 18015-3 installation zones for wall and ceiling surfaces. - Follows the cache-or-enqueue contract. Query params: mode=normal|false_ceiling|false_floor, force=1. - Returns on 200: {walls, ceilings, mode, stats} (see 04_wall_zones.md) - ceilings: per-IfcSpace ceiling zones (ZD-r, ZD-t, dead zones, or false ceiling fly zone) - Cached: saves to <stem>.wall_zones_<mode>.json per mode; each mode is a distinct job in the queue.

GET /storeys/<filename>

Building storeys with contained element express IDs. - Query params: project=<name> (optional) - Returns:

{
  "storeys": [
    {"expressID": 25, "name": "EG", "longName": "Erdgeschoss",
     "elevation": 0.0,
     "elementIDs": [260, 395, 453, 83, 180]}
  ]
}

POST /jobs

Manually start a background job (used by the MEP Routing button and the Connection Graph / Wall Zones recalculate buttons). Most consumers never call this directly — the heavy GETs above enqueue jobs themselves via the cache-or- enqueue contract. - Body: {"operation": "connection_graph|wall_zones|mep_routing|spaces|roombook|heizlast", "filename": "...", "project": "...", "mode": "..."} - Returns: {"job_id": "uuid"} - Deduplication: if a matching job (same operation + params) is already queued or running, returns the existing job_id instead of enqueueing a duplicate.

GET /jobs

List all jobs. Optional filters: ?filename=&project=. Order: running, then queued (FIFO by queue_position), then finished (newest first).

GET /jobs/<id>

Poll job status. - Returns: {"status": "queued|running|completed|failed", "current_phase": "...", "phases_completed": ["..."], "total_phases": 6, "elapsed_s": 12.5, "runtime_s": 4.8, "queue_position": 2, "error": null} - queue_position is null once the job leaves the queue; runtime_s counts only actual execution time, while elapsed_s includes queue wait.

GET /jobs/<id>/result

Fetch completed job result data. For roombook and heizlast the response is sourced from job.result so transient fields like dropped_override_ids are preserved; for other operations it reads from the on-disk cache.

GET /has_cache/<cache_type>/<filename>

Check if cached computation results exist. - cache_type: connection_graph, wall_zones, spaces, or roombook - Returns: {"exists": true|false} - Used by viewer on page load to show/hide action buttons

GET /project-info/<project>

Project metadata, file sizes, and cache flags in a single response. Used by the Detail view to populate file sizes + cache dots without serialising a HEAD per file and a /has_cache/ call per cache key (which otherwise queue behind the large IFC download on the browser's connection pool). - Returns:

{
  "name": "jella-haase",
  "files": [{"filename": "Sella-Hasse-Str3.ifc", "role": "architectural", "size": 48234567}],
  "caches": {"spaces": true, "connection_graph": true, "wall_zones": true, "roombook": false}
}

GET /roombook/<filename>?project=<name>&force=1

Roombook for an IFC: IFC-extracted fields keyed by GlobalId plus persisted manual overrides, plus the field specs that drive the frontend renderer. - Follows the cache-or-enqueue contract. - Returns on 200:

{
  "schema_version": 2,
  "ifc_fingerprint": "1776577853_1257250",
  "rooms_ifc": {"<gid>": {"name": "1", "long_name": "Raum 1", "purpose": "",
                          "storey": "OG1", "volume": 80.64}},
  "overrides":  {"<gid>": {"usage_type": "Wohnraum", "design_temp": 20.0}},
  "field_specs": [
    {"key": "name", "type": "text", "label": "Name"},
    {"key": "usage_type", "type": "enum", "label": "Nutzung",
     "options": ["Wohnraum", "Schlafraum", "Küche", "Bad", "WC", "Flur",
                 "Treppenhaus", "Büro", "Abstellraum", "Unbeheizt"]},
    {"key": "design_temp", "type": "number", "label": "θ_int (°C)",
     "step": 0.5, "derive_from_usage_type": true},
    // ...
  ],
  "dropped_override_ids": []
}

POST /roombook/<filename>/override?project=<name>

Persist a manual override. - Body: {"global_id": "...", "field": "<any key from field_specs>", "value": "..."} - Number fields coerced via float(); enum fields validated against options; text fields stored as-is. Empty string or null clears the field; empty entries are dropped. 400 on unknown field or invalid enum value. - Returns the updated roombook state.

POST /roombook/<filename>/recalculate?project=<name>

Force re-extraction from IFC. Always returns 202 + {job_id}; the frontend waits for the job to complete and then fetches /jobs/<id>/result to receive the bundle (including the transient dropped_override_ids list). Overrides matching an existing GlobalId are kept; orphans are pruned and surfaced via dropped_override_ids. Override keys that no longer appear in ROOM_FIELDS are also pruned.

GET /heizlast_presets/<filename>?project=<name>

Per-model Heizlast project parameters. Auto-creates <stem>.heizlast_presets.json with DIN defaults on first call. - Returns:

{
  "schema_version": 1,
  "theta_e": -12.0,
  "n_50": 3.0,
  "delta_u_tb": 0.10,
  "shielding": "normal",
  "reheat_enabled": false,
  "reheat_factor_wm2": 11.0,
  "u_defaults": {
    "wall_external": 0.28, "wall_internal": 1.50, "wall_to_unheated": 1.00,
    "window": 1.30, "door_external": 1.80, "door_internal": 3.00,
    "floor_ground": 0.35, "floor_internal": 1.80, "roof": 0.20,
    "ceiling_to_unheated": 0.50
  }
}

POST /heizlast_presets/<filename>?project=<name>

Partial update. Only the keys you pass are written; others remain unchanged. - Body (any subset): {"theta_e": -14.0, "u_defaults": {"wall_external": 0.24}} - Returns the full updated presets.

GET /heizlast/<filename>?project=<name>&force=1

Full DIN EN 12831 Heizlast bundle: extracted envelope + inputs snapshot + per-room results + building total. - Follows the cache-or-enqueue contract. Fast-path returns 200 only when both the envelope and the roombook caches are fresh; otherwise enqueues. - Returns on 200:

{
  "schema_version": 1,
  "ifc_fingerprint": "...",
  "roombook_fingerprint": "...",
  "presets_fingerprint": "...",
  "surfaces_by_room": {
    "<gid>": [
      {"surface_id": "<wall_gid>__face_max", "surface_type": "wall",
       "area_effective": 29.12, "u_value_effective": 3.49, "u_source": "pset",
       "adjacency_effective": "external", "fk_effective": 1.0, "phi_w": 3249.0,
       "source_element_type": "IfcWall", "source_name": "...", ...}
    ]
  },
  "results_by_room": {"<gid>": {"heated": true, "theta_int": 20.0, "volume_m3": 70.2,
                                "phi_T_w": 15117, "phi_V_w": 439, "phi_RH_w": 0,
                                "phi_HL_w": 15556}},
  "building_total": {"phi_HL_w": 24426, "phi_HL_kw": 24.43,
                     "heated_volume_m3": 161.28, "heated_area_m2": 57.6,
                     "heated_room_count": 2},
  "room_inputs": {"<gid>": {"usage_type": "Wohnraum", "theta_int": 20,
                            "ach_min": 0.5, "volume": 70.2, "shielding": "normal",
                            "heated": true}},
  "presets_snapshot": { ... same shape as /heizlast_presets ... },
  "rooms_ifc": { ... same as /roombook ... },
  "surface_overrides": {},
  "warnings": [],
  "dropped_override_ids": []
}

POST /heizlast/<filename>/surface-override?project=<name>

Persist a per-surface override. - Body: {"surface_id": "<wall_gid>__face_max", "field": "u_value", "value": 0.24} - Valid fields: area_m2, u_value, fk, exposed_perimeter_m (numbers); adjacency (one of external, roof, ground, to_heated, to_unheated). - exposed_perimeter_m only affects ground-adjacency floor surfaces (it feeds the EN ISO 13370 U_bf computation). Setting it on other surfaces is stored but ignored by the compute. - Empty string/null clears the field; empty entries are dropped. - Returns the recomputed full Heizlast bundle (same shape as GET).

POST /heizlast/<filename>/surface-writeback?project=<name>

Explicit per-field writeback-to-IFC flag for one surface. Drives the → IFC column in the Heizlast UI and the export-review flow. - Body: {"surface_id": "<sid>", "field": "u_value", "enabled": true} - Valid fields: u_value, area_m2, adjacency, fk. (exposed_perimeter_m is deliberately excluded — it's a derived geometric input, not a quantity architects read back through standard IFC Psets.) - enabled: null clears the explicit flag and reverts to the default rule: auto-tick iff an override exists for that field, plus auto-tick u_value when u_source == "iso13370". Explicit true/false always wins. - Persisted under surface_overrides[sid].write_to_ifc[field]. - Returns the recomputed full Heizlast bundle (same shape as GET).

POST /heizlast/<filename>/recalculate?project=<name>

Force re-extraction of the geometric envelope. Always returns 202 + {job_id}; same semantics as GET /heizlast/<filename>?force=1.

GET /heizlast/<filename>/export.csv?project=<name>

Flat CSV dump of the current Heizlast bundle. Two sections separated by a blank line: Räume (one row per room: Geschoss, Name, Nutzung, θ_int, V, Φ_T/V/RH/HL, GlobalId …) and Flächen (one row per surface: Raum, Typ, Quell-Element, Fläche, U, U-Quelle, Adjazenz, Nachbar, f_k, Φ, surface_id). - Follows the cache-or-enqueue contract — returns 200 with the CSV body on a fresh cache, 202 + {pending, job_id, operation} when the cache is stale. - Content-Disposition carries the filename {project}_{stem}_heizlast_{YYYYMMDD}.csv.

GET /heizlast/<filename>/export.xlsx?project=<name>

Three-sheet XLSX workbook (requires openpyxl): - Projekt — label/value rows for presets + building total (θ_e, n₅₀, ΔU_TB, Φ_HL kW/W, beheiztes Volumen, Nutzfläche, Anzahl Räume). - Räume — one row per room with the same columns as the CSV Räume section. - Flächen — one row per surface; first row frozen for scroll-through. - Same cache-or-enqueue contract as CSV. Content-Disposition filename ends .xlsx.

GET /heizlast/<filename>/print?project=<name>

Print-optimized HTML page (landscape A4, @media print inverts to white-on-white). Users open it in a new tab and hit the browser's print dialog to save as PDF. - Sections: title header, Gebäude-Gesamtergebnis (4-stat grid), Eingabedaten (presets), then one Geschoss section per storey with a room-block per room containing room-level stats + a surface-detail table. Every storey begins on a new page (page-break-before: always). - Same cache-or-enqueue contract. Returns HTML (not JSON).

Heizflächenauslegung (Sizing)

See Heizflächenauslegung for the feature overview. Every sizing response and export carries a Richtwerte disclaimer — these are not verbindliche Planungswerte.

GET /sizing/<filename>?project=<name>&force=1

Full sizing bundle. Synchronous (pure arithmetic; no job queue). - Returns 200 + a dict keyed by IfcSpace.GlobalId: json { "schema_version": 1, "ifc_fingerprint": "...", "heizlast_fingerprint": "...", "presets_fingerprint": "...", "disclaimer": "Richtwerte nach vereinfachter EN 442 / EN 1264 …", "presets_sizing": { "theta_v_c": 55.0, "theta_r_c": 45.0, ... }, "catalog": { "types": ["Type 11", "Type 22", …], "grid": {"heights_mm": [...], "lengths_mm": [...]}, "source_label": "Richtwerte — keine Herstellerdaten" }, "rooms": { "<GlobalId>": { "heated": true, "room_context": {"name": "…", "storey": "…", "theta_int_c": 20.0, "phi_HL_w": 2140.0, "floor_area_m2": 23.8, …}, "candidates": [ { "id": "<uuid>", "label": "Heizkörper", "type": "radiator", "is_active": true, "is_shipped": true, "params": {"catalog_key":"Type 22","height_mm":600,"length_mm":1000, "exponent_override":null,"theta_v_override_c":null,…}, "result": { "phi_delivered_w": 2280.0, "coverage_pct": 106.5, "covers_load": true, "warnings": [], "details": {"delta_T_ln": 49.83, "f_corr": 1.0, "k_m_w": 2280, "n": 1.3, …}, "disclaimer": "Richtwerte …" } } ], "active_candidate_id": "<uuid|null>" } }, "dropped_candidate_ids": [] } - First-visit heated rooms get one auto-synthesized default candidate using the preset preferred_type. Unheated rooms get an empty candidate list. - Once a user has touched a room (any CRUD), the entry is preserved even when empty — the server never re-synthesizes defaults over explicit deletes.

GET /sizing/<filename>/catalog?project=<name>

Radiator catalog (types + dimension grid + Richtwerte label).

{ "types": [{"key":"Type 22","n":1.3,"K_m_per_m2_w":1960,"depth_mm":100,…},…],
  "grid": {"heights_mm": [300,400,…,900], "lengths_mm": [400,…,3000]},
  "source_label": "Richtwerte — keine Herstellerdaten. …" }

POST /sizing/<filename>/candidate?project=<name>

Create a candidate. - Body: {"global_id": "<gid>", "type": "radiator" | "ufh" | "wall" | "ceiling" | "towel_rail", "label"?: "…", "params"?: {…}} - Returns 201 + full bundle. Stub types (wall/ceiling/towel_rail) are stored but their results carry "Noch nicht implementiert" warnings and is_shipped: false — the UI can render them disabled. - 400 on unknown type, 404 on unknown global_id.

PATCH /sizing/<filename>/candidate/<cid>?project=<name>

Update one field of a candidate. - Body: {"global_id": "<gid>", "field": "params.<name>" | "label" | "type", "value": <coerced>}. Empty value clears. - Returns 200 + full bundle; 400 on unknown field/type/value, 404 on unknown gid/cid.

DELETE /sizing/<filename>/candidate/<cid>?project=<name>

POST /sizing/<filename>/active?project=<name>

Set or clear the active candidate. - Body: {"global_id": "<gid>", "candidate_id": "<uuid>" | null} - Returns 200 + full bundle.

Sizing in the presets payload

The global system temperatures, UFH caps, and radiator defaults live inside the existing /heizlast_presets endpoint under a nested sizing key (schema v2):

{ "schema_version": 2,
  "theta_e": -12.0, ...,
  "sizing": {
    "theta_v_c": 55.0, "theta_r_c": 45.0,
    "preferred_type": "ufh",
    "radiator_default_type": "Type 22", "radiator_default_exponent": 1.3,
    "ufh_cap_living_w_m2": 100.0, "ufh_cap_bath_w_m2": 175.0,
    "ufh_active_area_fraction": 0.9 } }

POST accepts partial nested patches: {"sizing": {"theta_v_c": 40}}. Unknown keys inside sizing are rejected (strict schema — prevents silent drift). v1 files on disk are migrated on first read.

Sizing in the heizlast exports

Default-enabled in CSV/XLSX/Print. Append ?include_sizing=0 to suppress the Auslegung section in CSV/XLSX (the print template renders it unconditionally).

GET /has_cache/sizing/<filename>?project=<name>

Extension of the existing has_cache endpoint — {"exists": true|false}.

GET /climate/de

German-municipality lookup for the Presets "Standort" card. Small static dataset (~60 cities) with Norm-Außentemperatur (θ_e) per DWD TRY-region midpoint. - Returns:

{
  "entries": [
    {"plz_prefix": "80", "name": "München (Mitte)", "theta_e": -14.0, "try_region": "TRY 13"},
    {"plz_prefix": "20", "name": "Hamburg", "theta_e": -10.0, "try_region": "TRY 3"},
    …
  ],
  "count": 60,
  "note": "…caveats about precision and source…"
}

Caching Strategy

Computed results are saved as JSON files alongside the IFC in uploads/:

Endpoint Cache File Regeneration
/connection_graph/ <stem>.connection_graph.json Auto on first request, or via tool
/wall_zones/?mode=X <stem>.wall_zones_X.json Auto on first request
/spaces/ <stem>.spaces.json Auto, invalidated when IFC mtime+size changes
/roombook/ <stem>.roombook.json Auto, invalidated when IFC mtime+size changes; force=1 to rebuild and prune orphan overrides
/heizlast_presets/ <stem>.heizlast_presets.json Auto-created on first GET with DIN defaults; POSTs persist partial updates
/heizlast/ <stem>.heizlast.json Envelope re-extracted when IFC fingerprint changes or force=1; arithmetic runs fresh every GET
/sizing/ <stem>.sizing.json Candidates persist across Heizlast changes; orphan GIDs are pruned on next write and surfaced in dropped_candidate_ids

To force recomputation: delete the cached JSON file and reload the viewer.