# Central Model Repository

The Central Model Repository (CMR) was introduced with Portal and replaces both the model hosting folder and the model folder on the render server with a centrally managed system. UDiTH models are imported into the CMR so that they are available across your organisation.

## Concept

### Model

A model is created when a model version is imported for the first time. It is identified by a model identifier. Each model can have multiple model versions. The model identifier must be unique. Each model also stores a description, tags, title, preview image, and logical path.

The identifier can contain alphanumeric characters and `_`.

### Modelversion

Each model version is identified by a combination of the model identifier and the model version identifier. The latest model version is also available through the alias tag `latest`. It is not possible to assign the model version identifier 'latest' manually. When a model is requested, a specific model version is always required. If no version is specified, the alias tag `latest` is used.

The same model version identifier can be used across multiple models, but it must be unique within a single model. Each model version is only a snapshot in time and contains no delta information.

The identifier can contain alphanumeric characters and `_`.

### Revision

A revision changes individual files of an existing model version without a new import. Revisions are numbered per model version (`1`, `2`, ...) and are applied on top of each other. The model version without any revisions is available as revision `0`. For details, see [Revisions](#revisions).

### Storage

Storage is managed entirely by Portal. It is not possible to intervene manually in the process.

### Import

A model must be imported in order to transfer it into the storage system. During import, the model identifier and model version identifier are specified and other settings must also be defined.

### Attribute Import

It is possible to extract attributes into a tag register, which allows Portal search to find model objects, both 3D and PID, across multiple models.

## Import Process

> ⚠️WARNING⚠️ In the current version, models must be zipped by the administrator before import. This is expected to change in future UDiTH Builder versions, where this process will be automated. Ensure that the `Data` folder is at the top level inside the ZIP archive. Otherwise, the model will be rejected during the import process.


1. Place the ZIP file in the import folder on the server.
1. Navigate to the **CMR -> Model import** section.
1. Select the model to import.
![Model Import Selection](media/Portal_model_import_selection.png)
1. Choose either a new model or an existing model to which a new model version should be added.
![Model Import new or existing model](media/Portal_model_import_select.png)
1. Fill in the form by specifying the name, model ID, folder, version ID, preview image, description, tags, and the number of preloaded and maximum instances for BBV. For the tag register, you can choose which attributes to import. Import can also be disabled either here or globally through configuration. ⚠️ **Do not import all attributes.**
![New Model](media/Portal_model_new_model.png)
1. After the form has been submitted, the model import process starts. The model becomes available for viewing once the transfer to storage has been completed. Attribute import may take longer.
1. If you choose an existing model, the form is prefilled with the contents of the previous import.

## Folders

Models are organised in folders, and permissions are assigned per folder (see [Permission Management](/UDiTH%20Portal/Permission%20Management)). A folder that does not exist yet is created automatically during import, for example from `ParentFolderPath` in `modelDefinition.json`. Such a new folder has no permissions of its own, so the model is only visible to users who have permissions on a parent folder.

To set up permissions before the first import, create the folders ahead of time:

- **Administration → Access** (Admin only): select a parent folder in the folder tree and click **Add folder**.
- **Import form** (Admin or Browser Maintainer): open the folder selection, select a parent folder and click **Add folder**. Browser Maintainers can only create folders below a folder on which they hold a permission.

Rules:

- Folders can only be created inside the models folder.
- Folders cannot be created inside a model.
- The folder name must not contain `/` and can have a maximum of 200 characters.

## Automated Import

It is possible to automate the process by specifying a `modelDefinition.json` file inside the `Data` folder.

Many attributes can be ignored and may be left empty or removed. In that case, the default behaviour is used, or the previous value if a new model version is being added to an existing model.

During the first import of a model, ensure that the `Model` section is filled in completely. If the model already exists, only `ExternalIdentifier` is required. If other values are specified, they also overwrite the corresponding attributes of the existing model.

If `AutoApprove` is set, an automatic import is attempted. If this fails for any reason, a manual approval can still be attempted.

### Format
```json
{
    "Cmr": {
        "ProcessOptions": {
            "SkipProcessing": false,
            "MaxRetainedModelVersions": null,
            "AutoApprove": false,
            "KeepAllPreloads": null
        },
        "ModelVersion": {
            "ExternalIdentifier": "REQUIRED",
            "Description": null,
            "Tags": null,
            "AttributesPidProcess": null,
            "Attributes3DProcess": null,
            "IsProtected": false,
            "DesiredPreloadInstances": null,
            "MaximumInstances": null
        },
        "Model": {
            "ParentFolderPath": null,
            "Name": "REQUIRED on first import else can be null",
            "ExternalIdentifier": "REQUIRED",
            "ModelAliases": null
        }
    }
}
```

#### ProcessOptions

| Field | Type | Description |
|-------|------|-------------|
| `SkipProcessing` | `bool?` | Skip attribute extraction for this import. When `null`, the global `ModelProcessing__ForcedSkipProcessing` setting is used. When `true`, the model is imported but no attributes are written to the database. |
| `MaxRetainedModelVersions` | `int?` | Maximum number of model versions to keep for this model. When a new version is imported and the limit is exceeded, the oldest unprotected versions are automatically marked for deletion. Leave `null` to keep all versions. |
| `AutoApprove` | `bool?` | When `true`, the import proceeds automatically without manual approval in the web interface. If automatic approval fails for any reason, manual approval can still be attempted. |
| `KeepAllPreloads` | `bool?` | When `true`, all existing model versions retain their `DesiredPreloadInstances` value after a new version is imported. When `false` or `null`, preload instances of all previous versions are set to `0`, so that only the latest version is preloaded for BBV. |

#### ModelVersion

| Field | Type | Description |
|-------|------|-------------|
| `ExternalIdentifier` | `string` | **Required.** Unique identifier for this model version within its model. Can contain alphanumeric characters and `_`. |
| `Description` | `string?` | Optional description of the model version. |
| `Tags` | `string[]?` | Optional list of tags for categorisation and search. |
| `AttributesPidProcess` | `string[]?` | List of PID attribute names to extract into the tag register. ⚠️ Do not import all attributes. |
| `Attributes3DProcess` | `string[]?` | List of 3D attribute names to extract into the tag register. ⚠️ Do not import all attributes. |
| `IsProtected` | `bool` | When `true`, this model version is protected from automatic deletion by retention rules (`MaxRetainedModelVersions`). Protected versions are excluded from retention calculations. Default: `false`. |
| `DesiredPreloadInstances` | `int?` | Number of BBV instances to preload for this version. Preloaded instances reduce initial load time for users. When not set, the value from the previous version is used. |
| `MaximumInstances` | `int?` | Maximum number of concurrent BBV instances allowed for this version. When not set, the value from the previous version is used. |

#### Model

| Field | Type | Description |
|-------|------|-------------|
| `ExternalIdentifier` | `string` | **Required.** Unique identifier for the model. Can contain alphanumeric characters and `_`. Used to match imports to existing models. |
| `Name` | `string` | **Required on first import.** Display name of the model. On subsequent imports, this can be `null` to keep the existing name, or set to overwrite it. |
| `ParentFolderPath` | `string?` | Logical folder path for organising models in the web interface (e.g. `"Demoplants"`). |
| `ModelAliases` | `string[]?` | Alternative identifiers for the model. These aliases can be used to reference the model in addition to the primary `ExternalIdentifier`. |

## ModelUploader CLI

`ModelUploader.exe` is a command-line tool that uploads a model ZIP file to Portal. After the upload, the model appears in the import as described in [Import Process](#import-process). To import without manual approval, add a `modelDefinition.json` with `AutoApprove` to the ZIP file (see [Automated Import](#automated-import)).

### Authentication

The tool signs in with a Keycloak service account. The service account needs the **Admin** role (`CaxpertsAdmin`).

1. Create a Keycloak client with **Service accounts roles** enabled. See [Advanced integration options](/UDiTH%20Portal/Advanced%20integration%20options), section *How to create a service account*.
1. Assign the realm role `CaxpertsAdmin` to the service account of the client.
1. Copy the client secret from the **Credentials** tab of the client.

### Configuration

Enter the settings in `appsettings.json` next to `ModelUploader.exe`:

```json
{
  "TusSettings": {
    "TusEndpoint": "https://<portal-url>/api/files"
  },
  "Keycloak": {
    "ClientId": "<client-id>",
    "ClientSecret": "<client-secret>",
    "TokenEndpoint": "https://<keycloak-url>/realms/<realm>/protocol/openid-connect/token"
  }
}
```

You can also set the values as environment variables, for example `Keycloak__ClientSecret`. Environment variables override `appsettings.json`.

### Usage

```cmd
ModelUploader.exe upload C:\Models\my-model.zip
```

| Option | Description |
|--------|-------------|
| `--no-wait` | Exit as soon as Portal has received the file, without waiting for Portal to process it. |
| `--log <path>` | Write the log file to the given path. |

### Exit Codes

| Code | Meaning |
|------|---------|
| `0` | Success. The model is ready for import. |
| `10`, `11` | The file does not exist or cannot be opened. |
| `20` | The configuration is missing or invalid. The output lists all problems. |
| `30`–`33` | Sign-in with Keycloak failed. Check the client ID, the client secret and the token endpoint. |
| `40`–`44` | The upload failed. Check the connection to Portal and the Admin role of the service account. |
| `50` | Portal rejected the file, for example because it is not a valid model ZIP. |
| `51`, `52` | Portal received the file, but the result is not known yet. Check the import in Portal before you upload again. |
| `60` | The upload was cancelled. |

## Model Aliases

Model aliases are alternative identifiers for a model. They can be set during import via the `ModelAliases` field in `modelDefinition.json` or edited in the model settings. When a model is requested by identifier, the system first checks for an exact match on the `ExternalIdentifier`. If no match is found, it searches the aliases.

This is useful when a model needs to be referenced by different names, for example, when the external identifier follows an internal naming convention but users should be able to access it by a friendlier name.

Aliases can also be used to create shareable model links. For details, see the [Linking](/UDiTH%20Portal/Linking) page.

### Example First Import
```json
{
    "Cmr": {
        "ProcessOptions": {
            "AutoApprove": true
        },
        "ModelVersion": {
            "ExternalIdentifier": "2025_10_23",
            "Description": "this is optional",
            "Tags": ["this", "is", "also", "optional"],
            "AttributesPidProcess": ["PID", "attributes", "to", "import"],
            "Attributes3DProcess": ["3D", "attributes", "to", "import"],
            "DesiredPreloadInstances": 1,
            "MaximumInstances": 5
        },
        "Model": {
            "ParentFolderPath": "Demoplants",
            "Name": "Sample Demoplant",
            "ExternalIdentifier": "sample"
        }
    }
}
```

### Example Second Import
```json
{
    "Cmr": {
        "ProcessOptions": {
            "AutoApprove": true
        },
        "ModelVersion": {
            "ExternalIdentifier": "2025_10_24"
        },
        "Model": {
            "ExternalIdentifier": "sample"
        }
    }
}
```

## Revisions

A revision changes individual files of an existing model version, for example colour files, drawing templates, the standard catalogue or `projectInfo.bytes`. A new import is not necessary. The base model version stays unchanged in storage. Portal applies the revisions when the model is opened.

### Behaviour

- A revision starts as a **Draft**. Only Admins and Browser Maintainers can see and edit drafts.
- When you publish a revision, it gets the next number (`1`, `2`, ...). A published revision can no longer be changed.
- Published revisions are cumulative. Revision `2` contains the changes of revision `1` and its own changes.
- When a model version is opened without a revision, all published revisions are applied.
- The model version without any revisions is available as revision `0` (**Base version**). It can be opened, but not edited.
- A draft starts from the current state, that is the base version with all published revisions.
- When a model version is deleted, its revisions are also deleted.

### Permissions

Admins and Browser Maintainers can create, edit, publish and delete revisions. Browser Maintainers need a permission on the folder of the model. Other users with access to the model see only the published revisions and the base version, and can open them.

### Protected Files

Some files of the model are generated by UDiTH Builder and cannot be changed in a revision:

- **Hidden** (not shown, no download): geometry and attribute data, for example `Data/lod*nodes`, `Data/attributeSets`, `Data/octree.bytes`, license files, drawings, documents, IntelliPIDs, textures, pictures and the PDF cache.
- **Read-only** (download only): `Data/modelDefinition.json`.

All other files and folders can be changed.

### Create and Publish a Revision

1. In the CMR overview, open the model version.
1. In the **Revisions** section, click **Create new revision**.
1. Enter a description and click **Save**.
1. In **Revision Files**, change the files:
   - Upload a file to add it or to replace an existing file.
   - Create a folder.
   - Delete a file or folder.
   - Download a file.
1. The list **Files changed** shows all changes of the draft. To undo a change, delete the entry from this list.
1. Click **Publish**. The revision becomes available to all users with access to the model.

To delete a draft, open it and click **Delete**.

### Open a Revision

Each published revision and the base version has open buttons on its card in the **Revisions** section. Model links can also open a specific revision (see [Linking](/UDiTH%20Portal/Linking)):

```
https://<portal-url>/modellink/<alias-or-identifier>/<version>/revision_<number>
```

| Example | Result |
|---------|--------|
| `/modellink/myplant/latest` | Latest model version with all published revisions |
| `/modellink/myplant/2025_10_23/revision_1` | Version `2025_10_23` with revision `1` |
| `/modellink/myplant/2025_10_23/revision_0` | Version `2025_10_23` without revisions (base version) |

The number alone (`/modellink/myplant/2025_10_23/1`) is also accepted. Drafts cannot be opened with a link.
