Compare commits

...

3 Commits

Author SHA1 Message Date
DCCONSTRUCTIONS df4767a085 Sync BIM attachment versions from viewer storage 2026-06-23 11:32:49 +03:00
DCCONSTRUCTIONS 4ed460ae45 Build Tasker BIM iframe against production domain 2026-06-22 13:09:23 +03:00
DCCONSTRUCTIONS 477626c597 Send BIM viewer requests with credentials 2026-06-18 21:43:04 +03:00
7 changed files with 122 additions and 10 deletions

View File

@ -110,7 +110,11 @@ pnpm turbo run check:types --filter=web --filter='@plane/i18n' --filter='@plane/
```bash ```bash
cd /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src cd /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER/plane-src
docker build -t nodedc/plane-frontend:ru -f apps/web/Dockerfile.web . docker build \
--build-arg VITE_BEAM_VIEWER_BASE_URL=http://localhost:8080 \
--build-arg VITE_BEAM_API_BASE_URL=http://localhost:8080 \
-t nodedc/plane-frontend:ru \
-f apps/web/Dockerfile.web .
cd /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER cd /Users/dcconstructions/Downloads/mnt/data/dc_taskmanager/NODEDC_TASKMANAGER
./setup.sh stop ./setup.sh stop
./setup.sh start ./setup.sh start

View File

@ -21,8 +21,11 @@ The same codebase supports local and production-like runs. The deployment mode i
Frontend brand and launcher integration: Frontend brand and launcher integration:
- `VITE_NODEDC_LAUNCHER_URL` points the web/admin bundles to Launcher. - `VITE_NODEDC_LAUNCHER_URL` points the web/admin bundles to Launcher.
- `VITE_BEAM_VIEWER_BASE_URL` and `VITE_BEAM_API_BASE_URL` point the web bundle to BIM Viewer for issue attachments/iframe.
- Production default for NAS builds: `https://hub.nodedc.ru` - Production default for NAS builds: `https://hub.nodedc.ru`
- Production BIM default for NAS builds: `https://bim.nodedc.tech`
- Local default in source helpers: `http://launcher.local.nodedc` - Local default in source helpers: `http://launcher.local.nodedc`
- Local BIM default in source helpers: `http://localhost:8080`
Backend integration: Backend integration:
@ -63,6 +66,8 @@ Local/staging-style rebuild can override integration endpoints:
```sh ```sh
VITE_NODEDC_LAUNCHER_URL=http://launcher.local.nodedc \ VITE_NODEDC_LAUNCHER_URL=http://launcher.local.nodedc \
VITE_BEAM_VIEWER_BASE_URL=http://localhost:8080 \
VITE_BEAM_API_BASE_URL=http://localhost:8080 \
SMOKE_BASE_URL=http://task.local.nodedc:18080 \ SMOKE_BASE_URL=http://task.local.nodedc:18080 \
SMOKE_RESOLVE= \ SMOKE_RESOLVE= \
BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 \ BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 \

View File

