Skip to content

Commit b9de9b2

Browse files
jochem25claude
andcommitted
feat: save project.wefc manifest with IFC, validation and BCF links
Cloud save now builds and uploads a complete project.wefc manifest containing WefcModel (IFC files), WefcValidation (results), and WefcBcf (issues .bcf zip) entries. Adds PUT manifest API endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d86d7a5 commit b9de9b2

4 files changed

Lines changed: 137 additions & 0 deletions

File tree

server/routers/cloud.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,37 @@ async def cloud_get_manifest(
437437
)
438438

439439
return JSONResponse(content=manifest)
440+
441+
442+
@router.put("/projects/{project}/manifest")
443+
async def cloud_put_manifest(
444+
project: str,
445+
manifest: dict[str, Any],
446+
tenant: str | None = Query(None),
447+
):
448+
"""Write a full project.wefc manifest.
449+
450+
Accepts a complete manifest JSON body and writes it to the project
451+
root as project.wefc via WebDAV.
452+
453+
Args:
454+
project: Project folder name.
455+
manifest: Full manifest dict with header and data.
456+
tenant: Tenant slug (optional).
457+
"""
458+
config = _resolve_tenant(tenant)
459+
client = get_nc_client(config)
460+
461+
# Ensure header timestamp is current
462+
if "header" in manifest:
463+
manifest["header"]["timestamp"] = (
464+
datetime.now(timezone.utc).isoformat()
465+
)
466+
manifest["header"]["application"] = "bim-validator"
467+
468+
try:
469+
await client.write_manifest(project, manifest)
470+
except Exception as exc:
471+
raise _nc_error_to_http(exc) from exc
472+
473+
return {"success": True, "project": project}

viewer/src/api/cloudApi.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,35 @@ export async function cloudUploadFile(
166166
}
167167
}
168168