@ -28,10 +28,12 @@ import {
buildBeamViewerUrl, buildBeamViewerUrl,
dispatchBeamViewerOpenEvent, dispatchBeamViewerOpenEvent,
fetchBeamConversionStatus, fetchBeamConversionStatus,
fetchBeamModelVersions,
getBeamModelVersionRecords, getBeamModelVersionRecords,
getBeamVersionViewerUrl, getBeamVersionViewerUrl,
getBeamViewerAttachment, getBeamViewerAttachment,
isBeamModelFile, isBeamModelFile,
mergeBeamModelVersionRecordLists,
syncCurrentBeamVersionRecord, syncCurrentBeamVersionRecord,
type TBeamModelVersionRecord, type TBeamModelVersionRecord,
type TBeamViewerAttachment, type TBeamViewerAttachment,
@ -85,7 +87,8 @@ const withBeamViewerSettingsSrc = (
url: string | null | undefined, url: string | null | undefined,
settingsSrc: string | undefined settingsSrc: string | undefined
): string | undefined => { ): string | undefined => {
if (!url || !settingsSrc) return url; if (!url) return undefined;
if (!settingsSrc) return url;
try { try {
const parsedUrl = new URL(url, "http://localhost"); const parsedUrl = new URL(url, "http://localhost");
if (!parsedUrl.searchParams.has("settingsSrc")) parsedUrl.searchParams.set("settingsSrc", settingsSrc); if (!parsedUrl.searchParams.has("settingsSrc")) parsedUrl.searchParams.set("settingsSrc", settingsSrc);
@ -241,9 +244,14 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
const [deletingVersionKey, setDeletingVersionKey] = useState<string | null>(null); const [deletingVersionKey, setDeletingVersionKey] = useState<string | null>(null);
const [isVersionUploading, setIsVersionUploading] = useState(false); const [isVersionUploading, setIsVersionUploading] = useState(false);
const [isThumbnailError, setIsThumbnailError] = useState(false); const [isThumbnailError, setIsThumbnailError] = useState(false);
const [liveBeamVersions, setLiveBeamVersions] = useState<TBeamModelVersionRecord[]>([]);
const versionUploadInputRef = useRef<HTMLInputElement>(null); const versionUploadInputRef = useRef<HTMLInputElement>(null);
const beamVersions = useMemo(() => getBeamModelVersionRecords(beamViewer, attachment), [attachment, beamViewer]); const localBeamVersions = useMemo(() => getBeamModelVersionRecords(beamViewer, attachment), [attachment, beamViewer]);
const rawVersion = beamViewer?.version ?? attachment?.attributes.version; const beamVersions = useMemo(
() => mergeBeamModelVersionRecordLists(localBeamVersions, liveBeamVersions),
[localBeamVersions, liveBeamVersions]
);
const rawVersion = beamViewer?.version ?? (attachment?.attributes as { version?: number | string } | undefined)?.version;
const versionLabel = const versionLabel =
typeof rawVersion === "number" typeof rawVersion === "number"
? `v${rawVersion}` ? `v${rawVersion}`
@ -414,6 +422,29 @@ export const IssueAttachmentsListItem = observer(function IssueAttachmentsListIt
/> />
) : null; ) : null;
useEffect(() => {
if (!beamViewer?.src) {
setLiveBeamVersions([]);
return;
}
let isMounted = true;
fetchBeamModelVersions(beamViewer)
.then((history) => {
if (!isMounted) return;
setLiveBeamVersions(Array.isArray(history.versions) ? history.versions : []);
})
.catch((error) => {
if (!isMounted) return;
console.warn("BIM version history unavailable:", error);
setLiveBeamVersions([]);
});
return () => {
isMounted = false;
};
}, [beamViewer?.assetId, beamViewer?.projectId, beamViewer?.src, beamViewer?.versionId]);
useEffect(() => { useEffect(() => {
if (!beamViewer || !beamViewer.src) return; if (!beamViewer || !beamViewer.src) return;
if (beamViewer.conversion?.status === "ready" && storedModelViewerUrl) return; if (beamViewer.conversion?.status === "ready" && storedModelViewerUrl) return;
@ -973,8 +1004,8 @@ const BeamVersionHistoryModal = (props: TBeamVersionHistoryModal) => {
<div>Статус</div> <div>Статус</div>
<div className="text-right">Действия</div> <div className="text-right">Действия</div>
</div> </div>
{versions {[...versions]
.toSorted((first, second) => second.version - first.version) .sort((first, second) => second.version - first.version)
.map((version) => { .map((version) => {
const viewerUrl = getBeamVersionViewerUrl(version); const viewerUrl = getBeamVersionViewerUrl(version);
const isCurrent = currentVersionId const isCurrent = currentVersionId

View File

@ -148,6 +148,7 @@ export class IssueAttachmentService extends APIService {
{ {
...nextBeamViewer, ...nextBeamViewer,
assetId, assetId,
projectId: nextBeamViewer.projectId || previousBeamViewer.projectId,
previewAvailable: nextBeamViewer.previewAvailable, previewAvailable: nextBeamViewer.previewAvailable,
uploadedBy, uploadedBy,
version: nextVersion, version: nextVersion,

View File

@ -21,9 +21,12 @@ export type TBeamModelVersionRecord = {
downloadUrl: string; downloadUrl: string;
originalFilename: string; originalFilename: string;
previewAvailable: boolean; previewAvailable: boolean;
projectId?: string;
sha256?: string; sha256?: string;
size: number; size: number;
sourceSrc?: string;
src: string; src: string;
status?: string;
type: string; type: string;
uploadedBy?: string; uploadedBy?: string;
uploadedAt: string; uploadedAt: string;
@ -39,6 +42,7 @@ export type TBeamViewerAttachment = {
downloadUrl: string; downloadUrl: string;
originalFilename: string; originalFilename: string;
previewAvailable: boolean; previewAvailable: boolean;
projectId?: string;
sha256?: string; sha256?: string;
src: string; src: string;
type: string; type: string;
@ -72,6 +76,16 @@ export type TBeamConversionStatus = {
versionId?: string; versionId?: string;
}; };
export type TBeamVersionHistoryResponse = {
assetId?: string;
currentVersion?: number;
currentVersionId?: string;
ok: boolean;
projectId?: string;
updatedAt?: string;
versions: TBeamModelVersionRecord[];
};
export const BEAM_VIEWER_OPEN_EVENT = "nodedc:beam-viewer-open"; export const BEAM_VIEWER_OPEN_EVENT = "nodedc:beam-viewer-open";
export type TBeamViewerOpenEventDetail = { export type TBeamViewerOpenEventDetail = {
@ -192,7 +206,7 @@ export const getBeamViewerAttachment = (attachment: TIssueAttachment | undefined
}; };
const getAttachmentVersionNumber = (attachment: TIssueAttachment | undefined): number => { const getAttachmentVersionNumber = (attachment: TIssueAttachment | undefined): number => {
const rawVersion = attachment?.attributes.version; const rawVersion = (attachment?.attributes as { version?: number | string } | undefined)?.version;
if (typeof rawVersion === "number" && Number.isFinite(rawVersion)) return rawVersion; if (typeof rawVersion === "number" && Number.isFinite(rawVersion)) return rawVersion;
if (typeof rawVersion === "string") { if (typeof rawVersion === "string") {
const parsed = Number.parseInt(rawVersion.replace(/^v/i, ""), 10); const parsed = Number.parseInt(rawVersion.replace(/^v/i, ""), 10);
@ -222,9 +236,12 @@ export const createBeamModelVersionRecord = (
downloadUrl: beamViewer.downloadUrl, downloadUrl: beamViewer.downloadUrl,
originalFilename: beamViewer.originalFilename, originalFilename: beamViewer.originalFilename,
previewAvailable: beamViewer.previewAvailable, previewAvailable: beamViewer.previewAvailable,
projectId: beamViewer.projectId,
sha256: beamViewer.sha256, sha256: beamViewer.sha256,
size: options.size ?? beamViewer.conversion?.size ?? Number(options.attachment?.attributes.size ?? 0), size: options.size ?? beamViewer.conversion?.size ?? Number(options.attachment?.attributes.size ?? 0),
sourceSrc: beamViewer.conversion?.sourceSrc || beamViewer.src,
src: beamViewer.src, src: beamViewer.src,
status: beamViewer.conversion?.status || "ready",
type: beamViewer.type, type: beamViewer.type,
uploadedBy: beamViewer.uploadedBy || options.uploadedBy || options.attachment?.created_by, uploadedBy: beamViewer.uploadedBy || options.uploadedBy || options.attachment?.created_by,
uploadedAt: beamViewer.uploadedAt, uploadedAt: beamViewer.uploadedAt,
@ -251,7 +268,7 @@ export const getBeamModelVersionRecords = (
return versions return versions
.filter((version) => typeof version?.version === "number" && Number.isFinite(version.version)) .filter((version) => typeof version?.version === "number" && Number.isFinite(version.version))
.toSorted((first, second) => first.version - second.version); .sort((first, second) => first.version - second.version);
}; };
export const mergeBeamModelVersionRecords = ( export const mergeBeamModelVersionRecords = (
@ -268,11 +285,26 @@ export const mergeBeamModelVersionRecords = (
) || ) ||
nextBeamViewer.versions?.[0] || nextBeamViewer.versions?.[0] ||
createBeamModelVersionRecord(nextBeamViewer); createBeamModelVersionRecord(nextBeamViewer);
return [...previousVersions.filter((version) => !isSameBeamVersionRecord(version, nextRecord)), nextRecord].toSorted( return [...previousVersions.filter((version) => !isSameBeamVersionRecord(version, nextRecord)), nextRecord].sort(
(first, second) => first.version - second.version (first, second) => first.version - second.version
); );
}; };
export const mergeBeamModelVersionRecordLists = (
firstVersions: TBeamModelVersionRecord[] = [],
secondVersions: TBeamModelVersionRecord[] = []
): TBeamModelVersionRecord[] => {
const byKey = new Map<string, TBeamModelVersionRecord>();
[...firstVersions, ...secondVersions].forEach((version) => {
const key = version.versionId || `version-${version.version}`;
const previous = byKey.get(key);
byKey.set(key, { ...(previous || {}), ...version });
});
return [...byKey.values()]
.filter((version) => typeof version?.version === "number" && Number.isFinite(version.version))
.sort((first, second) => first.version - second.version);
};
export const syncCurrentBeamVersionRecord = ( export const syncCurrentBeamVersionRecord = (
beamViewer: TBeamViewerAttachment, beamViewer: TBeamViewerAttachment,
attachment?: TIssueAttachment attachment?: TIssueAttachment
@ -287,6 +319,14 @@ export const syncCurrentBeamVersionRecord = (
export const getBeamVersionViewerUrl = (version: TBeamModelVersionRecord): string | undefined => { export const getBeamVersionViewerUrl = (version: TBeamModelVersionRecord): string | undefined => {
if (version.viewerUrl) return version.viewerUrl; if (version.viewerUrl) return version.viewerUrl;
if (!version.conversion && version.previewAvailable && version.src) {
return buildBeamViewerUrl({
name: version.originalFilename,
settingsSrc: version.sourceSrc || version.src,
src: version.src,
type: version.type,
});
}
if (version.conversion?.status !== "ready" || !version.conversion.artifactSrc) return undefined; if (version.conversion?.status !== "ready" || !version.conversion.artifactSrc) return undefined;
const artifactType = version.conversion.artifactType || version.conversion.targetFormat || "gltf"; const artifactType = version.conversion.artifactType || version.conversion.targetFormat || "gltf";
return buildBeamViewerUrl({ return buildBeamViewerUrl({
@ -302,7 +342,7 @@ export const fetchBeamConversionStatus = async (beamViewer: TBeamViewerAttachmen
const statusUrl = new URL("/api/conversions/status", `${getBeamApiBaseUrl()}/`); const statusUrl = new URL("/api/conversions/status", `${getBeamApiBaseUrl()}/`);
statusUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src)); statusUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src));
const response = await fetch(statusUrl.toString()); const response = await fetch(statusUrl.toString(), { credentials: "include" });
if (!response.ok) { if (!response.ok) {
throw new Error(`BIM conversion status failed: HTTP ${response.status}`); throw new Error(`BIM conversion status failed: HTTP ${response.status}`);
} }
@ -315,6 +355,25 @@ export const fetchBeamConversionStatus = async (beamViewer: TBeamViewerAttachmen
}; };
}; };
export const fetchBeamModelVersions = async (
beamViewer: TBeamViewerAttachment
): Promise<TBeamVersionHistoryResponse> => {
const versionsUrl = new URL("/api/uploads/versions", `${getBeamApiBaseUrl()}/`);
if (beamViewer.projectId && beamViewer.assetId) {
versionsUrl.searchParams.set("projectId", beamViewer.projectId);
versionsUrl.searchParams.set("assetId", beamViewer.assetId);
} else {
versionsUrl.searchParams.set("src", toBeamRelativeUploadSrc(beamViewer.src));
}
const response = await fetch(versionsUrl.toString(), { credentials: "include" });
if (!response.ok) {
throw new Error(`BIM version history failed: HTTP ${response.status}`);
}
return (await response.json()) as TBeamVersionHistoryResponse;
};
export const uploadBeamModelFile = ( export const uploadBeamModelFile = (
file: File, file: File,
options: { options: {
@ -347,6 +406,7 @@ export const uploadBeamModelFile = (
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("POST", uploadUrl.toString()); xhr.open("POST", uploadUrl.toString());
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/octet-stream"); xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.upload.addEventListener("progress", (event) => { xhr.upload.addEventListener("progress", (event) => {
if (!event.lengthComputable || !options.onUploadProgress) return; if (!event.lengthComputable || !options.onUploadProgress) return;
@ -368,6 +428,7 @@ export const uploadBeamModelFile = (
assetId?: string; assetId?: string;
conversion?: TBeamViewerAttachment["conversion"]; conversion?: TBeamViewerAttachment["conversion"];
originalFilename?: string; originalFilename?: string;
projectId?: string;
sha256?: string; sha256?: string;
src?: string; src?: string;
uploadedAt?: string; uploadedAt?: string;
@ -386,6 +447,7 @@ export const uploadBeamModelFile = (
downloadUrl: downloadUrl.toString(), downloadUrl: downloadUrl.toString(),
originalFilename: response.originalFilename || file.name, originalFilename: response.originalFilename || file.name,
previewAvailable: !!directViewerType, previewAvailable: !!directViewerType,
projectId: response.projectId,
sha256: response.sha256, sha256: response.sha256,
src: downloadUrl.toString(), src: downloadUrl.toString(),
type, type,
@ -443,6 +505,7 @@ const deleteBeamStorageResource = async (
): Promise<void> => { ): Promise<void> => {
const response = await fetch(new URL(endpoint, `${getBeamApiBaseUrl()}/`).toString(), { const response = await fetch(new URL(endpoint, `${getBeamApiBaseUrl()}/`).toString(), {
method: "DELETE", method: "DELETE",
credentials: "include",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },

View File

@ -26,9 +26,12 @@ export type TBeamModelVersionRecord = {
downloadUrl: string; downloadUrl: string;
originalFilename: string; originalFilename: string;
previewAvailable: boolean; previewAvailable: boolean;
projectId?: string;
sha256?: string; sha256?: string;
size: number; size: number;
sourceSrc?: string;
src: string; src: string;
status?: string;
type: string; type: string;
uploadedBy?: string; uploadedBy?: string;
uploadedAt: string; uploadedAt: string;
@ -44,6 +47,7 @@ export type TBeamViewerAttachment = {
downloadUrl: string; downloadUrl: string;
originalFilename: string; originalFilename: string;
previewAvailable: boolean; previewAvailable: boolean;
projectId?: string;
sha256?: string; sha256?: string;
src: string; src: string;
type: string; type: string;

View File

@ -29,6 +29,8 @@ PLANE_ADMIN_IMAGE=${PLANE_ADMIN_IMAGE:-nodedc/plane-admin:ru}
VITE_NODEDC_LAUNCHER_URL=${VITE_NODEDC_LAUNCHER_URL:-https://hub.nodedc.ru} VITE_NODEDC_LAUNCHER_URL=${VITE_NODEDC_LAUNCHER_URL:-https://hub.nodedc.ru}
VITE_NODEDC_OIDC_LOGIN_ENABLED=${VITE_NODEDC_OIDC_LOGIN_ENABLED:-1} VITE_NODEDC_OIDC_LOGIN_ENABLED=${VITE_NODEDC_OIDC_LOGIN_ENABLED:-1}
VITE_BEAM_VIEWER_BASE_URL=${VITE_BEAM_VIEWER_BASE_URL:-https://bim.nodedc.tech}
VITE_BEAM_API_BASE_URL=${VITE_BEAM_API_BASE_URL:-$VITE_BEAM_VIEWER_BASE_URL}
RUN_SMOKE=${RUN_SMOKE:-1} RUN_SMOKE=${RUN_SMOKE:-1}
SMOKE_BASE_URL=${SMOKE_BASE_URL:-https://ops.nodedc.ru} SMOKE_BASE_URL=${SMOKE_BASE_URL:-https://ops.nodedc.ru}
@ -104,6 +106,8 @@ if [ "$BUILD_WEB" = "1" ]; then
$DOCKER_BUILD_NETWORK_ARGS \ $DOCKER_BUILD_NETWORK_ARGS \
--build-arg "VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL" \ --build-arg "VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL" \
--build-arg "VITE_NODEDC_OIDC_LOGIN_ENABLED=$VITE_NODEDC_OIDC_LOGIN_ENABLED" \ --build-arg "VITE_NODEDC_OIDC_LOGIN_ENABLED=$VITE_NODEDC_OIDC_LOGIN_ENABLED" \
--build-arg "VITE_BEAM_VIEWER_BASE_URL=$VITE_BEAM_VIEWER_BASE_URL" \
--build-arg "VITE_BEAM_API_BASE_URL=$VITE_BEAM_API_BASE_URL" \
-t "$PLANE_FRONTEND_IMAGE" \ -t "$PLANE_FRONTEND_IMAGE" \
-f apps/web/Dockerfile.web.nas-legacy . -f apps/web/Dockerfile.web.nas-legacy .
RECREATE_SERVICES="$RECREATE_SERVICES web" RECREATE_SERVICES="$RECREATE_SERVICES web"