169+
/** Save a project.wefc manifest to the cloud. */
170+
export async function cloudSaveManifest(
171+
project: string,
172+
manifest: Record<string, unknown>,
173+
): Promise<void> {
174+
let response: Response;
175+
try {
176+
response = await fetch(
177+
`${CLOUD_BASE}/projects/${encodeURIComponent(project)}/manifest`,
178+
{
179+
method: "PUT",
180+
headers: { "Content-Type": "application/json" },
181+
body: JSON.stringify(manifest),
182+
},
183+
);
184+
} catch (error) {
185+
throw new ApiError(
186+
"Network error: unable to save manifest.",
187+
undefined,
188+
error instanceof Error ? error.message : "Unknown network error",
189+
);
190+
}
191+
192+
if (!response.ok) {
193+
const msg = await parseError(response);
194+
throw new ApiError(msg, response.status, msg);
195+
}
196+
}
197+
169198
/** Delete a file from the cloud. */
170199
export async function cloudDeleteFile(
171200
project: string,

viewer/src/components/projects/SaveAsDialog.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export default function SaveAsDialog({
3838
const cloudError = useStore((s) => s.cloudError);
3939
const cloudLoadProjects = useStore((s) => s.cloudLoadProjects);
4040
const cloudUpload = useStore((s) => s.cloudUpload);
41+
const cloudSaveManifest = useStore((s) => s.cloudSaveManifest);
4142
const validationResult = useStore((s) => s.validationResult);
43+
const bcfIssues = useStore((s) => s.bcfIssues);
4244
const setSaveInfo = useStore((s) => s.setSaveInfo);
4345
const markClean = useStore((s) => s.markClean);
4446

@@ -148,6 +150,9 @@ export default function SaveAsDialog({
148150
setError(null);
149151

150152
try {
153+
const now = new Date().toISOString();
154+
const manifestData: Record<string, unknown>[] = [];
155+
151156
// Upload IFC files to models/ directory
152157
const { getModelBytes } = await import("../../engine/modelCache");
153158
for (const model of project.models) {
@@ -157,6 +162,17 @@ export default function SaveAsDialog({
157162
type: "application/octet-stream",
158163
});
159164
await cloudUpload(selectedCloudProject, model.fileName, blob, "bim");
165+
manifestData.push({
166+
type: "WefcModel",
167+
guid: model.id,
168+
name: model.fileName,
169+
path: `models/${model.fileName}`,
170+
format: model.format ?? "ifc",
171+
fileSize: model.fileSize,
172+
status: "active",
173+
created: now,
174+
modified: now,
175+
});
160176
}
161177
}
162178

@@ -172,8 +188,51 @@ export default function SaveAsDialog({
172188
resultBlob,
173189
"output",
174190
);
191+
manifestData.push({
192+
type: "WefcValidation",
193+
guid: crypto.randomUUID(),
194+
name: `BIM Validatie - ${now.slice(0, 10)}`,
195+
path: "validation/validation-result.json",
196+
status: "active",
197+
created: now,
198+
modified: now,
199+
});
200+
}
201+
202+
// Upload BCF issues as .bcf zip to validation/ directory
203+
if (bcfIssues.length > 0) {
204+
const { generateBcfZip } = await import("../../lib/bcfZipGenerator");
205+
const bcfBlob = await generateBcfZip(bcfIssues);
206+
const bcfFilename = `${project.name ?? "issues"}.bcf`;
207+
await cloudUpload(
208+
selectedCloudProject,
209+
bcfFilename,
210+
bcfBlob,
211+
"output",
212+
);
213+
manifestData.push({
214+
type: "WefcBcf",
215+
guid: crypto.randomUUID(),
216+
name: `BCF Issues - ${now.slice(0, 10)}`,
217+
path: `validation/${bcfFilename}`,
218+
issueCount: bcfIssues.length,
219+
status: "active",
220+
created: now,
221+
modified: now,
222+
});
175223
}
176224

225+
// Save project.wefc manifest
226+
await cloudSaveManifest(selectedCloudProject, {
227+
header: {
228+
schema: "WeFC",
229+
schema_version: "1.0.0",
230+
timestamp: now,
231+
application: "bim-validator",
232+
},
233+
data: manifestData,
234+
});
235+
177236
setSaveInfo({
178237
source: "cloud",
179238
cloudProject: selectedCloudProject,
@@ -191,7 +250,9 @@ export default function SaveAsDialog({
191250
project,
192251
selectedCloudProject,
193252
validationResult,
253+
bcfIssues,
194254
cloudUpload,
255+
cloudSaveManifest,
195256
setSaveInfo,
196257
markClean,
197258
onSaveComplete,

viewer/src/store/slices/cloudSlice.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
cloudUploadFile as apiUploadFile,
1515
cloudDownloadFile as apiDownloadFile,
1616
cloudDeleteFile as apiDeleteFile,
17+
cloudSaveManifest as apiSaveManifest,
1718
} from "../../api/cloudApi";
1819

1920
type CloudPhase =
@@ -43,6 +44,7 @@ export interface CloudSlice {
4344
cloudUpload: (project: string, filename: string, blob: Blob, category?: "bim" | "output") => Promise<boolean>;
4445
cloudDownload: (project: string, filename: string) => Promise<Blob | null>;
4546
cloudDelete: (project: string, filename: string) => Promise<boolean>;
47+
cloudSaveManifest: (project: string, manifest: Record<string, unknown>) => Promise<boolean>;
4648
cloudReset: () => void;
4749
}
4850

@@ -143,6 +145,17 @@ export const createCloudSlice: StateCreator<CloudSlice> = (set, get) => ({
143145
}
144146
},
145147

148+
cloudSaveManifest: async (project: string, manifest: Record<string, unknown>) => {
149+
try {
150+
await apiSaveManifest(project, manifest);
151+
return true;
152+
} catch (err) {
153+
const message = err instanceof Error ? err.message : "Manifest save failed";
154+
set({ cloudError: message });
155+
return false;
156+
}
157+
},
158+
146159
cloudReset: () => {
147160
set({
148161
cloudPhase: "idle",

0 commit comments

Comments
 (0)