Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0745de822a | ||
|
|
f4df6a0834 | ||
|
|
4b36136432 | ||
|
|
7dde9ffffe | ||
|
|
f6f59fae67 | ||
|
|
0752128789 | ||
|
|
517697197e | ||
|
|
c71296b01a | ||
|
|
86a8adf03b | ||
|
|
6153c7f145 | ||
|
|
885977c4aa | ||
|
|
95c6e60199 | ||
|
|
b54304fef8 | ||
|
|
8407c9a956 | ||
|
|
c2d33780f9 | ||
|
|
90c7be06de | ||
|
|
7d2d55543e |
No files matched your search
@@ -1,5 +1,41 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.11.0 — 2026-11-08
|
||||||
|
|
||||||
|
- **arc42 and ArchiMate examples.** *Archify | New Model…* now offers nine examples, three per framework. **arc42** (Cinema Tickets, IoT Telemetry, Team Wiki): the overview is the business context with quality goals, constraints, quality scenarios, risks and a glossary as cards; then `0300-context`, `0500-building-blocks` (levels 1 and 2), `0600-runtime` (sequence diagrams), `0700-deployment`, and the decisions as section 9. **ArchiMate** (Insurance Claims, HR Onboarding, Logistics Tracking): the overview is a layered view of the business, application and technology layers; then `0100-business` (with the business process as a workflow), `0200-application` and `0300-technology`, one registry file per layer, each element's ArchiMate type under its name.
|
||||||
|
- **`ref:` in every diagram kind.** Workflow and dataflow nodes, sequence participants and lifecycle states can use registry elements, as architecture components do; the Source view's schema check accepts them too. Sequence participants take no `tag`.
|
||||||
|
|
||||||
|
## 0.10.10 — 2026-11-08
|
||||||
|
|
||||||
|
- README: the Buy me a coffee button at the top.
|
||||||
|
|
||||||
|
## 0.10.9 — 2026-11-08
|
||||||
|
|
||||||
|
- The Sync button is square again on the model, registry, decision and Graph pages (0.10.8 squeezed it narrow there).
|
||||||
|
|
||||||
|
## 0.10.8 — 2026-11-08
|
||||||
|
|
||||||
|
- **A model opens as its outline.** On `main.archify` the top-level panels (Shared, Diagrams, Registries, Decisions) start collapsed; open the one you need. What you open or close is kept while the page is open.
|
||||||
|
- **Sync button in line on every page.** On the model, registry and decision pages it sits next to Model | Graph | Source instead of floating above it, and on the Graph page it comes after Export PNG instead of covering it. It takes the height of the controls beside it.
|
||||||
|
|
||||||
|
## 0.10.7 — 2026-11-08
|
||||||
|
|
||||||
|
- **Model | Graph | Source looks like 2D | 3D.** The selected view is the same inset pill, the same height as the other toolbar controls.
|
||||||
|
- The Sync button of a shared model is green in high contrast themes too.
|
||||||
|
|
||||||
|
## 0.10.6 — 2026-11-08
|
||||||
|
|
||||||
|
- **Sync sign-in comes back by itself.** The browser now returns the sign-in to a one-off port on this machine (`127.0.0.1`) instead of a `vscode://` link. Browsers open such links only after a click, so when you were already signed in to the identity provider the editor kept waiting. Remote and web editors still use the link.
|
||||||
|
|
||||||
|
## 0.10.5 — 2026-11-08
|
||||||
|
|
||||||
|
- **Sync button fixed.** It is now an icon only (a cloud; green with a broadcast icon once the model is shared), the same size on every page and theme. On a diagram it sits in the toolbar after **Export** instead of over it; in narrow windows and high contrast themes it no longer shrinks or loses its colour.
|
||||||
|
|
||||||
|
## 0.10.4 — 2026-11-07
|
||||||
|
|
||||||
|
- **A real Sync button.** Every Archify page (diagram, model, graph, registry, decision, and in Source) shows a **Sync** button top right. It opens the Sync menu: sign in, share the model, join one, set the server address. On a model that is shared it turns green and reads **Shared**.
|
||||||
|
- Removed the small title-bar icon and the status bar entry shown before anything was shared, with their settings `archify.sync.titleButton` and `archify.sync.statusBar`. A shared model still shows its live state in the status bar.
|
||||||
|
|
||||||
## 0.10.3 — 2026-11-07
|
## 0.10.3 — 2026-11-07
|
||||||
|
|
||||||
- **Sync button in the title bar.** Every Archify page (and `.archify` text editors) has a **Sync** button top right, next to *Open in Text Editor*: sign in, share a model, join one, set the server address. The status bar entry stays for those who show the status bar. `archify.sync.titleButton: false` hides the button.
|
- **Sync button in the title bar.** Every Archify page (and `.archify` text editors) has a **Sync** button top right, next to *Open in Text Editor*: sign in, share a model, join one, set the server address. The status bar entry stays for those who show the status bar. `archify.sync.titleButton: false` hides the button.
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
# Archify Diagram Viewer
|
# Archify Diagram Viewer
|
||||||
|
|
||||||
|
[](https://buymeacoffee.com/root.at.skic)
|
||||||
|
|
||||||
Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type.
|
Create and view [Archify](https://github.com/tt-a1i/archify) diagrams inside VS Code. You write a diagram as a **`.archify` file in YAML**; clicking it opens the interactive diagram, and the diagram follows your edits as you type.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
|
This is an unofficial extension. It bundles the Archify renderer (MIT, © tt-a1i) and runs it on VS Code's own Node runtime, so nothing else needs to be installed.
|
||||||
|
|
||||||
Free and open source. If it saves you time, you can [buy me a coffee](https://buymeacoffee.com/root.at.skic) ☕
|
|
||||||
|
|
||||||
## Supported files
|
## Supported files
|
||||||
|
|
||||||
| File | Format | What the extension does |
|
| File | Format | What the extension does |
|
||||||
@@ -22,7 +22,7 @@ New diagrams should be `.archify` files; everything below describes them unless
|
|||||||
|
|
||||||
The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case.
|
The fastest start: right-click a folder in the Explorer and choose **Archify | New Diagram…** (also in *File → New File…* and the Command Palette). Pick a kind of diagram (Architecture, Workflow, Sequence, Data Flow, Lifecycle or Everyday Plan), give it a name, and a small working `.archify` file is created and opened. Change its YAML to fit your case.
|
||||||
|
|
||||||
For a whole set of diagrams, choose **Archify | New Model…** instead and pick one of the examples (C4 - Standard Web App, C4 - E-Shop, C4 - Event-Based Integration). A model folder with context, container and component diagrams and a shared registry is created and its model page opens. See [Models](#models).
|
For a whole set of diagrams, choose **Archify | New Model…** instead and pick one of the examples: three in C4, three in arc42 and three in ArchiMate. A model folder with its diagrams, a shared registry and decisions is created and its model page opens. See [Models](#models).
|
||||||
|
|
||||||
To write one by hand:
|
To write one by hand:
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ The Archify CLI and the AI agents that use it write diagrams as JSON, named `nam
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder.
|
- **New diagram from a template**: *Archify | New Diagram…* creates a starter `.archify` file for any of the five diagram kinds, or an everyday plan, in the chosen folder.
|
||||||
- **New model from an example**: *Archify | New Model…* creates a complete model folder from a C4 example (Standard Web App, E-Shop, Event-Based Integration).
|
- **New model from an example**: *Archify | New Model…* creates a complete model folder from an example: C4, arc42 or ArchiMate, three of each.
|
||||||
- **Model graph**: the **Graph** switch on any page of a model shows the whole model as one graph, with a query box (`type:database`, `uses:api`, `from:web to:db`, …), a side panel per element and PNG export. See [Models](#models).
|
- **Model graph**: the **Graph** switch on any page of a model shows the whole model as one graph, with a query box (`type:database`, `uses:api`, `from:web to:db`, …), a side panel per element and PNG export. See [Models](#models).
|
||||||
- **Diagram editor**: `.archify` files open as the rendered diagram. **Source** shows the YAML in its place, in an editor inside the page: problems underlined as you type and counted on the switch, completion and hover from the schema, undo, and ⌘S/Ctrl+S to save. **Open in Text Editor** opens the YAML beside the diagram instead, and edits re-render live.
|
- **Diagram editor**: `.archify` files open as the rendered diagram. **Source** shows the YAML in its place, in an editor inside the page: problems underlined as you type and counted on the switch, completion and hover from the schema, undo, and ⌘S/Ctrl+S to save. **Open in Text Editor** opens the YAML beside the diagram instead, and edits re-render live.
|
||||||
- **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`).
|
- **Live updates**: the diagram re-renders as you edit, and keeps the focus and route you selected (`#focus=…`, `#route=…`).
|
||||||
@@ -126,7 +126,7 @@ online-shop/
|
|||||||
- **Decisions.** One `*.archify-decision` file per architecture decision: id, title, status, date, the registry elements and diagrams it is about, and prose for context, decision and consequences. Diagrams point at decisions with `decisions: [<id>]` on the diagram, a component or a connection. They show as a **Decisions** panel on the `main.archify` page, a **Decisions** rail panel under each diagram (hover lights up what the decision is attached to), on element cards and in the element dialog, and a decision opens as its own page. *Archify | New Decision…* creates one; links are checked in Problems.
|
- **Decisions.** One `*.archify-decision` file per architecture decision: id, title, status, date, the registry elements and diagrams it is about, and prose for context, decision and consequences. Diagrams point at decisions with `decisions: [<id>]` on the diagram, a component or a connection. They show as a **Decisions** panel on the `main.archify` page, a **Decisions** rail panel under each diagram (hover lights up what the decision is attached to), on element cards and in the element dialog, and a decision opens as its own page. *Archify | New Decision…* creates one; links are checked in Problems.
|
||||||
- **Registry view.** A `.archify-registry` file opens as its elements, as cards. Click one and a dialog shows its description, where it is defined and the diagrams that use it; click a diagram to open it. **View | Source** at the top right switches to the YAML, checked against the registry schema as you type.
|
- **Registry view.** A `.archify-registry` file opens as its elements, as cards. Click one and a dialog shows its description, where it is defined and the diagrams that use it; click a diagram to open it. **View | Source** at the top right switches to the YAML, checked against the registry schema as you type.
|
||||||
- **Elements and Connectors.** Under every diagram, Archify's node index is titled **Elements**, and a **Connectors** panel next to it lists every connection as "from → to" with its label. Hovering highlights the connector in the diagram; clicking keeps it highlighted and scrolls to it.
|
- **Elements and Connectors.** Under every diagram, Archify's node index is titled **Elements**, and a **Connectors** panel next to it lists every connection as "from → to" with its label. Hovering highlights the connector in the diagram; clicking keeps it highlighted and scrolls to it.
|
||||||
- **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers). Each has a registry and context, container and component diagrams. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`.
|
- **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers), each with context, container and component diagrams. **arc42 - Cinema Tickets**, **arc42 - IoT Telemetry** and **arc42 - Team Wiki** follow arc42's sections: the overview is the business context with goals, constraints, quality scenarios, risks and a glossary as cards, then `0300-context`, `0500-building-blocks`, `0600-runtime` (sequence diagrams) and `0700-deployment`, with section 9 as decisions. **ArchiMate - Insurance Claims**, **ArchiMate - HR Onboarding** and **ArchiMate - Logistics Tracking** are layered: the overview is a layered view, then `0100-business`, `0200-application` and `0300-technology`, one registry file per layer, each element's ArchiMate type as its tag. Every example has a registry and decisions. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`.
|
||||||
- **The graph.** The **Model | Graph | Source** switch at the top of the `main.archify` page (next to **Export** when the file has a diagram) replaces the page with the whole model as one graph: every element any diagram draws plus every registry element, every connection between them, each once. Registry elements have a solid border, inline ones a dashed border; colours are Archify's type colours and the legend chips at the bottom hide or show a type. **Decisions** are nodes too (tags coloured by status, dotted links to the elements they are about, dashed links to what they supersede), and so are **registries** (one node per file, dotted links to the elements it defines); their chips hide them when the picture gets busy. The layout is **Layered** by default, like Archify's stack: clients on the left, data on the right, registries in a row above, decisions in a row below, connections drawn orthogonally; click the button for a **Force** layout instead. Drag nodes, scroll to zoom, **Fit** (or <kbd>0</kbd>) frames what matches, **Labels** toggles connection labels, the dotted **Links** from decisions and registries to their elements are on by default and can be switched off, in which case only the selected element's show (hovering an element lights up its connections), the sun/moon button switches the page between its dark and light theme (remembered, shared with the diagram pages' theme choice), **Export PNG** saves the picture. On a diagram page the switch reads **Diagram | Graph | Source** and Graph opens the graph already narrowed to that diagram; **Source** on the graph page goes back to `main.archify` in its Source view. *Archify | Open Model Graph* does the same from the Command Palette or Explorer.
|
- **The graph.** The **Model | Graph | Source** switch at the top of the `main.archify` page (next to **Export** when the file has a diagram) replaces the page with the whole model as one graph: every element any diagram draws plus every registry element, every connection between them, each once. Registry elements have a solid border, inline ones a dashed border; colours are Archify's type colours and the legend chips at the bottom hide or show a type. **Decisions** are nodes too (tags coloured by status, dotted links to the elements they are about, dashed links to what they supersede), and so are **registries** (one node per file, dotted links to the elements it defines); their chips hide them when the picture gets busy. The layout is **Layered** by default, like Archify's stack: clients on the left, data on the right, registries in a row above, decisions in a row below, connections drawn orthogonally; click the button for a **Force** layout instead. Drag nodes, scroll to zoom, **Fit** (or <kbd>0</kbd>) frames what matches, **Labels** toggles connection labels, the dotted **Links** from decisions and registries to their elements are on by default and can be switched off, in which case only the selected element's show (hovering an element lights up its connections), the sun/moon button switches the page between its dark and light theme (remembered, shared with the diagram pages' theme choice), **Export PNG** saves the picture. On a diagram page the switch reads **Diagram | Graph | Source** and Graph opens the graph already narrowed to that diagram; **Source** on the graph page goes back to `main.archify` in its Source view. *Archify | Open Model Graph* does the same from the Command Palette or Explorer.
|
||||||
- **Queries.** Type a few words in the box (<kbd>/</kbd> focuses it); matches stay bright, the rest dims, the count shows "9 of 14 elements". Terms combine with AND and a leading `-` excludes: `database` (id, label, sublabel, brand or type contain it), `type:backend`, `diagram:0200-*` (what those diagrams draw), `decision:<id>` (what a decision is about or is attached to), `uses:api` (an element and its neighbours), `near:api,2` (within two steps), `from:web to:db` (the shortest path, following the arrows first), `unused` (registry elements no diagram draws), `shared` and `-shared` (registry or inline elements only), `type:decision` and `type:registry` for the decision and registry nodes. Decisions and registries follow the elements they are attached to into a result; `-type:decision` or `-type:registry` leaves them out. Walks and paths follow the diagrams' connections only, so a registry is never a shortcut between its elements. The **?** button lists them; clicking an example runs it.
|
- **Queries.** Type a few words in the box (<kbd>/</kbd> focuses it); matches stay bright, the rest dims, the count shows "9 of 14 elements". Terms combine with AND and a leading `-` excludes: `database` (id, label, sublabel, brand or type contain it), `type:backend`, `diagram:0200-*` (what those diagrams draw), `decision:<id>` (what a decision is about or is attached to), `uses:api` (an element and its neighbours), `near:api,2` (within two steps), `from:web to:db` (the shortest path, following the arrows first), `unused` (registry elements no diagram draws), `shared` and `-shared` (registry or inline elements only), `type:decision` and `type:registry` for the decision and registry nodes. Decisions and registries follow the elements they are attached to into a result; `-type:decision` or `-type:registry` leaves them out. Walks and paths follow the diagrams' connections only, so a registry is never a shortcut between its elements. The **?** button lists them; clicking an example runs it.
|
||||||
- **Side panel.** Click an element for its description, type, where it is defined (**Registry** opens the file at the line), the diagrams that draw it (click to open), its connections and its decisions (click to open). **Neighbours** runs `uses:` for it. Click a connection for its labels, the diagrams that draw it and its decisions. Click a decision node for its status, context and decision text, the elements and connections it is about, and **Open decision**; a registry node for the elements it defines, the ones no diagram draws yet, and **Open registry**.
|
- **Side panel.** Click an element for its description, type, where it is defined (**Registry** opens the file at the line), the diagrams that draw it (click to open), its connections and its decisions (click to open). **Neighbours** runs `uses:` for it. Click a connection for its labels, the diagrams that draw it and its decisions. Click a decision node for its status, context and decision text, the elements and connections it is about, and **Open decision**; a registry node for the elements it defines, the ones no diagram draws yet, and **Open registry**.
|
||||||
@@ -140,7 +140,7 @@ Optional. Without a sync server the extension works exactly as described above a
|
|||||||
|
|
||||||
[Archify Sync](https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext-sync) is a small server you run yourself (one container and PostgreSQL; a Helm chart is included). With it, a model is edited by several people at once, like a shared document, and its files stay ordinary files on each person's disk, so Git, agents and every page of this extension keep working.
|
[Archify Sync](https://gitea.lego-cloud.eu/vscode-extensions/archify-vscode-ext-sync) is a small server you run yourself (one container and PostgreSQL; a Helm chart is included). With it, a model is edited by several people at once, like a shared document, and its files stay ordinary files on each person's disk, so Git, agents and every page of this extension keep working.
|
||||||
|
|
||||||
- **Where.** The **Sync** button in the title bar of every Archify page (top right, next to *Open in Text Editor*) opens a menu: sign in, share a model, join one, the server address, settings. The same menu is **Archify Sync** in the status bar (in workspaces with Archify files) and *Archify | Sync…* in the Command Palette. `archify.sync.titleButton: false` and `archify.sync.statusBar: false` hide them.
|
- **Where.** Every Archify page (diagram, model, graph, registry, decision, also in Source) has a Sync button (a cloud) top right; on a diagram it sits after **Export**. It opens the Sync menu: sign in, share the model, join one, the server address, settings. On a shared model it turns green with a broadcast icon. The same menu is *Archify | Sync…* in the Command Palette.
|
||||||
- **Server address.** Set `archify.sync.server` once (Settings › Archify › Sync, or *Archify | Set Sync Server Address…*), for example `https://archify.example.com`. Put it in the workspace settings and everyone in the project uses the same server. Left empty, the address is asked for once and remembered.
|
- **Server address.** Set `archify.sync.server` once (Settings › Archify › Sync, or *Archify | Set Sync Server Address…*), for example `https://archify.example.com`. Put it in the workspace settings and everyone in the project uses the same server. Left empty, the address is asked for once and remembered.
|
||||||
- **Sign in.** *Archify | Sign in to Sync Server…* uses that address (or asks for it) and signs in through the browser; the session is kept in VS Code's secret storage and shows under **Accounts** as *Archify Sync*. A new server is set up first on its `/setup` page with the code from its log.
|
- **Sign in.** *Archify | Sign in to Sync Server…* uses that address (or asks for it) and signs in through the browser; the session is kept in VS Code's secret storage and shows under **Accounts** as *Archify Sync*. A new server is set up first on its `/setup` page with the code from its log.
|
||||||
- **Share.** *Archify | Share Model…* (also on `main.archify` in the Explorer) puts the model on the server and writes a small `.archify-sync` file next to `main.archify`: the server and the model's id, no secrets. Commit it, and everyone who clones the repository and is a member opens the shared model.
|
- **Share.** *Archify | Share Model…* (also on `main.archify` in the Explorer) puts the model on the server and writes a small `.archify-sync` file next to `main.archify`: the server and the model's id, no secrets. Commit it, and everyone who clones the repository and is a member opens the shared model.
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "archify-vscode-ext",
|
"name": "archify-vscode-ext",
|
||||||
"version": "0.10.3",
|
"version": "0.11.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "archify-vscode-ext",
|
"name": "archify-vscode-ext",
|
||||||
"version": "0.10.3",
|
"version": "0.11.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.4",
|
"@codemirror/autocomplete": "^6.20.4",
|
||||||
|
|||||||
+2
-17
@@ -2,7 +2,7 @@
|
|||||||
"name": "archify-vscode-ext",
|
"name": "archify-vscode-ext",
|
||||||
"displayName": "Archify Diagram Viewer",
|
"displayName": "Archify Diagram Viewer",
|
||||||
"description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.",
|
"description": "Archify diagrams in VS Code. Write diagrams as .archify files in YAML: they open as the interactive diagram, with live updates, validation, templates and completion. Also previews Archify's own JSON files (*.architecture.json, …) and opens rendered Archify HTML.",
|
||||||
"version": "0.10.3",
|
"version": "0.11.0",
|
||||||
"publisher": "root-at-skic",
|
"publisher": "root-at-skic",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sponsor": {
|
"sponsor": {
|
||||||
@@ -270,11 +270,6 @@
|
|||||||
"command": "archify.openModelGraph",
|
"command": "archify.openModelGraph",
|
||||||
"when": "archify.inModel && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused",
|
"when": "archify.inModel && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused",
|
||||||
"group": "navigation@61"
|
"group": "navigation@61"
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "archify.sync.menu",
|
|
||||||
"when": "config.archify.sync.titleButton && (archify.isDiagramSource || activeCustomEditorId == 'archify.diagram' || activeCustomEditorId == 'archify.registry' || activeCustomEditorId == 'archify.decision') && !notebookEditorFocused",
|
|
||||||
"group": "navigation@62"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"explorer/context": [
|
"explorer/context": [
|
||||||
@@ -526,17 +521,7 @@
|
|||||||
"scope": "window",
|
"scope": "window",
|
||||||
"pattern": "^$|^https?://",
|
"pattern": "^$|^https?://",
|
||||||
"patternErrorMessage": "An address such as https://archify.example.com",
|
"patternErrorMessage": "An address such as https://archify.example.com",
|
||||||
"markdownDescription": "Address of your Archify Sync server, for example `https://archify.example.com`. *Sign in*, *Share Model* and the status bar use it without asking. Set it in the workspace settings so everyone in the project uses the same server. Empty: asked once and remembered. Archify Sync is optional; see the README."
|
"markdownDescription": "Address of your Archify Sync server, for example `https://archify.example.com`. The **Sync** button on Archify pages, *Sign in* and *Share Model* use it without asking. Set it in the workspace settings so everyone in the project uses the same server. Empty: asked once and remembered. Archify Sync is optional; see the README."
|
||||||
},
|
|
||||||
"archify.sync.statusBar": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"markdownDescription": "Show **Archify Sync** in the status bar in workspaces with Archify files. Clicking it offers sign in, share, join and the server address. Shared models always show their state there."
|
|
||||||
},
|
|
||||||
"archify.sync.titleButton": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": true,
|
|
||||||
"markdownDescription": "Show the **Archify Sync** button in the title bar of Archify pages and `.archify` editors: sign in, share, join, server address. Archify Sync is optional; without a server the button only offers to set one up."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -214,7 +214,7 @@
|
|||||||
"ref": {
|
"ref": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||||
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds pos/size and may override label, sublabel and tag."
|
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
|
||||||
},
|
},
|
||||||
"diagram": {
|
"diagram": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -129,13 +129,6 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"label",
|
|
||||||
"stage",
|
|
||||||
"row"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "common.schema.json#/$defs/id"
|
"$ref": "common.schema.json#/$defs/id"
|
||||||
@@ -188,8 +181,35 @@
|
|||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
||||||
|
},
|
||||||
|
"ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||||
|
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
|
||||||
|
},
|
||||||
|
"diagram": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"ref",
|
||||||
|
"stage",
|
||||||
|
"row"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"label",
|
||||||
|
"stage",
|
||||||
|
"row"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flows": {
|
"flows": {
|
||||||
|
|||||||
@@ -145,13 +145,6 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"label",
|
|
||||||
"lane",
|
|
||||||
"col"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "common.schema.json#/$defs/id"
|
"$ref": "common.schema.json#/$defs/id"
|
||||||
@@ -216,8 +209,35 @@
|
|||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
||||||
|
},
|
||||||
|
"ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||||
|
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
|
||||||
|
},
|
||||||
|
"diagram": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"ref",
|
||||||
|
"lane",
|
||||||
|
"col"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"label",
|
||||||
|
"lane",
|
||||||
|
"col"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"transitions": {
|
"transitions": {
|
||||||
|
|||||||
@@ -117,11 +117,6 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"label"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "common.schema.json#/$defs/id"
|
"$ref": "common.schema.json#/$defs/id"
|
||||||
@@ -152,8 +147,31 @@
|
|||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
||||||
|
},
|
||||||
|
"ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||||
|
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
|
||||||
|
},
|
||||||
|
"diagram": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"ref"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"label"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"segments": {
|
"segments": {
|
||||||
|
|||||||
@@ -280,13 +280,6 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"lane",
|
|
||||||
"col",
|
|
||||||
"type",
|
|
||||||
"label"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"$ref": "common.schema.json#/$defs/id"
|
"$ref": "common.schema.json#/$defs/id"
|
||||||
@@ -339,8 +332,35 @@
|
|||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
"description": "Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram."
|
||||||
|
},
|
||||||
|
"ref": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$",
|
||||||
|
"description": "A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag."
|
||||||
|
},
|
||||||
|
"diagram": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Diagram that explains this element, relative to the model root (main.archify). Double-click the node to open it."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"ref",
|
||||||
|
"lane",
|
||||||
|
"col"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"lane",
|
||||||
|
"col",
|
||||||
|
"type",
|
||||||
|
"label"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"edges": {
|
"edges": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// Adds the model keys to the editor schemas in schemas/ (the CLI keeps the
|
// Adds the model keys to the editor schemas in schemas/ (the CLI keeps the
|
||||||
// strict originals): `model` on main.archify, `ref` and `diagram` on
|
// strict originals): `model` on main.archify, `ref` and `diagram` on each
|
||||||
// architecture components, and a schema for *.archify-registry files.
|
// kind's nodes, and a schema for *.archify-registry files.
|
||||||
// Run after sync-archify.mjs, or on its own after editing this file.
|
// Run after sync-archify.mjs, or on its own after editing this file.
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -28,6 +28,7 @@ const DIAGRAM_LINK = { type: 'string', description: 'Diagram that explains this
|
|||||||
const DECISIONS = { type: 'array', items: { type: 'string', minLength: 1 }, description: 'Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram.' };
|
const DECISIONS = { type: 'array', items: { type: 'string', minLength: 1 }, description: 'Ids of architecture decisions (*.archify-decision files of the model) this is about. Shown in the Decisions panel under the diagram.' };
|
||||||
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'];
|
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'];
|
||||||
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'];
|
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'];
|
||||||
|
const NODE_COLLECTION_OF = { architecture: 'components', workflow: 'nodes', dataflow: 'nodes', sequence: 'participants', lifecycle: 'states' };
|
||||||
|
|
||||||
for (const file of fs.readdirSync(schemas)) {
|
for (const file of fs.readdirSync(schemas)) {
|
||||||
if (!/^(architecture|workflow|sequence|dataflow|lifecycle)\.schema\.json$/.test(file)) continue;
|
if (!/^(architecture|workflow|sequence|dataflow|lifecycle)\.schema\.json$/.test(file)) continue;
|
||||||
@@ -38,13 +39,20 @@ for (const file of fs.readdirSync(schemas)) {
|
|||||||
const items = schema.properties[key]?.items;
|
const items = schema.properties[key]?.items;
|
||||||
if (items?.properties) items.properties.decisions = DECISIONS;
|
if (items?.properties) items.properties.decisions = DECISIONS;
|
||||||
}
|
}
|
||||||
if (file === 'architecture.schema.json') {
|
// The list where each kind keeps its nodes takes `ref` (as the extension's resolver does).
|
||||||
const item = schema.properties.components.items;
|
const nodes = NODE_COLLECTION_OF[file.replace('.schema.json', '')];
|
||||||
item.properties.ref = { type: 'string', pattern: '^[a-zA-Z][a-zA-Z0-9_-]*$', description: 'A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds pos/size and may override label, sublabel and tag.' };
|
const item = schema.properties[nodes]?.items;
|
||||||
|
if (item?.properties) {
|
||||||
|
item.properties.ref = { type: 'string', pattern: '^[a-zA-Z][a-zA-Z0-9_-]*$', description: 'A shared element from the model’s registry (*.archify-registry). Its type, label, sublabel, brand and sources come from there; the diagram adds its own placement and may override label, sublabel and tag.' };
|
||||||
item.properties.diagram = DIAGRAM_LINK;
|
item.properties.diagram = DIAGRAM_LINK;
|
||||||
// A ref brings type and label with it; an inline component must state them.
|
// A ref brings type and label with it; an inline node must state them. Re-runs keep the original list.
|
||||||
item.anyOf = [{ required: ['ref'] }, { required: ['id', 'type', 'label'] }];
|
const required = item.required ?? item.anyOf?.[1]?.required;
|
||||||
delete item.required;
|
if (required) {
|
||||||
|
// Placement (lane/col, stage/row) stays the diagram's own; only id, type and label may come from the ref.
|
||||||
|
const placement = required.filter((key) => !['id', 'type', 'label'].includes(key));
|
||||||
|
item.anyOf = [{ required: ['ref', ...placement] }, { required }];
|
||||||
|
delete item.required;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
write(file, schema);
|
write(file, schema);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { modelIdFromName, titleFromName } from './templates';
|
|||||||
/**
|
/**
|
||||||
* Example models shipped with the extension (templates/models/<folder>).
|
* Example models shipped with the extension (templates/models/<folder>).
|
||||||
* "Archify | New Model…" offers them; each is a complete model folder:
|
* "Archify | New Model…" offers them; each is a complete model folder:
|
||||||
* main.archify, registry/*.archify-registry and diagrams by level.
|
* main.archify, registry/*.archify-registry, decisions and diagrams in the
|
||||||
|
* framework's folders: C4 levels, arc42 sections or ArchiMate layers.
|
||||||
*/
|
*/
|
||||||
export interface ModelExample {
|
export interface ModelExample {
|
||||||
/** Folder under templates/models and the default name of the new model folder. */
|
/** Folder under templates/models and the default name of the new model folder. */
|
||||||
@@ -17,8 +18,17 @@ export const MODEL_EXAMPLES: readonly ModelExample[] = [
|
|||||||
{ folder: 'c4-standard-web-app', label: 'C4 - Standard Web App', description: 'Single-page app, API, database, cache, file storage, identity provider' },
|
{ folder: 'c4-standard-web-app', label: 'C4 - Standard Web App', description: 'Single-page app, API, database, cache, file storage, identity provider' },
|
||||||
{ folder: 'c4-e-shop', label: 'C4 - E-Shop', description: 'Web shop, API, worker, queue, payment and email providers' },
|
{ folder: 'c4-e-shop', label: 'C4 - E-Shop', description: 'Web shop, API, worker, queue, payment and email providers' },
|
||||||
{ folder: 'c4-event-based-integration', label: 'C4 - Event-Based Integration', description: 'Event bus, router, dead-letter queue, event store, consumers' },
|
{ folder: 'c4-event-based-integration', label: 'C4 - Event-Based Integration', description: 'Event bus, router, dead-letter queue, event store, consumers' },
|
||||||
|
{ folder: 'arc42-cinema-tickets', label: 'arc42 - Cinema Tickets', description: 'Context, building blocks, runtime and deployment views; seat booking with payment' },
|
||||||
|
{ folder: 'arc42-iot-telemetry', label: 'arc42 - IoT Telemetry', description: 'Devices over MQTT, ingest, time-series store, alerting, dashboards' },
|
||||||
|
{ folder: 'arc42-team-wiki', label: 'arc42 - Team Wiki', description: 'A small self-hosted wiki: the arc42 sections at their smallest' },
|
||||||
|
{ folder: 'archimate-insurance-claims', label: 'ArchiMate - Insurance Claims', description: 'Business, application and technology layers of claim handling' },
|
||||||
|
{ folder: 'archimate-hr-onboarding', label: 'ArchiMate - HR Onboarding', description: 'Onboarding a new employee across HR, identity and IT services' },
|
||||||
|
{ folder: 'archimate-logistics-tracking', label: 'ArchiMate - Logistics Tracking', description: 'Shipment tracking from pick-up to proof of delivery, layer by layer' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** The framework prefix of an example folder; the model id is the folder without it. */
|
||||||
|
export const EXAMPLE_PREFIX = /^(c4|arc42|archimate)-/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The example's main.archify for a new model folder. When the folder keeps
|
* The example's main.archify for a new model folder. When the folder keeps
|
||||||
* the example's name the file is used as written; otherwise id, name and the
|
* the example's name the file is used as written; otherwise id, name and the
|
||||||
|
|||||||
+23
-11
@@ -163,13 +163,21 @@ export interface DecisionRef {
|
|||||||
|
|
||||||
/** Collections whose items may carry `decisions` (and `diagram` links), per diagram kind. */
|
/** Collections whose items may carry `decisions` (and `diagram` links), per diagram kind. */
|
||||||
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'] as const;
|
const NODE_COLLECTIONS = ['components', 'nodes', 'participants', 'states'] as const;
|
||||||
|
/** The list that holds a diagram kind's nodes, where `ref` is resolved. */
|
||||||
|
const NODE_COLLECTION_OF: Record<string, (typeof NODE_COLLECTIONS)[number]> = {
|
||||||
|
architecture: 'components',
|
||||||
|
workflow: 'nodes',
|
||||||
|
dataflow: 'nodes',
|
||||||
|
sequence: 'participants',
|
||||||
|
lifecycle: 'states',
|
||||||
|
};
|
||||||
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'] as const;
|
const EDGE_COLLECTIONS = ['connections', 'edges', 'flows', 'transitions', 'messages'] as const;
|
||||||
|
|
||||||
export interface ResolvedDiagram {
|
export interface ResolvedDiagram {
|
||||||
/** The diagram as Archify expects it: refs expanded, model block and links removed. */
|
/** The diagram as Archify expects it: refs expanded, model block and links removed. */
|
||||||
diagram: Obj;
|
diagram: Obj;
|
||||||
problems: ModelProblem[];
|
problems: ModelProblem[];
|
||||||
/** Component index → the registry element it came from. */
|
/** Node index (in the kind's node list) → the registry element it came from. */
|
||||||
refs: Map<number, RegistryElement>;
|
refs: Map<number, RegistryElement>;
|
||||||
/** Node id → diagram path (relative to the model root) it links to. */
|
/** Node id → diagram path (relative to the model root) it links to. */
|
||||||
links: Map<string, string>;
|
links: Map<string, string>;
|
||||||
@@ -253,10 +261,14 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
|
|||||||
delete diagram.model;
|
delete diagram.model;
|
||||||
const decisionRefs = takeDecisionRefs(diagram, options, problems);
|
const decisionRefs = takeDecisionRefs(diagram, options, problems);
|
||||||
const { registry } = options;
|
const { registry } = options;
|
||||||
const components = Array.isArray(diagram.components) ? diagram.components : undefined;
|
// Each kind keeps its nodes in its own list; sequence participants have no tag.
|
||||||
if (!components) return { diagram, problems, refs, links, used, descriptions, decisionRefs };
|
const kind = str(diagram.diagram_type);
|
||||||
|
const collection = (kind && NODE_COLLECTION_OF[kind]) ?? NODE_COLLECTIONS.find((key) => Array.isArray(diagram[key]));
|
||||||
|
const nodes = collection && Array.isArray(diagram[collection]) ? (diagram[collection] as unknown[]) : undefined;
|
||||||
|
if (!collection || !nodes) return { diagram, problems, refs, links, used, descriptions, decisionRefs };
|
||||||
|
const copied: readonly string[] = kind === 'sequence' ? COPIED_FIELDS.filter((key) => key !== 'tag') : COPIED_FIELDS;
|
||||||
|
|
||||||
diagram.components = components.map((raw, index) => {
|
diagram[collection] = nodes.map((raw, index) => {
|
||||||
if (!isObj(raw)) return raw;
|
if (!isObj(raw)) return raw;
|
||||||
const ref = str(raw.ref);
|
const ref = str(raw.ref);
|
||||||
const linkFromComponent = str(raw.diagram);
|
const linkFromComponent = str(raw.diagram);
|
||||||
@@ -275,7 +287,7 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
|
|||||||
? `Unknown element "${ref}": no registry of this model defines it.`
|
? `Unknown element "${ref}": no registry of this model defines it.`
|
||||||
: `"${ref}" cannot be resolved: ${options.fileName.split(/[\\/]/).pop()} is not inside a model (no ${MAIN_FILE} above it).`,
|
: `"${ref}" cannot be resolved: ${options.fileName.split(/[\\/]/).pop()} is not inside a model (no ${MAIN_FILE} above it).`,
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
pointer: `/components/${index}/ref`,
|
pointer: `/${collection}/${index}/ref`,
|
||||||
});
|
});
|
||||||
return { type: 'external', label: ref, ...own, id };
|
return { type: 'external', label: ref, ...own, id };
|
||||||
}
|
}
|
||||||
@@ -283,19 +295,19 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
|
|||||||
refs.set(index, element);
|
refs.set(index, element);
|
||||||
if (element.description) descriptions.set(id, element.description.trim());
|
if (element.description) descriptions.set(id, element.description.trim());
|
||||||
for (const key of Object.keys(own)) {
|
for (const key of Object.keys(own)) {
|
||||||
if ((COPIED_FIELDS as readonly string[]).includes(key) && !(OVERRIDABLE_FIELDS as readonly string[]).includes(key)) {
|
if (copied.includes(key) && !(OVERRIDABLE_FIELDS as readonly string[]).includes(key)) {
|
||||||
problems.push({
|
problems.push({
|
||||||
message: `"${key}" comes from the registry element "${ref}" and cannot be changed in a diagram.`,
|
message: `"${key}" comes from the registry element "${ref}" and cannot be changed in a diagram.`,
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
pointer: `/components/${index}/${key}`,
|
pointer: `/${collection}/${index}/${key}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const merged: Obj = { id };
|
const merged: Obj = { id };
|
||||||
for (const key of COPIED_FIELDS) if (element[key] !== undefined) merged[key] = element[key];
|
for (const key of copied) if (element[key as (typeof COPIED_FIELDS)[number]] !== undefined) merged[key] = element[key as (typeof COPIED_FIELDS)[number]];
|
||||||
for (const [key, value] of Object.entries(own)) {
|
for (const [key, value] of Object.entries(own)) {
|
||||||
if (key === 'id') continue;
|
if (key === 'id') continue;
|
||||||
if (!(COPIED_FIELDS as readonly string[]).includes(key) || (OVERRIDABLE_FIELDS as readonly string[]).includes(key)) merged[key] = value;
|
if (!copied.includes(key) || (OVERRIDABLE_FIELDS as readonly string[]).includes(key)) merged[key] = value;
|
||||||
}
|
}
|
||||||
const link = linkFromComponent ?? element.diagram;
|
const link = linkFromComponent ?? element.diagram;
|
||||||
if (link) links.set(id, link);
|
if (link) links.set(id, link);
|
||||||
@@ -353,8 +365,8 @@ export function summarizeDiagram(text: string, relativePath: string): DiagramSum
|
|||||||
}
|
}
|
||||||
const obj = isObj(doc) ? doc : {};
|
const obj = isObj(doc) ? doc : {};
|
||||||
const meta = isObj(obj.meta) ? obj.meta : {};
|
const meta = isObj(obj.meta) ? obj.meta : {};
|
||||||
const components = Array.isArray(obj.components) ? obj.components : [];
|
const items = NODE_COLLECTIONS.flatMap((key) => (Array.isArray(obj[key]) ? (obj[key] as unknown[]) : []));
|
||||||
const used = [...new Set(components.map((c) => (isObj(c) ? str(c.ref) : undefined)).filter((r): r is string => !!r))];
|
const used = [...new Set(items.map((c) => (isObj(c) ? str(c.ref) : undefined)).filter((r): r is string => !!r))];
|
||||||
const decisions = takeDecisionRefs({ ...obj }, { registry: { elements: new Map(), files: [], problems: [] }, fileName: relativePath }, []);
|
const decisions = takeDecisionRefs({ ...obj }, { registry: { elements: new Map(), files: [], problems: [] }, fileName: relativePath }, []);
|
||||||
const nodes: DiagramNode[] = [];
|
const nodes: DiagramNode[] = [];
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|||||||
+36
-3
@@ -1,4 +1,5 @@
|
|||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
|
import * as http from 'node:http';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { SyncApi, SyncApiError } from './api';
|
import { SyncApi, SyncApiError } from './api';
|
||||||
import { Account, TokenResponse } from './protocol';
|
import { Account, TokenResponse } from './protocol';
|
||||||
@@ -100,8 +101,13 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
|
|||||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||||
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
|
||||||
const state = crypto.randomBytes(16).toString('base64url');
|
const state = crypto.randomBytes(16).toString('base64url');
|
||||||
const callback = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${this.context.extension.id}/auth`));
|
// On the desktop the browser comes back to a port on this machine: no
|
||||||
const redirectUri = callback.toString(true);
|
// click needed, and it reaches this window. Remote and web editors use
|
||||||
|
// the extension's URI handler (vscode://…/auth) instead.
|
||||||
|
const loopback = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Desktop ? await this.listen().catch(() => undefined) : undefined;
|
||||||
|
const redirectUri = loopback
|
||||||
|
? `http://127.0.0.1:${loopback.port}/auth`
|
||||||
|
: (await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://${this.context.extension.id}/auth`))).toString(true);
|
||||||
const query = new URLSearchParams({
|
const query = new URLSearchParams({
|
||||||
response_type: 'code',
|
response_type: 'code',
|
||||||
redirect_uri: redirectUri,
|
redirect_uri: redirectUri,
|
||||||
@@ -119,6 +125,7 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
|
|||||||
const done = () => {
|
const done = () => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
this.pending.delete(state);
|
this.pending.delete(state);
|
||||||
|
loopback?.close();
|
||||||
};
|
};
|
||||||
this.pending.set(state, {
|
this.pending.set(state, {
|
||||||
resolve: (c) => (done(), resolve(c)),
|
resolve: (c) => (done(), resolve(c)),
|
||||||
@@ -137,9 +144,35 @@ export class SyncAuth implements vscode.AuthenticationProvider, vscode.Disposabl
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A one-off HTTP listener on 127.0.0.1 for the browser's way back; closed when the sign-in ends. */
|
||||||
|
private listen(): Promise<{ port: number; close: () => void }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
||||||
|
const known = url.pathname === '/auth' && this.pending.has(url.searchParams.get('state') ?? '');
|
||||||
|
res.writeHead(known ? 200 : 404, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
||||||
|
res.end(
|
||||||
|
known
|
||||||
|
? '<!doctype html><meta charset="utf-8"><title>Archify Sync</title><body style="font:15px system-ui;margin:48px auto;max-width:420px"><h2>Signed in</h2><p>You can close this tab and go back to the editor.</p></body>'
|
||||||
|
: 'Not found',
|
||||||
|
);
|
||||||
|
if (known) this.completeSignIn(url.searchParams);
|
||||||
|
});
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === 'string') return reject(new Error('no port'));
|
||||||
|
resolve({ port: address.port, close: () => server.close() });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** The browser came back: `…/auth?code=&state=`. */
|
/** The browser came back: `…/auth?code=&state=`. */
|
||||||
handleAuthUri(uri: vscode.Uri): void {
|
handleAuthUri(uri: vscode.Uri): void {
|
||||||
const query = new URLSearchParams(uri.query);
|
this.completeSignIn(new URLSearchParams(uri.query));
|
||||||
|
}
|
||||||
|
|
||||||
|
private completeSignIn(query: URLSearchParams): void {
|
||||||
const state = query.get('state') ?? '';
|
const state = query.get('state') ?? '';
|
||||||
const code = query.get('code');
|
const code = query.get('code');
|
||||||
const pending = this.pending.get(state);
|
const pending = this.pending.get(state);
|
||||||
|
|||||||
+1
-15
@@ -41,8 +41,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
|
|||||||
private cursorTimer: ReturnType<typeof setTimeout> | undefined;
|
private cursorTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
/** Servers with a session on this machine, for the status bar before any model is shared. */
|
/** Servers with a session on this machine, for the status bar before any model is shared. */
|
||||||
private signedIn: string[] = [];
|
private signedIn: string[] = [];
|
||||||
/** Whether the workspace has Archify files; elsewhere the entry stays hidden until a model is shared. */
|
|
||||||
private archifyHere = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly context: vscode.ExtensionContext,
|
private readonly context: vscode.ExtensionContext,
|
||||||
@@ -80,7 +78,6 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
|
|||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
const found = await vscode.workspace.findFiles(`**/${LINK_FILE}`, SKIP, 50);
|
const found = await vscode.workspace.findFiles(`**/${LINK_FILE}`, SKIP, 50);
|
||||||
await Promise.all(found.map((uri) => this.open(uri, 'open')));
|
await Promise.all(found.map((uri) => this.open(uri, 'open')));
|
||||||
this.archifyHere = (await vscode.workspace.findFiles('**/*.{archify,archify-registry,archify-decision}', SKIP, 1)).length > 0;
|
|
||||||
await this.refreshSignedIn();
|
await this.refreshSignedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,19 +136,8 @@ export class SyncFeature implements vscode.Disposable, LiveProvider {
|
|||||||
private render(): void {
|
private render(): void {
|
||||||
const spaces = [...this.spaces.values()];
|
const spaces = [...this.spaces.values()];
|
||||||
if (!spaces.length) {
|
if (!spaces.length) {
|
||||||
|
this.status.hide();
|
||||||
void vscode.commands.executeCommand('setContext', 'archify.sync.active', false);
|
void vscode.commands.executeCommand('setContext', 'archify.sync.active', false);
|
||||||
// Nothing shared here yet: the way in, as before the sync bundle loaded.
|
|
||||||
if (!this.archifyHere || !vscode.workspace.getConfiguration('archify.sync').get<boolean>('statusBar', true)) {
|
|
||||||
this.status.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const hosts = this.signedIn.map((server) => new URL(server).host);
|
|
||||||
this.status.text = hosts.length ? `$(cloud) Archify Sync · ${hosts.join(', ')}` : '$(cloud) Archify Sync';
|
|
||||||
this.status.tooltip = hosts.length
|
|
||||||
? `Signed in to ${hosts.join(', ')}. Click to share a model, join one or sign out.`
|
|
||||||
: 'Archify Sync (optional): sign in, share a model with others or join one. Set the server under Settings › Archify › Sync.';
|
|
||||||
this.status.backgroundColor = undefined;
|
|
||||||
this.status.show();
|
|
||||||
this.bridge?.changed();
|
this.bridge?.changed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,26 +27,8 @@ const COMMANDS = [
|
|||||||
*/
|
*/
|
||||||
export function registerSync(context: vscode.ExtensionContext, models: ModelService): vscode.Disposable[] {
|
export function registerSync(context: vscode.ExtensionContext, models: ModelService): vscode.Disposable[] {
|
||||||
let feature: Promise<SyncFeature> | undefined;
|
let feature: Promise<SyncFeature> | undefined;
|
||||||
// Until the sync bundle loads, this entry is the way in; then the bundle's own one takes over.
|
|
||||||
const entry = vscode.window.createStatusBarItem('archify.sync', vscode.StatusBarAlignment.Left, 50);
|
|
||||||
entry.name = 'Archify Sync';
|
|
||||||
entry.text = '$(cloud) Archify Sync';
|
|
||||||
entry.tooltip = 'Archify Sync (optional): sign in, share a model with others or join one. Set the server under Settings › Archify › Sync.';
|
|
||||||
entry.command = 'archify.sync.menu';
|
|
||||||
// Only where there are Archify files: the extension also starts for JSON and HTML.
|
|
||||||
let archifyHere = false;
|
|
||||||
const showEntry = () => {
|
|
||||||
const wanted = archifyHere && !feature && vscode.workspace.getConfiguration('archify.sync').get<boolean>('statusBar', true);
|
|
||||||
if (wanted) entry.show();
|
|
||||||
else entry.hide();
|
|
||||||
};
|
|
||||||
void Promise.resolve(vscode.workspace.findFiles('**/*.{archify,archify-registry,archify-decision}', '{**/node_modules/**,**/.git/**}', 1)).then((found) => {
|
|
||||||
archifyHere = found.length > 0;
|
|
||||||
showEntry();
|
|
||||||
});
|
|
||||||
const load = (): Promise<SyncFeature> =>
|
const load = (): Promise<SyncFeature> =>
|
||||||
(feature ??= (async () => {
|
(feature ??= (async () => {
|
||||||
showEntry();
|
|
||||||
// A runtime path, so esbuild leaves the sync bundle out of this one.
|
// A runtime path, so esbuild leaves the sync bundle out of this one.
|
||||||
const bundle = path.join(__dirname, 'sync.js');
|
const bundle = path.join(__dirname, 'sync.js');
|
||||||
const mod = require(bundle) as typeof import('./index');
|
const mod = require(bundle) as typeof import('./index');
|
||||||
@@ -63,8 +45,6 @@ export function registerSync(context: vscode.ExtensionContext, models: ModelServ
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
entry,
|
|
||||||
vscode.workspace.onDidChangeConfiguration((e) => e.affectsConfiguration('archify.sync.statusBar') && showEntry()),
|
|
||||||
liveBridge,
|
liveBridge,
|
||||||
...COMMANDS.map((id) =>
|
...COMMANDS.map((id) =>
|
||||||
vscode.commands.registerCommand(id, async (...args: unknown[]) => (await load()).run(id, ...args)),
|
vscode.commands.registerCommand(id, async (...args: unknown[]) => (await load()).run(id, ...args)),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export type WebviewMessage =
|
|||||||
| { type: 'showModel' }
|
| { type: 'showModel' }
|
||||||
| { type: 'liveReady' }
|
| { type: 'liveReady' }
|
||||||
| { type: 'liveAction'; action: LiveAction }
|
| { type: 'liveAction'; action: LiveAction }
|
||||||
|
| { type: 'syncMenu' }
|
||||||
|
| { type: 'syncReady' }
|
||||||
| { type: 'error'; message: string };
|
| { type: 'error'; message: string };
|
||||||
|
|
||||||
/** Column for opening files next to a webview without covering it. */
|
/** Column for opening files next to a webview without covering it. */
|
||||||
@@ -66,6 +68,28 @@ async function openSource(
|
|||||||
vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`);
|
vscode.window.showWarningMessage(`Archify: source file "${message.path}" was not found in the workspace.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the file's model is shared: an .archify-sync file next to its
|
||||||
|
* main.archify. Read from disk, so the sync bundle stays unloaded.
|
||||||
|
*/
|
||||||
|
async function isShared(file: vscode.Uri): Promise<boolean> {
|
||||||
|
const folder = vscode.workspace.getWorkspaceFolder(file);
|
||||||
|
let dir = vscode.Uri.joinPath(file, '..');
|
||||||
|
for (let depth = 0; depth < 16; depth++) {
|
||||||
|
if (await exists(vscode.Uri.joinPath(dir, '.archify-sync'))) return true;
|
||||||
|
if (await exists(vscode.Uri.joinPath(dir, 'main.archify'))) return false;
|
||||||
|
const parent = vscode.Uri.joinPath(dir, '..');
|
||||||
|
if (!folder || dir.path === folder.uri.path || parent.path === dir.path) return false;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postSyncState(file: vscode.Uri, panel: vscode.WebviewPanel): Promise<void> {
|
||||||
|
const shared = file.scheme === 'file' && (await isShared(file));
|
||||||
|
await Promise.resolve(panel.webview.postMessage({ type: 'syncState', shared })).catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleWebviewMessage(
|
export async function handleWebviewMessage(
|
||||||
message: WebviewMessage,
|
message: WebviewMessage,
|
||||||
context: vscode.Uri,
|
context: vscode.Uri,
|
||||||
@@ -99,6 +123,14 @@ export async function handleWebviewMessage(
|
|||||||
case 'error':
|
case 'error':
|
||||||
vscode.window.showErrorMessage(`Archify: ${message.message}`);
|
vscode.window.showErrorMessage(`Archify: ${message.message}`);
|
||||||
break;
|
break;
|
||||||
|
case 'syncReady':
|
||||||
|
await postSyncState(context, panel);
|
||||||
|
break;
|
||||||
|
case 'syncMenu':
|
||||||
|
await vscode.commands.executeCommand('archify.sync.menu');
|
||||||
|
// Share or Stop may have changed it.
|
||||||
|
await postSyncState(context, panel);
|
||||||
|
break;
|
||||||
case 'hash':
|
case 'hash':
|
||||||
case 'openDiagram':
|
case 'openDiagram':
|
||||||
case 'showGraph':
|
case 'showGraph':
|
||||||
|
|||||||
+81
-5
@@ -208,6 +208,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
// Collapsible panels of the model page; open/closed is kept in the webview state.
|
// Collapsible panels of the model page; open/closed is kept in the webview state.
|
||||||
|
// The model page's top-level panels start closed: a model opens as its outline.
|
||||||
function panelStates() { return (vscode.getState() || {}).modelPanels || {}; }
|
function panelStates() { return (vscode.getState() || {}).modelPanels || {}; }
|
||||||
function panel(key, className, title, count, defaultOpen) {
|
function panel(key, className, title, count, defaultOpen) {
|
||||||
// count: a number or a short text such as "3 files".
|
// count: a number or a short text such as "3 files".
|
||||||
@@ -624,7 +625,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
live.hidden = true;
|
live.hidden = true;
|
||||||
body.appendChild(live);
|
body.appendChild(live);
|
||||||
var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0);
|
var count = model.groups.reduce(function (n, g) { return n + g.diagrams.length; }, 0);
|
||||||
var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, true);
|
var diagramsPanel = panel('diagrams', 'archify-model-diagrams', 'Diagrams', count, false);
|
||||||
body.appendChild(diagramsPanel);
|
body.appendChild(diagramsPanel);
|
||||||
if (!count) diagramsPanel.appendChild(el('p', 'archify-model-empty', 'No diagrams yet. Add .archify files next to main.archify or in subfolders.'));
|
if (!count) diagramsPanel.appendChild(el('p', 'archify-model-empty', 'No diagrams yet. Add .archify files next to main.archify or in subfolders.'));
|
||||||
model.groups.forEach(function (group) {
|
model.groups.forEach(function (group) {
|
||||||
@@ -658,7 +659,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
byFile[element.file].push(element);
|
byFile[element.file].push(element);
|
||||||
});
|
});
|
||||||
files.sort();
|
files.sort();
|
||||||
var registryPanel = panel('registry', 'archify-model-registry', 'Registries', files.length + (files.length === 1 ? ' file' : ' files'), true);
|
var registryPanel = panel('registry', 'archify-model-registry', 'Registries', files.length + (files.length === 1 ? ' file' : ' files'), false);
|
||||||
body.appendChild(registryPanel);
|
body.appendChild(registryPanel);
|
||||||
files.forEach(function (file) {
|
files.forEach(function (file) {
|
||||||
var elements = byFile[file].slice().sort(function (a, b) { return a.line - b.line; });
|
var elements = byFile[file].slice().sort(function (a, b) { return a.line - b.line; });
|
||||||
@@ -676,7 +677,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
}
|
}
|
||||||
// Decisions: one list, live ones first, superseded and rejected dimmed and last.
|
// Decisions: one list, live ones first, superseded and rejected dimmed and last.
|
||||||
if (model.decisions && model.decisions.length) {
|
if (model.decisions && model.decisions.length) {
|
||||||
var decisionsPanel = panel('decisions', 'archify-model-decisions', 'Decisions', model.decisions.length, true);
|
var decisionsPanel = panel('decisions', 'archify-model-decisions', 'Decisions', model.decisions.length, false);
|
||||||
body.appendChild(decisionsPanel);
|
body.appendChild(decisionsPanel);
|
||||||
var decisionList = el('ul', 'archify-model-list archify-decision-list');
|
var decisionList = el('ul', 'archify-model-list archify-decision-list');
|
||||||
model.decisions.forEach(function (row) { decisionList.appendChild(decisionRowItem(row)); });
|
model.decisions.forEach(function (row) { decisionList.appendChild(decisionRowItem(row)); });
|
||||||
@@ -723,7 +724,7 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
if (!info) { holder.hidden = true; return; }
|
if (!info) { holder.hidden = true; return; }
|
||||||
holder.hidden = false;
|
holder.hidden = false;
|
||||||
var others = info.people.filter(function (p) { return !p.me; });
|
var others = info.people.filter(function (p) { return !p.me; });
|
||||||
var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, true);
|
var livePanel = panel('live', 'archify-model-live is-' + info.state, 'Shared \u00b7 ' + info.name, others.length ? others.length + 1 + ' here' : info.host, false);
|
||||||
var status = el('div', 'archify-live-status');
|
var status = el('div', 'archify-live-status');
|
||||||
status.appendChild(el('span', 'archify-live-dot'));
|
status.appendChild(el('span', 'archify-live-dot'));
|
||||||
status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : '')));
|
status.appendChild(el('span', 'archify-live-state', (LIVE_STATE[info.state] || info.state) + (info.state === 'online' ? ' on ' + info.host : '')));
|
||||||
@@ -887,6 +888,78 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
onReady(renderDecisionsRail);
|
onReady(renderDecisionsRail);
|
||||||
onReady(describeElements);
|
onReady(describeElements);
|
||||||
onReady(renderModeSwitch);
|
onReady(renderModeSwitch);
|
||||||
|
|
||||||
|
// Archify Sync: one icon button, top right, on every page of an Archify
|
||||||
|
// file (diagram, model, graph, registry, decision). It opens the Sync menu.
|
||||||
|
function renderSyncButton() {
|
||||||
|
if (!(config.source || config.model || config.registry || config.decision || config.graph)) return;
|
||||||
|
if (document.getElementById('archify-sync-button')) return;
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.textContent = SYNC_BUTTON_STYLE;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
var button = document.createElement('button');
|
||||||
|
button.id = 'archify-sync-button';
|
||||||
|
button.type = 'button';
|
||||||
|
button.title = 'Archify Sync: sign in, share this model with others, or join one';
|
||||||
|
button.addEventListener('click', function (event) { event.stopPropagation(); post({ type: 'syncMenu' }); });
|
||||||
|
// Last in the row that holds the view switch: Archify's toolbar on a
|
||||||
|
// diagram (after Export), the Graph page's header actions (after Export,
|
||||||
|
// built a moment later by graph.js), next to the switch on the other
|
||||||
|
// pages. It floats only while there is no such row.
|
||||||
|
document.body.appendChild(button);
|
||||||
|
function place() {
|
||||||
|
var modeSwitch = document.getElementById('archify-mode-switch');
|
||||||
|
var row = document.querySelector('.ag-actions') || (modeSwitch && modeSwitch.parentElement) || document.querySelector('body > .toolbar');
|
||||||
|
if (!row || row === document.body) return false;
|
||||||
|
button.classList.add('in-toolbar');
|
||||||
|
row.classList.add('archify-sync-row');
|
||||||
|
row.appendChild(button);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!place() && window.MutationObserver) {
|
||||||
|
var watch = new MutationObserver(function () { if (place()) watch.disconnect(); });
|
||||||
|
watch.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
showSyncState(false);
|
||||||
|
post({ type: 'syncReady' });
|
||||||
|
}
|
||||||
|
// Not shared: a cloud. Shared (the model has an .archify-sync file): broadcast waves on green.
|
||||||
|
var SYNC_ICONS = {
|
||||||
|
off: '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12.5 13H4.25a3.25 3.25 0 0 1-.6-6.44 4.5 4.5 0 0 1 8.7-.81A3.6 3.6 0 0 1 12.5 13Zm-8.25-1h8.25a2.6 2.6 0 0 0 0-5.2h-.5l-.12-.39a3.5 3.5 0 0 0-6.82.66l-.04.4-.4.05A2.25 2.25 0 0 0 4.25 12Z"/></svg>',
|
||||||
|
on: '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><circle cx="8" cy="8" r="1.75" fill="currentColor"/><path fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" d="M5.2 5.2a4 4 0 0 0 0 5.6M10.8 5.2a4 4 0 0 1 0 5.6M3.1 3.1a7 7 0 0 0 0 9.8M12.9 3.1a7 7 0 0 1 0 9.8"/></svg>',
|
||||||
|
};
|
||||||
|
function showSyncState(shared) {
|
||||||
|
var button = document.getElementById('archify-sync-button');
|
||||||
|
if (!button) return;
|
||||||
|
button.classList.toggle('is-shared', !!shared);
|
||||||
|
button.innerHTML = shared ? SYNC_ICONS.on : SYNC_ICONS.off;
|
||||||
|
button.title = shared
|
||||||
|
? 'Archify Sync: this model is shared. Invite people, publish a version, see versions'
|
||||||
|
: 'Archify Sync: sign in, share this model with others, or join one';
|
||||||
|
button.setAttribute('aria-label', shared ? 'Archify Sync (shared)' : 'Archify Sync');
|
||||||
|
}
|
||||||
|
window.addEventListener('message', function (event) {
|
||||||
|
var message = event.data || {};
|
||||||
|
if (message.type === 'syncState') showSyncState(message.shared);
|
||||||
|
});
|
||||||
|
// Every rule is !important: the page's own button styles (Archify's toolbar,
|
||||||
|
// VS Code's high contrast themes) must not resize or recolour it.
|
||||||
|
var SYNC_BUTTON_STYLE = '#archify-sync-button { all: unset; position: fixed !important; top: 12px !important; right: 28px !important; z-index: 1001 !important;'
|
||||||
|
+ ' box-sizing: border-box !important; width: 30px !important; height: 30px !important; min-width: 0 !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important;'
|
||||||
|
+ ' display: flex !important; align-items: center !important; justify-content: center !important; border-radius: 8px !important; cursor: pointer !important;'
|
||||||
|
+ ' color: #fff !important; background: var(--vscode-button-background, #0e639c) !important; border: 1px solid var(--vscode-contrastBorder, transparent) !important;'
|
||||||
|
+ ' box-shadow: 0 1px 4px rgba(0,0,0,.35) !important; }'
|
||||||
|
+ ' .archify-model-switch-row.archify-sync-row { display: flex !important; align-items: center !important; gap: 0.5rem !important; }'
|
||||||
|
+ ' #archify-sync-button.in-toolbar { position: relative !important; top: auto !important; right: auto !important; align-self: center !important; flex: none !important; width: 36px !important; height: 36px !important; }'
|
||||||
|
+ ' .archify-model-switch-row #archify-sync-button.in-toolbar { width: 33px !important; height: 33px !important; }'
|
||||||
|
+ ' .ag-actions #archify-sync-button.in-toolbar { width: 29px !important; height: 29px !important; }'
|
||||||
|
+ ' body.vscode-high-contrast #archify-sync-button:not(.is-shared) { background: #0e639c !important; }'
|
||||||
|
+ ' #archify-sync-button svg { display: block !important; width: 16px !important; height: 16px !important; flex: none !important; }'
|
||||||
|
+ ' #archify-sync-button:hover { background: var(--vscode-button-hoverBackground, #1177bb) !important; }'
|
||||||
|
+ ' #archify-sync-button.is-shared { background: #1f8f5f !important; }'
|
||||||
|
+ ' #archify-sync-button.is-shared:hover { background: #23a36c !important; }'
|
||||||
|
+ ' #archify-sync-button:focus-visible { outline: 2px solid var(--vscode-focusBorder, #007fd4) !important; outline-offset: 2px !important; }';
|
||||||
|
onReady(renderSyncButton);
|
||||||
if (config.source) {
|
if (config.source) {
|
||||||
// The host answers with the problem count for the Source badge.
|
// The host answers with the problem count for the Source badge.
|
||||||
post({ type: 'sourceReady' });
|
post({ type: 'sourceReady' });
|
||||||
@@ -1013,10 +1086,13 @@ const MODEL_STYLE = `
|
|||||||
/* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */
|
/* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */
|
||||||
const SWITCH_STYLE = `
|
const SWITCH_STYLE = `
|
||||||
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; }
|
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); position: relative; }
|
||||||
|
.toolbar .archify-mode-switch { height: 2.75rem; box-sizing: border-box; }
|
||||||
.toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
|
.toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
|
||||||
.toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
|
.toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
|
||||||
/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */
|
/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */
|
||||||
#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; min-height: 0; margin: 0; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
||||||
|
/* Archify's toolbar buttons are 2.75rem tall with a painted ::before; the segments are not, like 2D | 3D. */
|
||||||
|
#archify-mode-switch .archify-mode-seg::before, #archify-mode-switch .archify-mode-seg::after { content: none; }
|
||||||
#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
|
#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
|
||||||
#archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); }
|
#archify-mode-switch .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); }
|
||||||
#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; }
|
#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; }
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type HostMessage =
|
|||||||
|
|
||||||
const STYLE = `
|
const STYLE = `
|
||||||
html.archify-source-on .container > :not(.header):not(#archify-source),
|
html.archify-source-on .container > :not(.header):not(#archify-source),
|
||||||
html.archify-source-on body > :not(.toolbar):not(.container):not(.archify-model-head):not(.archify-model-switch-row):not(#archify-source):not(script):not(style):not(.archify-modal-backdrop),
|
html.archify-source-on body > :not(.toolbar):not(.container):not(.archify-model-head):not(.archify-model-switch-row):not(#archify-source):not(#archify-sync-button):not(script):not(style):not(.archify-modal-backdrop),
|
||||||
html.archify-source-on #btn-3d,
|
html.archify-source-on #btn-3d,
|
||||||
html.archify-source-on .archify-vscode-notice { display: none !important; }
|
html.archify-source-on .archify-vscode-notice { display: none !important; }
|
||||||
html:not(.archify-source-on) #archify-source { display: none !important; }
|
html:not(.archify-source-on) #archify-source { display: none !important; }
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Technical context
|
||||||
|
output: technical-context.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: usher
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tickets
|
||||||
|
pos: [290, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: psp
|
||||||
|
pos: [560, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: mail
|
||||||
|
pos: [560, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: pos
|
||||||
|
pos: [560, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: tickets, label: HTTPS JSON, variant: emphasis }
|
||||||
|
- { from: usher, to: tickets, label: HTTPS app }
|
||||||
|
- { from: tickets, to: psp, label: REST webhook, variant: security }
|
||||||
|
- { from: tickets, to: mail, label: SMTP TLS }
|
||||||
|
- { from: tickets, to: pos, label: REST VPN }
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Booking API - level 2
|
||||||
|
output: booking.html
|
||||||
|
decisions: [seat-holds-with-ttl]
|
||||||
|
components:
|
||||||
|
- ref: web
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: programme
|
||||||
|
type: backend
|
||||||
|
label: Programme
|
||||||
|
sublabel: shows, halls
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: seats
|
||||||
|
type: backend
|
||||||
|
label: Seat Holds
|
||||||
|
sublabel: lock, release
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: checkout
|
||||||
|
type: backend
|
||||||
|
label: Checkout
|
||||||
|
sublabel: orders, webhook
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: seatlock
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [seat-holds-with-ttl]
|
||||||
|
- ref: db
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: psp
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [150, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Booking API
|
||||||
|
wraps: [programme, seats, checkout]
|
||||||
|
connections:
|
||||||
|
- { from: web, to: programme, label: browse }
|
||||||
|
- { from: web, to: seats, label: hold, variant: emphasis }
|
||||||
|
- { from: web, to: checkout, label: pay }
|
||||||
|
- { from: programme, to: db, label: SQL }
|
||||||
|
- { from: seats, to: seatlock, label: SET NX, decisions: [seat-holds-with-ttl] }
|
||||||
|
- { from: checkout, to: psp, label: session, variant: security }
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Building blocks - level 1
|
||||||
|
output: level-1.html
|
||||||
|
decisions: [booking-owns-seat-inventory]
|
||||||
|
components:
|
||||||
|
- ref: web
|
||||||
|
pos: [40, 110]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mobile
|
||||||
|
pos: [40, 210]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: scanner
|
||||||
|
pos: [40, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: booking
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: seatlock
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [460, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ticketing
|
||||||
|
pos: [250, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: possync
|
||||||
|
pos: [670, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: psp
|
||||||
|
pos: [900, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: pos
|
||||||
|
pos: [900, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: mail
|
||||||
|
pos: [900, 460]
|
||||||
|
size: [150, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Cinema Tickets
|
||||||
|
wraps: [web, mobile, scanner, booking, seatlock, db, ticketing, possync]
|
||||||
|
connections:
|
||||||
|
- { from: web, to: booking, label: JSON, variant: emphasis }
|
||||||
|
- { from: mobile, to: booking, label: JSON }
|
||||||
|
- { from: booking, to: seatlock, label: hold }
|
||||||
|
- { from: booking, to: db, label: SQL }
|
||||||
|
- { from: booking, to: psp, label: checkout, variant: security }
|
||||||
|
- { from: booking, to: ticketing, label: issue, labelAt: [315, 234] }
|
||||||
|
- { from: scanner, to: ticketing, label: check-in }
|
||||||
|
- { from: ticketing, to: db, label: SQL }
|
||||||
|
- { from: ticketing, to: mail, label: QR mail }
|
||||||
|
- { from: possync, to: db, label: SQL }
|
||||||
|
- { from: possync, to: pos, label: sales, decisions: [booking-owns-seat-inventory] }
|
||||||
|
cards:
|
||||||
|
- dot: amber
|
||||||
|
title: 8 Crosscutting concepts
|
||||||
|
items:
|
||||||
|
- "Times in UTC; shown in cinema local time"
|
||||||
|
- "Every write carries an idempotency key"
|
||||||
|
- "QR payload signed with an Ed25519 key"
|
||||||
|
- "Structured logs with order id as trace key"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: sequence
|
||||||
|
meta:
|
||||||
|
title: Runtime - book seats
|
||||||
|
output: book-seats.html
|
||||||
|
viewBox: [1060, 680]
|
||||||
|
column_fit: spread
|
||||||
|
decisions: [seat-holds-with-ttl]
|
||||||
|
participants:
|
||||||
|
- { ref: customer }
|
||||||
|
- { ref: web }
|
||||||
|
- { ref: booking }
|
||||||
|
- { ref: seatlock }
|
||||||
|
- { ref: psp }
|
||||||
|
- { ref: ticketing }
|
||||||
|
- { ref: mail }
|
||||||
|
messages:
|
||||||
|
- { from: customer, to: web, y: 170, label: pick seats, variant: emphasis }
|
||||||
|
- { from: web, to: booking, y: 214, label: POST /holds }
|
||||||
|
- { from: booking, to: seatlock, y: 258, label: SET NX TTL 8m, decisions: [seat-holds-with-ttl] }
|
||||||
|
- { from: seatlock, to: booking, y: 302, label: OK, variant: return }
|
||||||
|
- { from: booking, to: web, y: 346, label: hold id, variant: return }
|
||||||
|
- { from: web, to: psp, y: 390, label: hosted checkout, variant: security }
|
||||||
|
- { from: psp, to: booking, y: 434, label: webhook paid, variant: security }
|
||||||
|
- { from: booking, to: ticketing, y: 478, label: issue tickets }
|
||||||
|
- { from: ticketing, to: mail, y: 522, label: QR e-mail }
|
||||||
|
- { from: mail, to: customer, y: 566, label: tickets, variant: return }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: sequence
|
||||||
|
meta:
|
||||||
|
title: Runtime - check-in at the door
|
||||||
|
output: check-in.html
|
||||||
|
viewBox: [900, 480]
|
||||||
|
column_fit: spread
|
||||||
|
decisions: [signed-qr-codes]
|
||||||
|
participants:
|
||||||
|
- { ref: usher }
|
||||||
|
- { ref: scanner }
|
||||||
|
- { ref: ticketing }
|
||||||
|
- { ref: db }
|
||||||
|
messages:
|
||||||
|
- { from: usher, to: scanner, y: 180, label: scan QR, variant: emphasis, note: signature checked offline, decisions: [signed-qr-codes] }
|
||||||
|
- { from: scanner, to: ticketing, y: 230, label: POST /checkins }
|
||||||
|
- { from: ticketing, to: db, y: 280, label: first scan wins }
|
||||||
|
- { from: ticketing, to: scanner, y: 330, label: valid seat 7F, variant: return }
|
||||||
|
- { from: scanner, to: usher, y: 380, label: green light, variant: return }
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Deployment - production
|
||||||
|
output: production.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: ingress
|
||||||
|
type: security
|
||||||
|
label: Ingress
|
||||||
|
sublabel: TLS, WAF
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: web
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: booking
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ticketing
|
||||||
|
pos: [460, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: possync
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: seatlock
|
||||||
|
pos: [710, 110]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [710, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: pos
|
||||||
|
pos: [250, 460]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: scanner
|
||||||
|
pos: [40, 460]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: EU cloud - k8s cluster
|
||||||
|
wraps: [ingress, web, booking, ticketing, possync]
|
||||||
|
- kind: security-group
|
||||||
|
label: Managed data zone
|
||||||
|
wraps: [seatlock, db]
|
||||||
|
- kind: region
|
||||||
|
label: Cinema site
|
||||||
|
wraps: [pos, scanner]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: ingress, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: ingress, to: web, label: route }
|
||||||
|
- { from: ingress, to: booking, label: route }
|
||||||
|
- { from: booking, to: seatlock, label: TLS }
|
||||||
|
- { from: booking, to: db, label: TLS }
|
||||||
|
- { from: ticketing, to: db, label: TLS }
|
||||||
|
- { from: possync, to: pos, label: VPN, variant: security }
|
||||||
|
- { from: scanner, to: ingress, label: HTTPS }
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: seat-holds-with-ttl
|
||||||
|
title: Seat holds as Redis keys with a TTL
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-08
|
||||||
|
elements: [booking, seatlock]
|
||||||
|
diagrams: [0500-building-blocks/booking.archify, 0600-runtime/book-seats.archify]
|
||||||
|
context: >
|
||||||
|
A customer may spend minutes at the payment page. Holding seats
|
||||||
|
as rows in the database left orphans when a browser closed, and
|
||||||
|
a nightly job freed them far too late for Friday shows.
|
||||||
|
decision: >
|
||||||
|
A hold is one Redis key per seat, set with SET NX and an
|
||||||
|
8-minute TTL. The payment webhook turns holds into an order in
|
||||||
|
PostgreSQL; an expired key frees the seat without any job.
|
||||||
|
consequences: >
|
||||||
|
Holds survive no Redis restart; at worst a customer re-picks
|
||||||
|
seats. The webhook must check the hold still exists before it
|
||||||
|
confirms the order, otherwise it refunds.
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: booking-owns-seat-inventory
|
||||||
|
title: The Booking API owns the seat inventory
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-22
|
||||||
|
elements: [booking, db, possync, pos]
|
||||||
|
diagrams: [0500-building-blocks/level-1.archify]
|
||||||
|
context: >
|
||||||
|
The box-office POS and the web shop both sold seats from their
|
||||||
|
own copy of the seat map. Twice a month a seat was sold at the
|
||||||
|
counter and online.
|
||||||
|
decision: >
|
||||||
|
The Tickets DB is the only seat inventory. The POS asks POS Sync
|
||||||
|
for a seat before it prints a ticket; POS Sync writes the sale
|
||||||
|
through the same rules as online orders.
|
||||||
|
consequences: >
|
||||||
|
The counter needs the network to sell; a fallback block of
|
||||||
|
reserved seats per show covers outages. The POS vendor adapter
|
||||||
|
is ours to maintain.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: signed-qr-codes
|
||||||
|
title: Signed QR codes checked offline at the door
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-06
|
||||||
|
elements: [ticketing, scanner]
|
||||||
|
diagrams: [0600-runtime/check-in.archify]
|
||||||
|
context: >
|
||||||
|
Cinema Wi-Fi drops when a full hall arrives. A scanner that
|
||||||
|
needs the server for every ticket stops the queue.
|
||||||
|
decision: >
|
||||||
|
The Ticket Service signs each QR payload (ticket id, show,
|
||||||
|
seat) with a private key. The scanner app verifies it with the
|
||||||
|
public key and queues check-ins until it is online again.
|
||||||
|
consequences: >
|
||||||
|
A forwarded ticket scans twice while offline; the second scan
|
||||||
|
is flagged when the queue syncs. Key rotation needs an app
|
||||||
|
update window.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: cinema-tickets
|
||||||
|
name: Cinema Tickets
|
||||||
|
description: >
|
||||||
|
An arc42 example: online and mobile seat booking for a small
|
||||||
|
cinema chain. This overview is section 3 (business context) with
|
||||||
|
cards for sections 1, 2, 10, 11 and 12. 0300-context holds the
|
||||||
|
technical context (3), 0500-building-blocks the building block
|
||||||
|
view (5), 0600-runtime the runtime view (6), 0700-deployment the
|
||||||
|
deployment view (7), decisions/ the architecture decisions (9).
|
||||||
|
Crosscutting concepts (8) are a card on the level-1 diagram.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Cinema Tickets - business context
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: usher
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tickets
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: psp
|
||||||
|
pos: [540, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: mail
|
||||||
|
pos: [540, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: pos
|
||||||
|
pos: [540, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: tickets, label: books seats, variant: emphasis }
|
||||||
|
- { from: usher, to: tickets, label: checks in }
|
||||||
|
- { from: tickets, to: psp, label: payments, variant: security }
|
||||||
|
- { from: tickets, to: mail, label: tickets }
|
||||||
|
- { from: tickets, to: pos, label: seat sales }
|
||||||
|
cards:
|
||||||
|
- dot: cyan
|
||||||
|
title: 1 Quality goals
|
||||||
|
items:
|
||||||
|
- "No seat is ever sold twice"
|
||||||
|
- "Checkout under 2 s at Friday peak"
|
||||||
|
- "Door check-in works offline"
|
||||||
|
- dot: slate
|
||||||
|
title: 2 Constraints
|
||||||
|
items:
|
||||||
|
- "Existing box-office POS stays"
|
||||||
|
- "Card data only at the provider"
|
||||||
|
- "Team of four; hosted in the EU"
|
||||||
|
- dot: emerald
|
||||||
|
title: 10 Quality scenarios
|
||||||
|
items:
|
||||||
|
- "500 holds on one show in 1 min: no double sale"
|
||||||
|
- "Wi-Fi down at the door: scans go on, sync later"
|
||||||
|
- "Payment fails: seats free again within 8 min"
|
||||||
|
- dot: rose
|
||||||
|
title: 11 Risks
|
||||||
|
items:
|
||||||
|
- "POS vendor API is slow and undocumented"
|
||||||
|
- "Forwarded QR e-mails: first scan wins"
|
||||||
|
- dot: violet
|
||||||
|
title: 12 Glossary
|
||||||
|
items:
|
||||||
|
- "Show: one screening of a film in a hall"
|
||||||
|
- "Hold: a seat locked for one customer"
|
||||||
|
- "Check-in: first valid scan of a ticket"
|
||||||
|
- "POS: point-of-sale till at the box office"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
web:
|
||||||
|
type: frontend
|
||||||
|
label: Web Shop
|
||||||
|
sublabel: React
|
||||||
|
brand: react
|
||||||
|
description: >
|
||||||
|
Programme, seat map and checkout in the browser.
|
||||||
|
mobile:
|
||||||
|
type: frontend
|
||||||
|
label: Mobile App
|
||||||
|
sublabel: iOS and Android
|
||||||
|
description: >
|
||||||
|
The same booking flow as the web shop, plus a ticket wallet.
|
||||||
|
scanner:
|
||||||
|
type: frontend
|
||||||
|
label: Scanner App
|
||||||
|
sublabel: door phones
|
||||||
|
description: >
|
||||||
|
Reads QR codes at the door; checks signatures offline.
|
||||||
|
booking:
|
||||||
|
type: backend
|
||||||
|
label: Booking API
|
||||||
|
sublabel: Node.js
|
||||||
|
brand: node-js
|
||||||
|
description: >
|
||||||
|
Shows, seat maps, seat holds and orders; the only writer of
|
||||||
|
the seat inventory.
|
||||||
|
diagram: 0500-building-blocks/booking.archify
|
||||||
|
ticketing:
|
||||||
|
type: backend
|
||||||
|
label: Ticket Service
|
||||||
|
sublabel: QR, check-in
|
||||||
|
description: >
|
||||||
|
Issues signed QR tickets, sends them by e-mail and records
|
||||||
|
check-ins at the door.
|
||||||
|
possync:
|
||||||
|
type: backend
|
||||||
|
label: POS Sync
|
||||||
|
sublabel: adapter
|
||||||
|
description: >
|
||||||
|
Exchanges seat sales with the box-office POS in both directions.
|
||||||
|
seatlock:
|
||||||
|
type: database
|
||||||
|
label: Seat Locks
|
||||||
|
sublabel: Redis
|
||||||
|
brand: redis
|
||||||
|
description: >
|
||||||
|
Short-lived seat holds with a TTL while the customer pays.
|
||||||
|
db:
|
||||||
|
type: database
|
||||||
|
label: Tickets DB
|
||||||
|
sublabel: PostgreSQL
|
||||||
|
brand: postgresql
|
||||||
|
description: >
|
||||||
|
Shows, seats, orders and tickets; the source of truth.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
customer:
|
||||||
|
type: external
|
||||||
|
label: Moviegoer
|
||||||
|
sublabel: web and mobile
|
||||||
|
description: >
|
||||||
|
Picks a show, holds seats and pays; gets the tickets by e-mail.
|
||||||
|
usher:
|
||||||
|
type: external
|
||||||
|
label: Usher
|
||||||
|
sublabel: door staff
|
||||||
|
description: >
|
||||||
|
Scans tickets at the auditorium door with the scanner app.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
tickets:
|
||||||
|
type: cloud
|
||||||
|
label: Cinema Tickets
|
||||||
|
sublabel: the system
|
||||||
|
description: >
|
||||||
|
Online booking, seat holds, e-mail tickets and door check-in
|
||||||
|
for the five cinemas of the fictional Lantern Cinemas chain.
|
||||||
|
diagram: 0500-building-blocks/level-1.archify
|
||||||
|
psp:
|
||||||
|
type: external
|
||||||
|
label: Payment provider
|
||||||
|
sublabel: hosted checkout
|
||||||
|
description: >
|
||||||
|
Takes card and wallet payments on its own page; we never see
|
||||||
|
card numbers.
|
||||||
|
pos:
|
||||||
|
type: external
|
||||||
|
label: Box-office POS
|
||||||
|
sublabel: in each cinema
|
||||||
|
description: >
|
||||||
|
Till system at the counter; sells walk-in seats from the same
|
||||||
|
seat map.
|
||||||
|
mail:
|
||||||
|
type: external
|
||||||
|
label: E-mail service
|
||||||
|
sublabel: transactional
|
||||||
|
description: >
|
||||||
|
Delivers ticket e-mails with QR codes and receipts.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Technical context
|
||||||
|
output: technical-context.html
|
||||||
|
decisions: [mqtt-with-mutual-tls]
|
||||||
|
components:
|
||||||
|
- ref: homeowner
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ops-engineer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: installer
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: telemetry-platform
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: heat-pump
|
||||||
|
pos: [540, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: push-service
|
||||||
|
pos: [540, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: service-desk
|
||||||
|
pos: [540, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: homeowner, to: telemetry-platform, label: HTTPS }
|
||||||
|
- { from: ops-engineer, to: telemetry-platform, label: HTTPS, variant: security }
|
||||||
|
- { from: installer, to: telemetry-platform, label: HTTPS }
|
||||||
|
- { from: heat-pump, to: telemetry-platform, label: MQTT 8883, variant: emphasis, decisions: [mqtt-with-mutual-tls] }
|
||||||
|
- { from: telemetry-platform, to: push-service, label: HTTP/2 }
|
||||||
|
- { from: telemetry-platform, to: service-desk, label: webhook }
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Ingest, level 2
|
||||||
|
output: ingest.html
|
||||||
|
components:
|
||||||
|
- ref: mqtt-broker
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: decoder
|
||||||
|
type: backend
|
||||||
|
label: Decoder
|
||||||
|
sublabel: payload v1, v2
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: validator
|
||||||
|
type: backend
|
||||||
|
label: Validator
|
||||||
|
sublabel: ranges, clock
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: writer
|
||||||
|
type: backend
|
||||||
|
label: Batch writer
|
||||||
|
sublabel: 500 rows
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: dead-letter
|
||||||
|
type: database
|
||||||
|
label: Dead letters
|
||||||
|
sublabel: rejected readings
|
||||||
|
pos: [460, 310]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tsdb
|
||||||
|
pos: [880, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: rules
|
||||||
|
pos: [670, 310]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Ingest
|
||||||
|
wraps: [decoder, validator, writer, dead-letter]
|
||||||
|
connections:
|
||||||
|
- { from: mqtt-broker, to: decoder, label: messages, variant: emphasis }
|
||||||
|
- { from: decoder, to: validator, label: readings, variant: emphasis }
|
||||||
|
- { from: validator, to: writer, label: valid, variant: emphasis }
|
||||||
|
- { from: validator, to: dead-letter, label: invalid, variant: dashed, labelAt: [525, 234] }
|
||||||
|
- { from: writer, to: tsdb, label: COPY, variant: emphasis }
|
||||||
|
- { from: writer, to: rules, label: stream, labelAt: [735, 234] }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Building blocks, level 1
|
||||||
|
output: level-1.html
|
||||||
|
decisions: [timescaledb-for-telemetry]
|
||||||
|
components:
|
||||||
|
- ref: heat-pump
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: installer
|
||||||
|
pos: [40, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mqtt-broker
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [mqtt-with-mutual-tls]
|
||||||
|
- ref: provisioning
|
||||||
|
pos: [250, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: rules
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ingest
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: device-registry
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tsdb
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: device-api
|
||||||
|
pos: [670, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ops-dashboard
|
||||||
|
pos: [880, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: customer-app
|
||||||
|
pos: [880, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: push-service
|
||||||
|
pos: [1090, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ops-engineer
|
||||||
|
pos: [1090, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: homeowner
|
||||||
|
pos: [1090, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: IoT Telemetry
|
||||||
|
wraps: [mqtt-broker, provisioning, rules, ingest, device-registry, tsdb, device-api, ops-dashboard, customer-app]
|
||||||
|
connections:
|
||||||
|
- { from: heat-pump, to: mqtt-broker, label: publish, variant: emphasis, decisions: [mqtt-with-mutual-tls] }
|
||||||
|
- { from: mqtt-broker, to: ingest, label: subscribe, variant: emphasis }
|
||||||
|
- { from: ingest, to: tsdb, label: write, variant: emphasis }
|
||||||
|
- { from: ingest, to: rules, label: stream }
|
||||||
|
- { from: ingest, to: device-registry, label: lookup, labelAt: [525, 234] }
|
||||||
|
- { from: rules, to: push-service, label: alert }
|
||||||
|
- { from: installer, to: provisioning, label: register }
|
||||||
|
- { from: provisioning, to: mqtt-broker, label: certs, variant: security }
|
||||||
|
- { from: provisioning, to: device-registry, label: store }
|
||||||
|
- { from: device-api, to: tsdb, label: read }
|
||||||
|
- { from: device-api, to: device-registry, label: read }
|
||||||
|
- { from: tsdb, to: ops-dashboard, label: query }
|
||||||
|
- { from: customer-app, to: device-api, label: REST }
|
||||||
|
- { from: ops-engineer, to: ops-dashboard, label: views }
|
||||||
|
- { from: homeowner, to: customer-app, label: uses }
|
||||||
|
cards:
|
||||||
|
- dot: amber
|
||||||
|
title: 8 Crosscutting concepts
|
||||||
|
items:
|
||||||
|
- Device identity, one X.509 certificate per device
|
||||||
|
- Idempotent writes, keyed by device id and timestamp
|
||||||
|
- Observability, metrics and traces on every service
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: sequence
|
||||||
|
meta:
|
||||||
|
title: Device provisioning
|
||||||
|
output: device-provisioning.html
|
||||||
|
decisions: [mqtt-with-mutual-tls]
|
||||||
|
participants:
|
||||||
|
- { ref: installer }
|
||||||
|
- { ref: provisioning }
|
||||||
|
- { ref: device-registry, label: Devices }
|
||||||
|
- { ref: heat-pump }
|
||||||
|
- { ref: mqtt-broker }
|
||||||
|
messages:
|
||||||
|
- { from: installer, to: provisioning, y: 160, label: register serial, variant: emphasis }
|
||||||
|
- { from: provisioning, to: device-registry, y: 210, label: store device }
|
||||||
|
- { from: provisioning, to: installer, y: 260, label: certificate, variant: return }
|
||||||
|
- { from: installer, to: heat-pump, y: 310, label: load certificate }
|
||||||
|
- { from: heat-pump, to: mqtt-broker, y: 360, label: connect mTLS, variant: emphasis }
|
||||||
|
- { from: mqtt-broker, to: provisioning, y: 410, label: check revocation }
|
||||||
|
- { from: mqtt-broker, to: heat-pump, y: 460, label: CONNACK, variant: return }
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: sequence
|
||||||
|
meta:
|
||||||
|
title: Fault reading to alert
|
||||||
|
output: telemetry-to-alert.html
|
||||||
|
participants:
|
||||||
|
- { ref: heat-pump }
|
||||||
|
- { ref: mqtt-broker }
|
||||||
|
- { ref: ingest }
|
||||||
|
- { ref: tsdb, label: TSDB }
|
||||||
|
- { ref: rules }
|
||||||
|
- { ref: push-service }
|
||||||
|
messages:
|
||||||
|
- { from: heat-pump, to: mqtt-broker, y: 160, label: publish fault, variant: emphasis }
|
||||||
|
- { from: mqtt-broker, to: ingest, y: 210, label: deliver QoS 1 }
|
||||||
|
- { from: ingest, to: tsdb, y: 260, label: write batch }
|
||||||
|
- { from: tsdb, to: ingest, y: 310, label: committed, variant: return }
|
||||||
|
- { from: ingest, to: mqtt-broker, y: 360, label: ack, variant: return }
|
||||||
|
- { from: ingest, to: rules, y: 410, label: reading }
|
||||||
|
- { from: rules, to: push-service, y: 460, label: send alert, variant: emphasis }
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Deployment, production
|
||||||
|
output: production.html
|
||||||
|
decisions: [timescaledb-for-telemetry]
|
||||||
|
components:
|
||||||
|
- ref: heat-pump
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mqtt-broker
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ingest
|
||||||
|
pos: [490, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: rules
|
||||||
|
pos: [490, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: provisioning
|
||||||
|
pos: [280, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: device-api
|
||||||
|
pos: [490, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tsdb
|
||||||
|
pos: [760, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: device-registry
|
||||||
|
pos: [760, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Kubernetes cluster, eu-central
|
||||||
|
wraps: [mqtt-broker, ingest, rules, provisioning, device-api]
|
||||||
|
- kind: security-group
|
||||||
|
label: Managed DB zone
|
||||||
|
wraps: [tsdb, device-registry]
|
||||||
|
connections:
|
||||||
|
- { from: heat-pump, to: mqtt-broker, label: "8883", variant: emphasis }
|
||||||
|
- { from: mqtt-broker, to: ingest, label: in-cluster }
|
||||||
|
- { from: ingest, to: tsdb, label: "5432", variant: emphasis }
|
||||||
|
- { from: ingest, to: rules, label: gRPC }
|
||||||
|
- { from: device-api, to: device-registry, label: "5432" }
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: mqtt-with-mutual-tls
|
||||||
|
title: MQTT with mutual TLS for device traffic
|
||||||
|
status: accepted
|
||||||
|
date: 2026-08-12
|
||||||
|
elements: [heat-pump, mqtt-broker, provisioning]
|
||||||
|
diagrams: [0300-context/technical-context.archify, 0500-building-blocks/level-1.archify]
|
||||||
|
context: >
|
||||||
|
Heat pumps sit behind home routers on slow and flaky links.
|
||||||
|
The first prototype polled an HTTPS endpoint and drained
|
||||||
|
mobile data plans; shared API keys leaked from one device.
|
||||||
|
decision: >
|
||||||
|
Devices publish over MQTT 3.1.1 on port 8883. Each device
|
||||||
|
has its own client certificate issued by Provisioning;
|
||||||
|
the broker rejects any connection without one.
|
||||||
|
consequences: >
|
||||||
|
Small messages and one long-lived connection per device.
|
||||||
|
Certificates must be rotated and revoked, so Provisioning
|
||||||
|
is a critical building block. Installers need a step to
|
||||||
|
register the device before it can connect.
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: timescaledb-for-telemetry
|
||||||
|
title: TimescaleDB for telemetry
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-03
|
||||||
|
elements: [tsdb, ingest, ops-dashboard]
|
||||||
|
diagrams: [0500-building-blocks/level-1.archify, 0700-deployment/production.archify]
|
||||||
|
context: >
|
||||||
|
The fleet sends about 4,000 readings a second. A plain
|
||||||
|
PostgreSQL table grew too fast and dashboards slowed down
|
||||||
|
after a few weeks of data.
|
||||||
|
decision: >
|
||||||
|
Store telemetry in TimescaleDB hypertables with hourly
|
||||||
|
continuous aggregates. Raw data is kept 90 days, rollups
|
||||||
|
10 years. The team already runs PostgreSQL.
|
||||||
|
consequences: >
|
||||||
|
SQL stays the query language for the API and the
|
||||||
|
dashboard. Retention jobs must be monitored. Very high
|
||||||
|
device counts may need sharding later (see risk R1).
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: iot-telemetry
|
||||||
|
name: IoT Telemetry
|
||||||
|
description: >
|
||||||
|
An arc42 example: a fictional heat pump maker collects device telemetry
|
||||||
|
over MQTT, stores it, raises alerts and shows it to operations and customers.
|
||||||
|
main.archify holds section 3 business context with cards for sections
|
||||||
|
1 goals, 2 constraints, 10 quality, 11 risks and 12 glossary;
|
||||||
|
0300-context the technical context; 0500-building-blocks section 5;
|
||||||
|
0600-runtime section 6; 0700-deployment section 7; decisions/ section 9;
|
||||||
|
the crosscutting concepts of section 8 are a card on level-1.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Business context
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: homeowner
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: ops-engineer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: installer
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: telemetry-platform
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: heat-pump
|
||||||
|
pos: [540, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: push-service
|
||||||
|
pos: [540, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: service-desk
|
||||||
|
pos: [540, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: homeowner, to: telemetry-platform, label: checks }
|
||||||
|
- { from: ops-engineer, to: telemetry-platform, label: monitors }
|
||||||
|
- { from: installer, to: telemetry-platform, label: registers }
|
||||||
|
- { from: heat-pump, to: telemetry-platform, label: telemetry, variant: emphasis }
|
||||||
|
- { from: telemetry-platform, to: push-service, label: alerts }
|
||||||
|
- { from: telemetry-platform, to: service-desk, label: tickets }
|
||||||
|
cards:
|
||||||
|
- dot: cyan
|
||||||
|
title: 1 Quality goals
|
||||||
|
items:
|
||||||
|
- Reliability, no telemetry lost once accepted
|
||||||
|
- Timeliness, alert within 60 s of a fault
|
||||||
|
- Security, every device authenticated
|
||||||
|
- dot: slate
|
||||||
|
title: 2 Constraints
|
||||||
|
items:
|
||||||
|
- Devices speak MQTT only; firmware by a partner
|
||||||
|
- Data stays in one EU region
|
||||||
|
- Team of six; PostgreSQL skills in house
|
||||||
|
- dot: emerald
|
||||||
|
title: 10 Quality scenarios
|
||||||
|
items:
|
||||||
|
- A fault code reaches the homeowner within 60 s
|
||||||
|
- Broker restart, devices reconnect, no data lost
|
||||||
|
- A stolen certificate is revoked within 15 min
|
||||||
|
- dot: rose
|
||||||
|
title: 11 Risks
|
||||||
|
items:
|
||||||
|
- R1 fleet growth beyond one TSDB node
|
||||||
|
- R2 partner firmware delays certificate rotation
|
||||||
|
- dot: violet
|
||||||
|
title: 12 Glossary
|
||||||
|
items:
|
||||||
|
- Telemetry, readings a device sends
|
||||||
|
- Rollup, hourly aggregate of telemetry
|
||||||
|
- Provisioning, giving a device its identity
|
||||||
|
- Fault code, error number from the device
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
mqtt-broker:
|
||||||
|
type: messagebus
|
||||||
|
label: MQTT broker
|
||||||
|
sublabel: mutual TLS
|
||||||
|
description: >
|
||||||
|
Terminates device connections; one topic per device.
|
||||||
|
ingest:
|
||||||
|
type: backend
|
||||||
|
label: Ingest
|
||||||
|
sublabel: Go
|
||||||
|
brand: go
|
||||||
|
description: >
|
||||||
|
Decodes, validates and stores telemetry; forwards it to the rules.
|
||||||
|
diagram: 0500-building-blocks/ingest.archify
|
||||||
|
tsdb:
|
||||||
|
type: database
|
||||||
|
label: Time series DB
|
||||||
|
sublabel: TimescaleDB
|
||||||
|
brand: postgresql
|
||||||
|
description: >
|
||||||
|
Raw telemetry for 90 days and hourly rollups for 10 years.
|
||||||
|
rules:
|
||||||
|
type: backend
|
||||||
|
label: Rules engine
|
||||||
|
sublabel: alerting
|
||||||
|
description: >
|
||||||
|
Evaluates thresholds and fault codes; raises alerts.
|
||||||
|
provisioning:
|
||||||
|
type: security
|
||||||
|
label: Provisioning
|
||||||
|
sublabel: device certs
|
||||||
|
description: >
|
||||||
|
Registers devices and issues their client certificates.
|
||||||
|
device-registry:
|
||||||
|
type: database
|
||||||
|
label: Device registry
|
||||||
|
sublabel: PostgreSQL
|
||||||
|
brand: postgresql
|
||||||
|
description: >
|
||||||
|
Devices, owners, firmware versions and certificate state.
|
||||||
|
device-api:
|
||||||
|
type: backend
|
||||||
|
label: Device API
|
||||||
|
sublabel: REST
|
||||||
|
description: >
|
||||||
|
Serves telemetry and device data to the customer app.
|
||||||
|
ops-dashboard:
|
||||||
|
type: frontend
|
||||||
|
label: Ops dashboard
|
||||||
|
sublabel: Grafana
|
||||||
|
brand: grafana
|
||||||
|
description: >
|
||||||
|
Fleet view for the operations team, read only on the TSDB.
|
||||||
|
customer-app:
|
||||||
|
type: frontend
|
||||||
|
label: Customer app
|
||||||
|
sublabel: mobile
|
||||||
|
description: >
|
||||||
|
Shows the homeowner their heat pump and its alerts.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
homeowner:
|
||||||
|
type: external
|
||||||
|
label: Homeowner
|
||||||
|
sublabel: customer app
|
||||||
|
description: >
|
||||||
|
Owns a heat pump; checks temperatures and gets fault alerts.
|
||||||
|
ops-engineer:
|
||||||
|
type: external
|
||||||
|
label: Ops engineer
|
||||||
|
sublabel: operations team
|
||||||
|
description: >
|
||||||
|
Watches the fleet, handles alerts and plans service visits.
|
||||||
|
installer:
|
||||||
|
type: external
|
||||||
|
label: Installer
|
||||||
|
sublabel: field service
|
||||||
|
description: >
|
||||||
|
Mounts heat pumps on site and registers them with the platform.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
telemetry-platform:
|
||||||
|
type: cloud
|
||||||
|
label: IoT Telemetry
|
||||||
|
sublabel: the system
|
||||||
|
description: >
|
||||||
|
Everything we build and run; broker, ingest, stores, rules and apps.
|
||||||
|
diagram: 0500-building-blocks/level-1.archify
|
||||||
|
heat-pump:
|
||||||
|
type: external
|
||||||
|
label: Heat pump
|
||||||
|
sublabel: field device
|
||||||
|
description: >
|
||||||
|
Sends telemetry every 30 seconds over MQTT; built by a partner.
|
||||||
|
push-service:
|
||||||
|
type: external
|
||||||
|
label: Push service
|
||||||
|
sublabel: mobile push
|
||||||
|
description: >
|
||||||
|
Delivers push notifications to the customer app.
|
||||||
|
service-desk:
|
||||||
|
type: external
|
||||||
|
label: Service desk
|
||||||
|
sublabel: ticketing
|
||||||
|
description: >
|
||||||
|
Opens service tickets for faults that need a visit.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Technical context
|
||||||
|
output: technical-context.html
|
||||||
|
components:
|
||||||
|
- ref: engineer
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: wiki-admin
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: wiki
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [520, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: object-store
|
||||||
|
pos: [520, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: engineer, to: wiki, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: wiki-admin, to: wiki, label: kubectl }
|
||||||
|
- { from: wiki, to: idp, label: OIDC, variant: security }
|
||||||
|
- { from: wiki, to: object-store, label: S3 API }
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Building blocks - level 1
|
||||||
|
output: level-1.html
|
||||||
|
decisions: [postgres-full-text-search]
|
||||||
|
components:
|
||||||
|
- ref: engineer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: web
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: api
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: backup-job
|
||||||
|
pos: [670, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [670, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: object-store
|
||||||
|
pos: [880, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Team Wiki
|
||||||
|
wraps: [web, api, db, backup-job]
|
||||||
|
connections:
|
||||||
|
- { from: engineer, to: web, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: web, to: api, label: JSON, variant: emphasis }
|
||||||
|
- { from: api, to: db, label: SQL, labelAt: [525, 234], decisions: [postgres-full-text-search] }
|
||||||
|
- { from: api, to: idp, label: OIDC, variant: security }
|
||||||
|
- { from: api, to: object-store, label: files }
|
||||||
|
- { from: backup-job, to: db, label: dump }
|
||||||
|
- { from: backup-job, to: object-store, label: upload }
|
||||||
|
cards:
|
||||||
|
- dot: amber
|
||||||
|
title: 8 Crosscutting concepts
|
||||||
|
items:
|
||||||
|
- Access - every request checked against space groups
|
||||||
|
- Logs - JSON to stdout, collected by the cluster
|
||||||
|
- Config - environment variables, secrets from the vault
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Building blocks - level 2, Wiki API
|
||||||
|
output: wiki-api.html
|
||||||
|
components:
|
||||||
|
- ref: web
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: auth
|
||||||
|
type: security
|
||||||
|
label: Auth
|
||||||
|
sublabel: OIDC session
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: pages
|
||||||
|
type: backend
|
||||||
|
label: Pages
|
||||||
|
sublabel: revisions
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: search
|
||||||
|
type: backend
|
||||||
|
label: Search
|
||||||
|
sublabel: full-text
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [postgres-full-text-search]
|
||||||
|
- id: attachments
|
||||||
|
type: backend
|
||||||
|
label: Attachments
|
||||||
|
pos: [250, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [460, 210]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: object-store
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Wiki API
|
||||||
|
wraps: [auth, pages, search, attachments]
|
||||||
|
connections:
|
||||||
|
- { from: web, to: auth, label: sign in, variant: security }
|
||||||
|
- { from: web, to: pages, label: edit, labelAt: [222, 182] }
|
||||||
|
- { from: web, to: search, label: query }
|
||||||
|
- { from: web, to: attachments, label: upload }
|
||||||
|
- { from: auth, to: idp, label: OIDC, variant: security }
|
||||||
|
- { from: pages, to: db, label: SQL }
|
||||||
|
- { from: search, to: db, label: SQL }
|
||||||
|
- { from: attachments, to: object-store, label: S3 }
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: sequence
|
||||||
|
meta:
|
||||||
|
title: Runtime - search and open a page
|
||||||
|
output: search-page.html
|
||||||
|
participants:
|
||||||
|
- ref: engineer
|
||||||
|
- ref: web
|
||||||
|
- ref: api
|
||||||
|
- { ref: db, label: DB }
|
||||||
|
- { ref: object-store, label: Object store }
|
||||||
|
messages:
|
||||||
|
- { from: engineer, to: web, y: 160, label: type query, variant: emphasis }
|
||||||
|
- { from: web, to: api, y: 210, label: GET /search }
|
||||||
|
- { from: api, to: db, y: 260, label: full-text query }
|
||||||
|
- { from: db, to: api, y: 310, label: ranked pages, variant: return }
|
||||||
|
- { from: api, to: web, y: 360, label: results, variant: return }
|
||||||
|
- { from: web, to: api, y: 410, label: GET /pages/id }
|
||||||
|
- { from: api, to: object-store, y: 460, label: signed URL }
|
||||||
|
- { from: api, to: web, y: 510, label: page, variant: return }
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: sequence
|
||||||
|
meta:
|
||||||
|
title: Runtime - sign in
|
||||||
|
output: sign-in.html
|
||||||
|
decisions: [sso-only-via-company-idp]
|
||||||
|
participants:
|
||||||
|
- ref: engineer
|
||||||
|
- ref: web
|
||||||
|
- ref: api
|
||||||
|
- { ref: idp, label: Company IdP }
|
||||||
|
messages:
|
||||||
|
- { from: engineer, to: web, y: 160, label: open wiki, variant: emphasis }
|
||||||
|
- { from: web, to: api, y: 210, label: GET /session }
|
||||||
|
- { from: api, to: web, y: 260, label: redirect, variant: return }
|
||||||
|
- { from: web, to: idp, y: 310, label: OIDC login, variant: security }
|
||||||
|
- { from: idp, to: api, y: 360, label: code, variant: return }
|
||||||
|
- { from: api, to: idp, y: 410, label: token exchange, variant: security }
|
||||||
|
- { from: api, to: web, y: 460, label: session cookie, variant: return }
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Deployment - production
|
||||||
|
output: production.html
|
||||||
|
components:
|
||||||
|
- ref: engineer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- id: ingress
|
||||||
|
type: security
|
||||||
|
label: Ingress
|
||||||
|
sublabel: TLS
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: web
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: api
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: backup-job
|
||||||
|
pos: [670, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [460, 460]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: object-store
|
||||||
|
pos: [880, 460]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Company cluster
|
||||||
|
wraps: [ingress, web, api, db, backup-job]
|
||||||
|
- kind: security-group
|
||||||
|
label: Namespace team-wiki
|
||||||
|
wraps: [web, api, db, backup-job]
|
||||||
|
connections:
|
||||||
|
- { from: engineer, to: ingress, label: HTTPS, variant: emphasis }
|
||||||
|
- { from: ingress, to: web, label: static }
|
||||||
|
- { from: ingress, to: api, label: /api, variant: emphasis }
|
||||||
|
- { from: api, to: db, label: SQL }
|
||||||
|
- { from: api, to: idp, label: OIDC, variant: security, labelAt: [525, 234] }
|
||||||
|
- { from: backup-job, to: db, label: dump }
|
||||||
|
- { from: backup-job, to: object-store, label: S3 }
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: postgres-full-text-search
|
||||||
|
title: PostgreSQL full-text search, no search engine
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-05
|
||||||
|
elements: [api, db]
|
||||||
|
diagrams: [0500-building-blocks/level-1.archify, 0500-building-blocks/wiki-api.archify]
|
||||||
|
context: >
|
||||||
|
The wiki holds a few thousand pages for one team. A
|
||||||
|
separate search engine would be one more service to run,
|
||||||
|
back up and keep in sync.
|
||||||
|
decision: >
|
||||||
|
Search uses PostgreSQL full-text search on a generated
|
||||||
|
column of each page. The API queries it in the same
|
||||||
|
transaction as the page data.
|
||||||
|
consequences: >
|
||||||
|
Results are never stale and backups cover search too.
|
||||||
|
Ranking is simpler than a search engine. Revisit when
|
||||||
|
the wiki grows past 100000 pages.
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: sso-only-via-company-idp
|
||||||
|
title: Sign-in only through the company identity provider
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-12
|
||||||
|
elements: [idp, api, web]
|
||||||
|
diagrams: [0300-context/technical-context.archify, 0600-runtime/sign-in.archify]
|
||||||
|
context: >
|
||||||
|
Leavers kept access to the old wiki because it had its
|
||||||
|
own passwords. Nobody owned the user list.
|
||||||
|
decision: >
|
||||||
|
The wiki has no local accounts. Users sign in with OIDC
|
||||||
|
at the company identity provider; team groups map to
|
||||||
|
wiki spaces.
|
||||||
|
consequences: >
|
||||||
|
Offboarding removes wiki access at once. The wiki cannot
|
||||||
|
be used when the identity provider is down. The admin
|
||||||
|
needs a break-glass account kept in the vault.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: team-wiki
|
||||||
|
name: Team Wiki
|
||||||
|
description: >
|
||||||
|
An arc42 example: a small self-hosted wiki for an engineering
|
||||||
|
team. Sections 1 goals, 2 constraints, 3 business context,
|
||||||
|
10 quality, 11 risks and 12 glossary are on this overview;
|
||||||
|
0300-context holds 3 technical context, 0500-building-blocks
|
||||||
|
5 building blocks, 0600-runtime 6 runtime, 0700-deployment
|
||||||
|
7 deployment and decisions 9 architecture decisions.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Team Wiki - business context
|
||||||
|
output: main.html
|
||||||
|
decisions: [sso-only-via-company-idp]
|
||||||
|
components:
|
||||||
|
- ref: engineer
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: wiki-admin
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: wiki
|
||||||
|
pos: [280, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: idp
|
||||||
|
pos: [520, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: object-store
|
||||||
|
pos: [520, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
connections:
|
||||||
|
- { from: engineer, to: wiki, label: reads, variant: emphasis }
|
||||||
|
- { from: wiki-admin, to: wiki, label: runs }
|
||||||
|
- { from: wiki, to: idp, label: sign-in, variant: security }
|
||||||
|
- { from: wiki, to: object-store, label: files }
|
||||||
|
cards:
|
||||||
|
- dot: cyan
|
||||||
|
title: 1 Quality goals
|
||||||
|
items:
|
||||||
|
- Findability - any page found in two searches
|
||||||
|
- Security - only current staff can read
|
||||||
|
- Operability - one person runs it in an hour a week
|
||||||
|
- dot: slate
|
||||||
|
title: 2 Constraints
|
||||||
|
items:
|
||||||
|
- Runs on the company Kubernetes cluster
|
||||||
|
- Sign-in only via the company identity provider
|
||||||
|
- dot: emerald
|
||||||
|
title: 10 Quality scenarios
|
||||||
|
items:
|
||||||
|
- A search over 5000 pages answers in under 1 s
|
||||||
|
- A leaver loses access within 5 minutes
|
||||||
|
- The admin restores last night's state in 30 minutes
|
||||||
|
- dot: rose
|
||||||
|
title: 11 Risks
|
||||||
|
items:
|
||||||
|
- Identity provider outage blocks all sign-ins
|
||||||
|
- Backups untested until a restore drill is run
|
||||||
|
- dot: violet
|
||||||
|
title: 12 Glossary
|
||||||
|
items:
|
||||||
|
- Space - pages of one team, mapped to a group
|
||||||
|
- Revision - one saved version of a page
|
||||||
|
- Attachment - a file stored beside a page
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
web:
|
||||||
|
type: frontend
|
||||||
|
label: Web App
|
||||||
|
sublabel: React
|
||||||
|
brand: react
|
||||||
|
description: >
|
||||||
|
Page editor, viewer and search box in the browser; talks
|
||||||
|
only to the API.
|
||||||
|
api:
|
||||||
|
type: backend
|
||||||
|
label: Wiki API
|
||||||
|
sublabel: REST
|
||||||
|
description: >
|
||||||
|
Pages, search, attachments and access checks; the only
|
||||||
|
writer of the database.
|
||||||
|
diagram: 0500-building-blocks/wiki-api.archify
|
||||||
|
db:
|
||||||
|
type: database
|
||||||
|
label: Database
|
||||||
|
sublabel: PostgreSQL
|
||||||
|
brand: postgresql
|
||||||
|
description: >
|
||||||
|
Pages, revisions, users and the full-text index; the
|
||||||
|
source of truth.
|
||||||
|
backup-job:
|
||||||
|
type: backend
|
||||||
|
label: Backup job
|
||||||
|
sublabel: nightly
|
||||||
|
description: >
|
||||||
|
Dumps the database at 02:00 and writes it to the backup
|
||||||
|
bucket; keeps 30 days.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
engineer:
|
||||||
|
type: external
|
||||||
|
label: Engineer
|
||||||
|
sublabel: team member
|
||||||
|
description: >
|
||||||
|
Reads and edits pages, searches, attaches files; signs in
|
||||||
|
with the company account.
|
||||||
|
wiki-admin:
|
||||||
|
type: external
|
||||||
|
label: Wiki admin
|
||||||
|
sublabel: platform team
|
||||||
|
description: >
|
||||||
|
Runs the wiki, restores backups and manages spaces.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
wiki:
|
||||||
|
type: cloud
|
||||||
|
label: Team Wiki
|
||||||
|
sublabel: the system
|
||||||
|
description: >
|
||||||
|
Everything we build and run; web app, API, database and
|
||||||
|
the backup job.
|
||||||
|
diagram: 0500-building-blocks/level-1.archify
|
||||||
|
idp:
|
||||||
|
type: security
|
||||||
|
label: Identity provider
|
||||||
|
sublabel: company SSO
|
||||||
|
description: >
|
||||||
|
The company identity provider; signs users in over OIDC
|
||||||
|
and owns team membership.
|
||||||
|
object-store:
|
||||||
|
type: external
|
||||||
|
label: Object storage
|
||||||
|
sublabel: S3-compatible
|
||||||
|
description: >
|
||||||
|
Company object storage; holds attachments and nightly
|
||||||
|
backups in separate buckets.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Business actors and services
|
||||||
|
output: business-services.html
|
||||||
|
components:
|
||||||
|
- ref: hr
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: manager
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: service-desk
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: onboard
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
diagram: 0100-business/onboard-employee.archify
|
||||||
|
- ref: onboarding-svc
|
||||||
|
pos: [480, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: workplace-svc
|
||||||
|
pos: [480, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: new-hire
|
||||||
|
pos: [690, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: hr, to: onboard, label: assigned }
|
||||||
|
- { from: manager, to: onboard, label: assigned }
|
||||||
|
- { from: service-desk, to: onboard, label: assigned }
|
||||||
|
- { from: onboard, to: onboarding-svc, label: realizes }
|
||||||
|
- { from: onboard, to: workplace-svc, label: realizes }
|
||||||
|
- { from: onboarding-svc, to: new-hire, label: serves, variant: emphasis }
|
||||||
|
- { from: workplace-svc, to: new-hire, label: serves }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
schema_version: 2
|
||||||
|
diagram_type: workflow
|
||||||
|
meta:
|
||||||
|
title: Onboard Employee
|
||||||
|
output: onboard-employee.html
|
||||||
|
viewBox: [1040, 660]
|
||||||
|
lanes:
|
||||||
|
- { id: hr-lane, label: HR }
|
||||||
|
- { id: manager-lane, label: Hiring manager }
|
||||||
|
- { id: desk-lane, label: IT service desk }
|
||||||
|
- { id: hire-lane, label: New hire }
|
||||||
|
mainPath: [contract-signed, record, request, create-accounts, issue-laptop, first-day]
|
||||||
|
nodes:
|
||||||
|
- { ref: contract-signed, lane: hr-lane, col: 0, width: 120 }
|
||||||
|
- { id: record, lane: hr-lane, col: 1, type: backend, label: Create record, tag: Business Process }
|
||||||
|
- { id: request, lane: manager-lane, col: 2, type: frontend, label: Request access, tag: Business Process }
|
||||||
|
- { ref: create-accounts, lane: desk-lane, col: 3, width: 120, decisions: [hr-system-drives-identity] }
|
||||||
|
- { ref: issue-laptop, lane: desk-lane, col: 4, decisions: [zero-touch-laptops] }
|
||||||
|
- { ref: first-day, lane: hire-lane, col: 5 }
|
||||||
|
edges:
|
||||||
|
- { from: contract-signed, to: record, label: triggers }
|
||||||
|
- { from: record, to: request, label: notifies }
|
||||||
|
- { from: request, to: create-accounts, label: ticket }
|
||||||
|
- { from: create-accounts, to: issue-laptop, label: then }
|
||||||
|
- { from: issue-laptop, to: first-day, label: hand over }
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Application components, services and data
|
||||||
|
output: application-components.html
|
||||||
|
components:
|
||||||
|
- ref: provisioning-svc
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: portal
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: device-svc
|
||||||
|
pos: [670, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: hr-system
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: iam
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: itsm
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mdm-app
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: employee-record
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: account
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: asset
|
||||||
|
pos: [460, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: iam, to: provisioning-svc, label: realizes }
|
||||||
|
- { from: itsm, to: portal, label: realizes }
|
||||||
|
- { from: mdm-app, to: device-svc, label: realizes }
|
||||||
|
- { from: hr-system, to: employee-record, label: writes, labelAt: [105, 234] }
|
||||||
|
- { from: iam, to: account, label: writes, labelAt: [315, 234] }
|
||||||
|
- { from: itsm, to: asset, label: writes, labelAt: [525, 234] }
|
||||||
|
- { from: mdm-app, to: asset, label: reads }
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Application cooperation
|
||||||
|
output: application-cooperation.html
|
||||||
|
decisions: [hr-system-drives-identity]
|
||||||
|
components:
|
||||||
|
- ref: hr-system
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: iam
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
diagram: 0200-application/application-components.archify
|
||||||
|
- ref: itsm
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: collab
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mdm-app
|
||||||
|
pos: [460, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: hr-system, to: iam, label: joiner event, variant: emphasis, decisions: [hr-system-drives-identity] }
|
||||||
|
- { from: hr-system, to: itsm, label: laptop request }
|
||||||
|
- { from: iam, to: collab, label: provisions }
|
||||||
|
- { from: iam, to: mdm-app, label: users }
|
||||||
|
- { from: itsm, to: mdm-app, label: assigns }
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Technology layer
|
||||||
|
output: infrastructure.html
|
||||||
|
decisions: [zero-touch-laptops]
|
||||||
|
components:
|
||||||
|
- ref: hr-saas
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: sso
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: directory
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: itsm-saas
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mdm-saas
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: laptop
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: office-net
|
||||||
|
pos: [250, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: SaaS tenants
|
||||||
|
wraps: [hr-saas, sso, directory, itsm-saas, mdm-saas]
|
||||||
|
- kind: security-group
|
||||||
|
label: Office
|
||||||
|
wraps: [laptop, office-net]
|
||||||
|
connections:
|
||||||
|
- { from: directory, to: sso, label: realizes }
|
||||||
|
- { from: sso, to: hr-saas, label: serves }
|
||||||
|
- { from: sso, to: itsm-saas, label: serves }
|
||||||
|
- { from: directory, to: mdm-saas, label: devices, labelAt: [525, 134] }
|
||||||
|
- { from: mdm-saas, to: laptop, label: manages, labelAt: [525, 234], variant: security, decisions: [zero-touch-laptops] }
|
||||||
|
- { from: laptop, to: office-net, label: connects }
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: hr-system-drives-identity
|
||||||
|
title: The HR system drives identity
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-08
|
||||||
|
elements: [hr-system, iam, account, employee-record]
|
||||||
|
diagrams: [0200-application/application-cooperation.archify, 0100-business/onboard-employee.archify]
|
||||||
|
context: >
|
||||||
|
Accounts were created by hand from emails sent by HR.
|
||||||
|
New hires waited days for access, and leavers kept
|
||||||
|
accounts for weeks after their last day.
|
||||||
|
decision: >
|
||||||
|
The HR system is the source of truth for joiners and
|
||||||
|
leavers. Identity management reads its events and
|
||||||
|
creates, changes and disables accounts automatically.
|
||||||
|
consequences: >
|
||||||
|
No account without an employee record. HR data quality
|
||||||
|
now decides access, so start dates must be right. The
|
||||||
|
service desk only handles exceptions.
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: zero-touch-laptops
|
||||||
|
title: Zero-touch enrollment for laptops
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-05
|
||||||
|
elements: [mdm-app, mdm-saas, laptop, device-svc, asset]
|
||||||
|
diagrams: [0300-technology/infrastructure.archify, 0100-business/onboard-employee.archify]
|
||||||
|
context: >
|
||||||
|
Every laptop was imaged by hand at the service desk,
|
||||||
|
which took half a day per device and left settings that
|
||||||
|
differed from one laptop to the next.
|
||||||
|
decision: >
|
||||||
|
Laptops are registered in device management at purchase.
|
||||||
|
They configure themselves at first sign-in over any
|
||||||
|
internet connection; the office network needs no VPN.
|
||||||
|
consequences: >
|
||||||
|
Laptops can be shipped straight to the new hire. A lost
|
||||||
|
laptop is wiped remotely. Setup depends on the MDM
|
||||||
|
tenant being reachable on the first day.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: hr-onboarding
|
||||||
|
name: HR Onboarding
|
||||||
|
description: >
|
||||||
|
An ArchiMate example: how a new employee is onboarded, from signed
|
||||||
|
contract to first day. Business, application and technology layers
|
||||||
|
map to folders; each element's ArchiMate type is shown as its tag.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: HR Onboarding - layered view
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: new-hire
|
||||||
|
pos: [40, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: onboarding-svc
|
||||||
|
pos: [270, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: onboard
|
||||||
|
pos: [500, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: portal
|
||||||
|
pos: [40, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: iam
|
||||||
|
pos: [270, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: provisioning-svc
|
||||||
|
pos: [500, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: office-net
|
||||||
|
pos: [40, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: sso
|
||||||
|
pos: [270, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: directory
|
||||||
|
pos: [500, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Business layer
|
||||||
|
wraps: [new-hire, onboarding-svc, onboard]
|
||||||
|
- kind: region
|
||||||
|
label: Application layer
|
||||||
|
wraps: [portal, iam, provisioning-svc]
|
||||||
|
- kind: region
|
||||||
|
label: Technology layer
|
||||||
|
wraps: [office-net, sso, directory]
|
||||||
|
connections:
|
||||||
|
- { from: onboarding-svc, to: new-hire, label: serves, variant: emphasis }
|
||||||
|
- { from: onboard, to: onboarding-svc, label: realizes }
|
||||||
|
- { from: portal, to: new-hire, label: serves }
|
||||||
|
- { from: provisioning-svc, to: onboard, label: serves }
|
||||||
|
- { from: iam, to: provisioning-svc, label: realizes }
|
||||||
|
- { from: office-net, to: portal, label: serves }
|
||||||
|
- { from: sso, to: iam, label: serves }
|
||||||
|
- { from: directory, to: sso, label: realizes }
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
hr-system:
|
||||||
|
type: backend
|
||||||
|
label: HR system
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Holds employee records and contracts; the source of truth for joiners.
|
||||||
|
diagram: 0200-application/application-cooperation.archify
|
||||||
|
iam:
|
||||||
|
type: security
|
||||||
|
label: Identity mgmt
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Creates and removes accounts from HR events; single sign-on for all apps.
|
||||||
|
diagram: 0200-application/application-components.archify
|
||||||
|
itsm:
|
||||||
|
type: backend
|
||||||
|
label: IT service mgmt
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Tickets, requests and the asset inventory of the IT service desk.
|
||||||
|
mdm-app:
|
||||||
|
type: backend
|
||||||
|
label: Device mgmt
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Enrolls laptops, pushes policies and apps, wipes lost devices.
|
||||||
|
collab:
|
||||||
|
type: backend
|
||||||
|
label: Mail and chat
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Mailbox, calendar and chat; accounts provisioned by identity management.
|
||||||
|
provisioning-svc:
|
||||||
|
type: cloud
|
||||||
|
label: Provisioning
|
||||||
|
sublabel: App Service
|
||||||
|
tag: App Service
|
||||||
|
description: >
|
||||||
|
Account create, change and disable, triggered by the HR system.
|
||||||
|
device-svc:
|
||||||
|
type: cloud
|
||||||
|
label: Enrollment
|
||||||
|
sublabel: App Service
|
||||||
|
tag: App Service
|
||||||
|
description: >
|
||||||
|
Zero-touch enrollment; the laptop configures itself at first sign-in.
|
||||||
|
portal:
|
||||||
|
type: frontend
|
||||||
|
label: Service portal
|
||||||
|
sublabel: App Interface
|
||||||
|
tag: App Interface
|
||||||
|
description: >
|
||||||
|
Where managers request equipment and access for a new hire.
|
||||||
|
employee-record:
|
||||||
|
type: database
|
||||||
|
label: Employee record
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
Name, role, department, manager and start date of an employee.
|
||||||
|
account:
|
||||||
|
type: database
|
||||||
|
label: Account
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
User id, groups and licences of one person.
|
||||||
|
asset:
|
||||||
|
type: database
|
||||||
|
label: Asset
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
A laptop with serial number, owner and enrollment state.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
new-hire:
|
||||||
|
type: external
|
||||||
|
label: New hire
|
||||||
|
sublabel: Business Actor
|
||||||
|
tag: Business Actor
|
||||||
|
description: >
|
||||||
|
The person joining the company; signs the contract and starts on day one.
|
||||||
|
hr:
|
||||||
|
type: external
|
||||||
|
label: HR officer
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
Prepares the contract and the employee record; owns the onboarding process.
|
||||||
|
manager:
|
||||||
|
type: external
|
||||||
|
label: Hiring manager
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
Requests access and equipment for the new hire and plans the first day.
|
||||||
|
service-desk:
|
||||||
|
type: external
|
||||||
|
label: IT service desk
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
Creates accounts, prepares the laptop and hands it over.
|
||||||
|
onboard:
|
||||||
|
type: backend
|
||||||
|
label: Onboard Employee
|
||||||
|
sublabel: Business Process
|
||||||
|
tag: Business Process
|
||||||
|
description: >
|
||||||
|
From signed contract to a productive first day.
|
||||||
|
diagram: 0100-business/onboard-employee.archify
|
||||||
|
onboarding-svc:
|
||||||
|
type: cloud
|
||||||
|
label: Onboarding
|
||||||
|
sublabel: Business Service
|
||||||
|
tag: Business Service
|
||||||
|
description: >
|
||||||
|
What the new hire gets from HR; contract, record, welcome and first day plan.
|
||||||
|
diagram: 0100-business/business-services.archify
|
||||||
|
workplace-svc:
|
||||||
|
type: cloud
|
||||||
|
label: Workplace setup
|
||||||
|
sublabel: Business Service
|
||||||
|
tag: Business Service
|
||||||
|
description: >
|
||||||
|
Accounts and a ready laptop, delivered by the IT service desk.
|
||||||
|
contract-signed:
|
||||||
|
type: messagebus
|
||||||
|
label: Contract signed
|
||||||
|
sublabel: Business Event
|
||||||
|
tag: Business Event
|
||||||
|
description: >
|
||||||
|
Starts the process; HR records the start date.
|
||||||
|
create-accounts:
|
||||||
|
type: backend
|
||||||
|
label: Create accounts
|
||||||
|
sublabel: Business Process
|
||||||
|
tag: Business Process
|
||||||
|
description: >
|
||||||
|
Service desk confirms the accounts the identity system provisioned.
|
||||||
|
issue-laptop:
|
||||||
|
type: backend
|
||||||
|
label: Issue laptop
|
||||||
|
sublabel: Business Process
|
||||||
|
tag: Business Process
|
||||||
|
description: >
|
||||||
|
An enrolled laptop is assigned and handed over.
|
||||||
|
first-day:
|
||||||
|
type: messagebus
|
||||||
|
label: First day
|
||||||
|
sublabel: Business Event
|
||||||
|
tag: Business Event
|
||||||
|
description: >
|
||||||
|
The new hire signs in for the first time; the process ends.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
hr-saas:
|
||||||
|
type: cloud
|
||||||
|
label: HR tenant
|
||||||
|
sublabel: Node
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
The SaaS tenant that runs the HR system.
|
||||||
|
diagram: 0300-technology/infrastructure.archify
|
||||||
|
itsm-saas:
|
||||||
|
type: cloud
|
||||||
|
label: ITSM tenant
|
||||||
|
sublabel: Node
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
The SaaS tenant that runs IT service management.
|
||||||
|
mdm-saas:
|
||||||
|
type: cloud
|
||||||
|
label: MDM tenant
|
||||||
|
sublabel: Node
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
The SaaS tenant that runs device management.
|
||||||
|
directory:
|
||||||
|
type: backend
|
||||||
|
label: Directory
|
||||||
|
sublabel: System Software
|
||||||
|
tag: System Software
|
||||||
|
description: >
|
||||||
|
Cloud directory service; users, groups and devices.
|
||||||
|
diagram: 0300-technology/infrastructure.archify
|
||||||
|
sso:
|
||||||
|
type: security
|
||||||
|
label: Single sign-on
|
||||||
|
sublabel: Tech Service
|
||||||
|
tag: Tech Service
|
||||||
|
description: >
|
||||||
|
SAML and OIDC sign-in for every SaaS tenant, backed by the directory.
|
||||||
|
laptop:
|
||||||
|
type: frontend
|
||||||
|
label: Laptop
|
||||||
|
sublabel: Device
|
||||||
|
tag: Device
|
||||||
|
description: >
|
||||||
|
Company laptop, enrolled before it is handed over.
|
||||||
|
office-net:
|
||||||
|
type: messagebus
|
||||||
|
label: Office network
|
||||||
|
sublabel: Network
|
||||||
|
tag: Network
|
||||||
|
description: >
|
||||||
|
Wired and wireless network of the office; internet only, no VPN.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Business layer
|
||||||
|
output: business-layer.html
|
||||||
|
components:
|
||||||
|
- ref: customer
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: insurant
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: claim-registration
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: claim-payment
|
||||||
|
pos: [460, 260]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: handle-claim
|
||||||
|
pos: [690, 160]
|
||||||
|
size: [150, 60]
|
||||||
|
- ref: claims-handler
|
||||||
|
pos: [920, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: customer, to: insurant, label: assigned to }
|
||||||
|
- { from: claim-registration, to: insurant, label: serves, variant: emphasis }
|
||||||
|
- { from: claim-payment, to: insurant, label: serves }
|
||||||
|
- { from: handle-claim, to: claim-registration, label: realizes }
|
||||||
|
- { from: handle-claim, to: claim-payment, label: realizes }
|
||||||
|
- { from: claims-handler, to: handle-claim, label: assigned to }
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
schema_version: 2
|
||||||
|
diagram_type: workflow
|
||||||
|
meta:
|
||||||
|
title: Handle Claim
|
||||||
|
output: handle-claim.html
|
||||||
|
viewBox: [840, 534]
|
||||||
|
lanes:
|
||||||
|
- { id: insurant-lane, label: Insurant }
|
||||||
|
- { id: handler-lane, label: Claims handler }
|
||||||
|
- { id: systems-lane, label: Applications }
|
||||||
|
mainPath: [damage, register, accept, valuate, pay]
|
||||||
|
nodes:
|
||||||
|
- { id: damage, lane: insurant-lane, col: 0, type: external, label: Damage occurs, tag: Business Event }
|
||||||
|
- { ref: insurant, lane: insurant-lane, col: 5 }
|
||||||
|
- { id: register, lane: handler-lane, col: 1, type: backend, label: Register, tag: Business Process }
|
||||||
|
- { id: accept, lane: handler-lane, col: 2, type: security, label: Accept, sublabel: check coverage, tag: Business Process }
|
||||||
|
- { id: valuate, lane: handler-lane, col: 3, type: backend, label: Valuate, tag: Business Process }
|
||||||
|
- { id: pay, lane: handler-lane, col: 4, type: backend, label: Pay, tag: Business Process }
|
||||||
|
- { ref: claims-app, lane: systems-lane, col: 2 }
|
||||||
|
- { ref: payment, lane: systems-lane, col: 4 }
|
||||||
|
edges:
|
||||||
|
- { from: damage, to: register, label: report }
|
||||||
|
- { from: register, to: accept }
|
||||||
|
- { from: accept, to: valuate, label: covered }
|
||||||
|
- { from: valuate, to: pay, label: approved }
|
||||||
|
- { from: pay, to: insurant, label: paid }
|
||||||
|
- { from: accept, to: insurant, label: rejected, role: branch, variant: dashed }
|
||||||
|
- { from: claims-app, to: accept, label: serves, role: async }
|
||||||
|
- { from: payment, to: pay, label: serves, role: async }
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Application cooperation
|
||||||
|
output: application-cooperation.html
|
||||||
|
components:
|
||||||
|
- ref: portal
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: claims-app
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: crm
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: policy-admin
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [policies-stay-on-mainframe]
|
||||||
|
- ref: doc-mgmt
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [documents-in-object-storage]
|
||||||
|
- ref: payment
|
||||||
|
pos: [250, 460]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: portal, to: claims-app, label: claims, variant: emphasis }
|
||||||
|
- { from: crm, to: claims-app, label: customer, labelAt: [315, 134] }
|
||||||
|
- { from: policy-admin, to: claims-app, label: coverage, decisions: [policies-stay-on-mainframe] }
|
||||||
|
- { from: claims-app, to: doc-mgmt, label: documents }
|
||||||
|
- { from: claims-app, to: payment, label: payments, labelAt: [315, 334] }
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Application structure
|
||||||
|
output: application-structure.html
|
||||||
|
components:
|
||||||
|
- ref: portal
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: claims-admin
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: claims-app
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: claim
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: crm
|
||||||
|
pos: [670, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: customer-file
|
||||||
|
pos: [880, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: policy-admin
|
||||||
|
pos: [670, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: policy
|
||||||
|
pos: [880, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: claims-admin, to: portal, label: serves, variant: emphasis }
|
||||||
|
- { from: claims-app, to: claims-admin, label: realizes }
|
||||||
|
- { from: claims-app, to: claim, label: accesses, labelAt: [525, 234] }
|
||||||
|
- { from: crm, to: customer-file, label: accesses }
|
||||||
|
- { from: policy-admin, to: policy, label: accesses }
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Technology layer
|
||||||
|
output: infrastructure.html
|
||||||
|
components:
|
||||||
|
- ref: dmz
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: doc-store
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [documents-in-object-storage]
|
||||||
|
- ref: app-servers
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db-service
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: db-cluster
|
||||||
|
pos: [250, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mq
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mainframe
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [policies-stay-on-mainframe]
|
||||||
|
- ref: dc-lan
|
||||||
|
pos: [670, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Data centre
|
||||||
|
wraps: [doc-store, app-servers, db-service, db-cluster, mq, mainframe, dc-lan]
|
||||||
|
connections:
|
||||||
|
- { from: dmz, to: app-servers, label: HTTPS, variant: security }
|
||||||
|
- { from: app-servers, to: doc-store, label: S3 API }
|
||||||
|
- { from: db-service, to: app-servers, label: serves }
|
||||||
|
- { from: db-cluster, to: db-service, label: realizes }
|
||||||
|
- { from: app-servers, to: mq, label: MQ }
|
||||||
|
- { from: mq, to: mainframe, label: bridge }
|
||||||
|
- { from: db-cluster, to: dc-lan, label: LAN }
|
||||||
|
- { from: mainframe, to: dc-lan, label: LAN, labelAt: [735, 234] }
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: policies-stay-on-mainframe
|
||||||
|
title: Policies stay on the mainframe
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-08
|
||||||
|
elements: [policy-admin, mainframe, mq, claims-app]
|
||||||
|
diagrams: [0200-application/application-cooperation.archify, 0300-technology/infrastructure.archify]
|
||||||
|
context: >
|
||||||
|
The new claims application needs coverage data for every
|
||||||
|
claim. Policy administration has run on the mainframe for
|
||||||
|
twenty years and a migration was estimated at three years.
|
||||||
|
decision: >
|
||||||
|
Policy administration stays on the mainframe. The claims
|
||||||
|
application reads coverage through the message queue bridge
|
||||||
|
and never copies policies into its own database.
|
||||||
|
consequences: >
|
||||||
|
Accepting a claim depends on the mainframe being up during
|
||||||
|
office hours. The queue bridge must be kept and tested.
|
||||||
|
A later migration needs its own decision.
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: documents-in-object-storage
|
||||||
|
title: Claim documents in object storage
|
||||||
|
status: accepted
|
||||||
|
date: 2026-10-14
|
||||||
|
elements: [doc-mgmt, doc-store, claims-app]
|
||||||
|
diagrams: [0200-application/application-cooperation.archify, 0300-technology/infrastructure.archify]
|
||||||
|
context: >
|
||||||
|
Photos and expert reports were stored as blobs in the claims
|
||||||
|
database. The database grew by forty gigabytes a month and
|
||||||
|
backups no longer fitted the nightly window.
|
||||||
|
decision: >
|
||||||
|
Document management stores every file in the document store
|
||||||
|
and keeps only a reference in the claim. The claims
|
||||||
|
application never writes files itself.
|
||||||
|
consequences: >
|
||||||
|
Database backups shrink and stay fast. Documents get their own
|
||||||
|
retention rules. A claim and its files are restored from two
|
||||||
|
places after an incident.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: insurance-claims
|
||||||
|
name: Insurance Claims
|
||||||
|
description: >
|
||||||
|
ArchiMate example: how Halbrook Mutual handles claims. The business,
|
||||||
|
application and technology layers are mapped to folders, and each
|
||||||
|
element's ArchiMate type is shown as its tag.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Insurance Claims - layered view
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: insurant
|
||||||
|
pos: [40, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: claim-registration
|
||||||
|
pos: [270, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: handle-claim
|
||||||
|
pos: [500, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: portal
|
||||||
|
pos: [40, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: claims-app
|
||||||
|
pos: [270, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: claims-admin
|
||||||
|
pos: [500, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: dmz
|
||||||
|
pos: [40, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: app-servers
|
||||||
|
pos: [270, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: db-cluster
|
||||||
|
pos: [500, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Business layer
|
||||||
|
wraps: [insurant, claim-registration, handle-claim]
|
||||||
|
- kind: region
|
||||||
|
label: Application layer
|
||||||
|
wraps: [portal, claims-app, claims-admin]
|
||||||
|
- kind: region
|
||||||
|
label: Technology layer
|
||||||
|
wraps: [dmz, app-servers, db-cluster]
|
||||||
|
connections:
|
||||||
|
- { from: claim-registration, to: insurant, label: serves, variant: emphasis }
|
||||||
|
- { from: handle-claim, to: claim-registration, label: realizes }
|
||||||
|
- { from: portal, to: insurant, label: serves }
|
||||||
|
- { from: claims-admin, to: handle-claim, label: serves }
|
||||||
|
- { from: claims-app, to: claims-admin, label: realizes }
|
||||||
|
- { from: dmz, to: portal, label: serves }
|
||||||
|
- { from: app-servers, to: claims-app, label: serves }
|
||||||
|
- { from: db-cluster, to: app-servers, label: serves }
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
portal:
|
||||||
|
type: frontend
|
||||||
|
label: Customer Portal
|
||||||
|
sublabel: App Interface
|
||||||
|
tag: App Interface
|
||||||
|
description: >
|
||||||
|
Web and mobile front end where customers report claims and
|
||||||
|
follow their status.
|
||||||
|
claims-admin:
|
||||||
|
type: cloud
|
||||||
|
label: Claims Admin
|
||||||
|
sublabel: App Service
|
||||||
|
tag: App Service
|
||||||
|
description: >
|
||||||
|
Register, look up and update claims; used by the portal and
|
||||||
|
by claims handlers.
|
||||||
|
claims-app:
|
||||||
|
type: backend
|
||||||
|
label: Claims App
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Owns claims from registration to payment; the system of
|
||||||
|
record for claims.
|
||||||
|
diagram: 0200-application/application-structure.archify
|
||||||
|
crm:
|
||||||
|
type: backend
|
||||||
|
label: CRM
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Customer contacts, addresses and the customer file.
|
||||||
|
policy-admin:
|
||||||
|
type: backend
|
||||||
|
label: Policy Admin
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Policies, coverage and premiums; runs on the mainframe.
|
||||||
|
doc-mgmt:
|
||||||
|
type: backend
|
||||||
|
label: Document Mgmt
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Scanned letters, photos and expert reports attached to claims.
|
||||||
|
payment:
|
||||||
|
type: backend
|
||||||
|
label: Payment
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Sends approved claim payments to the bank in daily batches.
|
||||||
|
claim:
|
||||||
|
type: database
|
||||||
|
label: Claim
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
One reported damage, its valuation and its payment status.
|
||||||
|
policy:
|
||||||
|
type: database
|
||||||
|
label: Policy
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
Coverage, deductible and term of an insurance contract.
|
||||||
|
customer-file:
|
||||||
|
type: database
|
||||||
|
label: Customer File
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
Everything known about a customer; contacts, policies, claims.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
customer:
|
||||||
|
type: external
|
||||||
|
label: Customer
|
||||||
|
sublabel: Business Actor
|
||||||
|
tag: Business Actor
|
||||||
|
description: >
|
||||||
|
A person or small business holding one or more policies
|
||||||
|
with Halbrook Mutual.
|
||||||
|
insurant:
|
||||||
|
type: external
|
||||||
|
label: Insurant
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
The role a customer plays while a claim is open; reports
|
||||||
|
damage and receives the payment.
|
||||||
|
claims-handler:
|
||||||
|
type: external
|
||||||
|
label: Claims handler
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
Employee of the claims department who accepts, valuates
|
||||||
|
and approves claims.
|
||||||
|
handle-claim:
|
||||||
|
type: backend
|
||||||
|
label: Handle Claim
|
||||||
|
sublabel: Business Process
|
||||||
|
tag: Business Process
|
||||||
|
description: >
|
||||||
|
Register, accept, valuate and pay a claim; the core process
|
||||||
|
of the claims department.
|
||||||
|
diagram: 0100-business/handle-claim.archify
|
||||||
|
claim-registration:
|
||||||
|
type: frontend
|
||||||
|
label: Claim Registration
|
||||||
|
sublabel: Business Service
|
||||||
|
tag: Business Service
|
||||||
|
description: >
|
||||||
|
Lets an insurant report damage by phone, mail or the
|
||||||
|
customer portal.
|
||||||
|
claim-payment:
|
||||||
|
type: frontend
|
||||||
|
label: Claim Payment
|
||||||
|
sublabel: Business Service
|
||||||
|
tag: Business Service
|
||||||
|
description: >
|
||||||
|
Pays the valuated amount to the insurant or a repairer.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
app-servers:
|
||||||
|
type: backend
|
||||||
|
label: App Servers
|
||||||
|
sublabel: Node
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
Two virtual machine hosts running the claims app, CRM and the
|
||||||
|
portal back end.
|
||||||
|
diagram: 0300-technology/infrastructure.archify
|
||||||
|
db-cluster:
|
||||||
|
type: database
|
||||||
|
label: DB Cluster
|
||||||
|
sublabel: Node
|
||||||
|
brand: postgresql
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
Primary and standby database servers for claims and CRM data.
|
||||||
|
doc-store:
|
||||||
|
type: database
|
||||||
|
label: Document Store
|
||||||
|
sublabel: System Software
|
||||||
|
tag: System Software
|
||||||
|
description: >
|
||||||
|
Object storage for scans and photos; written by document
|
||||||
|
management only.
|
||||||
|
mainframe:
|
||||||
|
type: backend
|
||||||
|
label: Mainframe
|
||||||
|
sublabel: Node
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
Hosts policy administration and the nightly payment batch.
|
||||||
|
mq:
|
||||||
|
type: messagebus
|
||||||
|
label: Message Queue
|
||||||
|
sublabel: System Software
|
||||||
|
tag: System Software
|
||||||
|
description: >
|
||||||
|
Queue manager between the app servers and the mainframe.
|
||||||
|
db-service:
|
||||||
|
type: cloud
|
||||||
|
label: DB Service
|
||||||
|
sublabel: Tech Service
|
||||||
|
tag: Tech Service
|
||||||
|
description: >
|
||||||
|
Managed SQL access with backups, offered to all applications.
|
||||||
|
dc-lan:
|
||||||
|
type: messagebus
|
||||||
|
label: Data Centre LAN
|
||||||
|
sublabel: Network
|
||||||
|
tag: Network
|
||||||
|
description: >
|
||||||
|
Internal network linking app servers, databases and mainframe.
|
||||||
|
dmz:
|
||||||
|
type: security
|
||||||
|
label: DMZ Gateway
|
||||||
|
sublabel: Network
|
||||||
|
tag: Network
|
||||||
|
description: >
|
||||||
|
Reverse proxy and firewall between the internet and the LAN.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Business actors and services
|
||||||
|
output: business-services.html
|
||||||
|
components:
|
||||||
|
- ref: shipper
|
||||||
|
pos: [40, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: recipient
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: delivery-service
|
||||||
|
pos: [250, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tracking-service
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: deliver-shipment
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: driver
|
||||||
|
pos: [670, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: customer-service
|
||||||
|
pos: [670, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: delivery-service, to: shipper, label: serves, variant: emphasis }
|
||||||
|
- { from: tracking-service, to: recipient, label: serves }
|
||||||
|
- { from: deliver-shipment, to: delivery-service, label: realizes }
|
||||||
|
- { from: deliver-shipment, to: tracking-service, label: realizes }
|
||||||
|
- { from: driver, to: deliver-shipment, label: assigned to }
|
||||||
|
- { from: customer-service, to: tracking-service, label: assigned to }
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
schema_version: 2
|
||||||
|
diagram_type: workflow
|
||||||
|
meta:
|
||||||
|
title: Deliver Shipment
|
||||||
|
output: deliver-shipment.html
|
||||||
|
viewBox: [840, 534]
|
||||||
|
lanes:
|
||||||
|
- { id: customer-lane, label: Shipper and recipient }
|
||||||
|
- { id: driver-lane, label: Driver }
|
||||||
|
- { id: hub-lane, label: Hub }
|
||||||
|
mainPath: [book, pick-up, sort, line-haul, last-mile, pod-step]
|
||||||
|
nodes:
|
||||||
|
- { id: book, lane: customer-lane, col: 0, type: external, label: Book shipment, tag: Business Event }
|
||||||
|
- { id: pick-up, lane: driver-lane, col: 1, type: backend, label: Pick up, tag: Business Process }
|
||||||
|
- { id: sort, lane: hub-lane, col: 2, type: backend, label: Sort at hub, tag: Business Process }
|
||||||
|
- { id: line-haul, lane: hub-lane, col: 3, type: backend, label: Line haul, tag: Business Process }
|
||||||
|
- { id: last-mile, lane: driver-lane, col: 4, type: backend, label: Last mile, tag: Business Process }
|
||||||
|
- { id: pod-step, lane: customer-lane, col: 5, type: security, label: Delivered, sublabel: proof of delivery, tag: Business Process }
|
||||||
|
edges:
|
||||||
|
- { from: book, to: pick-up, label: triggers }
|
||||||
|
- { from: pick-up, to: sort, label: parcel }
|
||||||
|
- { from: sort, to: line-haul, label: trailer }
|
||||||
|
- { from: line-haul, to: last-mile, label: route }
|
||||||
|
- { from: last-mile, to: pod-step, label: hand over }
|
||||||
|
- { from: last-mile, to: sort, label: not home, role: return, variant: dashed }
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Application cooperation
|
||||||
|
output: application-cooperation.html
|
||||||
|
decisions: [edi-gateway-in-front-of-tms]
|
||||||
|
components:
|
||||||
|
- ref: shipper
|
||||||
|
pos: [40, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: edi-gateway
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [edi-gateway-in-front-of-tms]
|
||||||
|
- ref: tms
|
||||||
|
pos: [460, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: driver-app
|
||||||
|
pos: [460, 60]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: telematics
|
||||||
|
pos: [460, 460]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tracking-portal
|
||||||
|
pos: [670, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: shipper, to: edi-gateway, label: bookings, variant: emphasis }
|
||||||
|
- { from: edi-gateway, to: tms, label: shipments, variant: emphasis }
|
||||||
|
- { from: driver-app, to: tms, label: scans, labelAt: [525, 134], decisions: [broker-for-tracking-events] }
|
||||||
|
- { from: telematics, to: tms, label: positions, decisions: [broker-for-tracking-events] }
|
||||||
|
- { from: tms, to: tracking-portal, label: status }
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Application services and data
|
||||||
|
output: application-services.html
|
||||||
|
components:
|
||||||
|
- ref: tracking-portal
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tracking-svc
|
||||||
|
pos: [250, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tms
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: driver-app
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: shipment
|
||||||
|
pos: [250, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: tracking-event
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: pod
|
||||||
|
pos: [670, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
connections:
|
||||||
|
- { from: tracking-svc, to: tracking-portal, label: serves, variant: emphasis }
|
||||||
|
- { from: tms, to: tracking-svc, label: realizes }
|
||||||
|
- { from: driver-app, to: tms, label: uses }
|
||||||
|
- { from: tms, to: shipment, label: accesses }
|
||||||
|
- { from: tms, to: tracking-event, label: accesses, labelAt: [525, 234] }
|
||||||
|
- { from: driver-app, to: pod, label: accesses, labelAt: [735, 234] }
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
schema_version: 1
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Technology infrastructure
|
||||||
|
output: infrastructure.html
|
||||||
|
decisions: [broker-for-tracking-events]
|
||||||
|
components:
|
||||||
|
- ref: handhelds
|
||||||
|
pos: [40, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: telematics-units
|
||||||
|
pos: [40, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: mobile-network
|
||||||
|
pos: [250, 260]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: cloud-cluster
|
||||||
|
pos: [460, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: message-broker
|
||||||
|
pos: [460, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
decisions: [broker-for-tracking-events]
|
||||||
|
- ref: db-server
|
||||||
|
pos: [670, 160]
|
||||||
|
size: [130, 60]
|
||||||
|
- ref: event-streaming
|
||||||
|
pos: [670, 360]
|
||||||
|
size: [130, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Field devices
|
||||||
|
wraps: [handhelds, telematics-units]
|
||||||
|
- kind: security-group
|
||||||
|
label: Cloud platform
|
||||||
|
wraps: [cloud-cluster, message-broker, db-server, event-streaming]
|
||||||
|
connections:
|
||||||
|
- { from: handhelds, to: mobile-network, label: LTE }
|
||||||
|
- { from: telematics-units, to: mobile-network, label: LTE }
|
||||||
|
- { from: mobile-network, to: cloud-cluster, label: HTTPS, variant: security }
|
||||||
|
- { from: mobile-network, to: message-broker, label: MQTT, variant: security }
|
||||||
|
- { from: cloud-cluster, to: message-broker, label: hosts, labelAt: [525, 234] }
|
||||||
|
- { from: cloud-cluster, to: db-server, label: SQL }
|
||||||
|
- { from: message-broker, to: event-streaming, label: realizes }
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: broker-for-tracking-events
|
||||||
|
title: A message broker for tracking events
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-08
|
||||||
|
elements: [message-broker, telematics, driver-app, tms, tracking-event]
|
||||||
|
diagrams: [0300-technology/infrastructure.archify, 0200-application/application-cooperation.archify]
|
||||||
|
context: >
|
||||||
|
Handhelds and telematics units posted every scan and position
|
||||||
|
straight to the TMS. At evening peaks the TMS fell behind and
|
||||||
|
dropped events when a device retried over a weak signal.
|
||||||
|
decision: >
|
||||||
|
Devices publish tracking events to a Kafka message broker on
|
||||||
|
the cloud cluster. The TMS and telematics read the topics at
|
||||||
|
their own pace; events are kept for seven days for replay.
|
||||||
|
consequences: >
|
||||||
|
Spikes no longer reach the TMS. Status on the tracking portal
|
||||||
|
can lag by seconds. Consumers must handle duplicate events,
|
||||||
|
and the broker is one more platform service to run.
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: edi-gateway-in-front-of-tms
|
||||||
|
title: An EDI gateway in front of the TMS
|
||||||
|
status: accepted
|
||||||
|
date: 2026-09-22
|
||||||
|
elements: [edi-gateway, tms, shipper, shipment]
|
||||||
|
diagrams: [0200-application/application-cooperation.archify]
|
||||||
|
context: >
|
||||||
|
Large shippers send bookings as EDI files in three dialects.
|
||||||
|
Each dialect was parsed inside the TMS, so a new shipper meant
|
||||||
|
a TMS release.
|
||||||
|
decision: >
|
||||||
|
A separate EDI gateway receives, validates and translates
|
||||||
|
every booking into one shipment message for the TMS. It also
|
||||||
|
sends status messages back in the shipper's dialect.
|
||||||
|
consequences: >
|
||||||
|
New shippers are onboarded in the gateway alone. The TMS has
|
||||||
|
one booking interface. A rejected booking now fails at the
|
||||||
|
gateway, which customer service must be able to see.
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
schema_version: 1
|
||||||
|
decision:
|
||||||
|
id: photo-proof-of-delivery
|
||||||
|
title: Photo as proof of delivery
|
||||||
|
status: proposed
|
||||||
|
date: 2026-10-27
|
||||||
|
elements: [pod, driver-app, handhelds]
|
||||||
|
diagrams: [0200-application/application-services.archify, 0100-business/deliver-shipment.archify]
|
||||||
|
context: >
|
||||||
|
Most parcels are left at the door when nobody signs. Disputes
|
||||||
|
about such parcels take customer service days to settle.
|
||||||
|
decision: >
|
||||||
|
The driver app takes a photo of every parcel left without a
|
||||||
|
signature and stores it with the proof of delivery.
|
||||||
|
consequences: >
|
||||||
|
Disputes are settled from the photo. Photos are personal data
|
||||||
|
and are deleted after 90 days. Uploads add mobile data cost.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
schema_version: 1
|
||||||
|
model:
|
||||||
|
id: logistics-tracking
|
||||||
|
name: Logistics Tracking
|
||||||
|
description: >
|
||||||
|
An ArchiMate example: a parcel carrier lets shippers and recipients
|
||||||
|
track shipments. Business, application and technology layers are
|
||||||
|
mapped to folders; each element's ArchiMate type is shown as its tag.
|
||||||
|
diagram_type: architecture
|
||||||
|
meta:
|
||||||
|
title: Logistics Tracking - layered view
|
||||||
|
output: main.html
|
||||||
|
components:
|
||||||
|
- ref: recipient
|
||||||
|
pos: [40, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: tracking-service
|
||||||
|
pos: [270, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: deliver-shipment
|
||||||
|
pos: [500, 70]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: tracking-portal
|
||||||
|
pos: [40, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: tms
|
||||||
|
pos: [270, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: tracking-svc
|
||||||
|
pos: [500, 270]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: mobile-network
|
||||||
|
pos: [40, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: cloud-cluster
|
||||||
|
pos: [270, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
- ref: event-streaming
|
||||||
|
pos: [500, 470]
|
||||||
|
size: [170, 60]
|
||||||
|
boundaries:
|
||||||
|
- kind: region
|
||||||
|
label: Business layer
|
||||||
|
wraps: [recipient, tracking-service, deliver-shipment]
|
||||||
|
- kind: region
|
||||||
|
label: Application layer
|
||||||
|
wraps: [tracking-portal, tms, tracking-svc]
|
||||||
|
- kind: region
|
||||||
|
label: Technology layer
|
||||||
|
wraps: [mobile-network, cloud-cluster, event-streaming]
|
||||||
|
connections:
|
||||||
|
- { from: tracking-service, to: recipient, label: serves, variant: emphasis }
|
||||||
|
- { from: deliver-shipment, to: tracking-service, label: realizes }
|
||||||
|
- { from: tracking-portal, to: recipient, label: serves }
|
||||||
|
- { from: tracking-svc, to: deliver-shipment, label: serves }
|
||||||
|
- { from: tms, to: tracking-svc, label: realizes }
|
||||||
|
- { from: mobile-network, to: tracking-portal, label: serves }
|
||||||
|
- { from: cloud-cluster, to: tms, label: serves }
|
||||||
|
- { from: event-streaming, to: tracking-svc, label: serves }
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
tms:
|
||||||
|
type: backend
|
||||||
|
label: TMS
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Owns shipments, routes and hub plans; the source of truth for status.
|
||||||
|
diagram: 0200-application/application-cooperation.archify
|
||||||
|
tracking-portal:
|
||||||
|
type: frontend
|
||||||
|
label: Tracking portal
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Public site where shippers and recipients follow a shipment.
|
||||||
|
edi-gateway:
|
||||||
|
type: security
|
||||||
|
label: EDI gateway
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Receives booking messages from large shippers and returns status.
|
||||||
|
driver-app:
|
||||||
|
type: frontend
|
||||||
|
label: Driver app
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Scans parcels at every hand-over and captures proof of delivery.
|
||||||
|
telematics:
|
||||||
|
type: backend
|
||||||
|
label: Telematics
|
||||||
|
sublabel: App Component
|
||||||
|
tag: App Component
|
||||||
|
description: >
|
||||||
|
Collects vehicle positions and turns them into arrival estimates.
|
||||||
|
tracking-svc:
|
||||||
|
type: backend
|
||||||
|
label: Tracking service
|
||||||
|
sublabel: App Service
|
||||||
|
tag: App Service
|
||||||
|
description: >
|
||||||
|
Status and arrival estimate of one shipment, offered by the TMS.
|
||||||
|
diagram: 0200-application/application-services.archify
|
||||||
|
shipment:
|
||||||
|
type: database
|
||||||
|
label: Shipment
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
One booked parcel with sender, recipient, route and status.
|
||||||
|
tracking-event:
|
||||||
|
type: database
|
||||||
|
label: Tracking event
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
A scan or position report; the history of a shipment.
|
||||||
|
pod:
|
||||||
|
type: database
|
||||||
|
label: Proof of delivery
|
||||||
|
sublabel: Data Object
|
||||||
|
tag: Data Object
|
||||||
|
description: >
|
||||||
|
Signature or photo taken at the door; closes the shipment.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
shipper:
|
||||||
|
type: external
|
||||||
|
label: Shipper
|
||||||
|
sublabel: Business Actor
|
||||||
|
tag: Business Actor
|
||||||
|
description: >
|
||||||
|
Books shipments, hands parcels over at pick-up and follows
|
||||||
|
them until delivery.
|
||||||
|
recipient:
|
||||||
|
type: external
|
||||||
|
label: Recipient
|
||||||
|
sublabel: Business Actor
|
||||||
|
tag: Business Actor
|
||||||
|
description: >
|
||||||
|
Receives the parcel; tracks it and may reschedule delivery.
|
||||||
|
driver:
|
||||||
|
type: external
|
||||||
|
label: Driver
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
Collects parcels, drives the routes and captures proof of delivery.
|
||||||
|
customer-service:
|
||||||
|
type: external
|
||||||
|
label: Customer service
|
||||||
|
sublabel: Business Role
|
||||||
|
tag: Business Role
|
||||||
|
description: >
|
||||||
|
Answers shipper and recipient questions about late or lost parcels.
|
||||||
|
deliver-shipment:
|
||||||
|
type: backend
|
||||||
|
label: Deliver Shipment
|
||||||
|
sublabel: Business Process
|
||||||
|
tag: Business Process
|
||||||
|
description: >
|
||||||
|
Pick up, sort at hub, line haul, last mile and proof of delivery.
|
||||||
|
diagram: 0100-business/deliver-shipment.archify
|
||||||
|
delivery-service:
|
||||||
|
type: backend
|
||||||
|
label: Parcel delivery
|
||||||
|
sublabel: Business Service
|
||||||
|
tag: Business Service
|
||||||
|
description: >
|
||||||
|
What the carrier sells; a parcel collected and delivered on time.
|
||||||
|
diagram: 0100-business/business-services.archify
|
||||||
|
tracking-service:
|
||||||
|
type: backend
|
||||||
|
label: Shipment tracking
|
||||||
|
sublabel: Business Service
|
||||||
|
tag: Business Service
|
||||||
|
description: >
|
||||||
|
Where a parcel is and when it arrives, for shippers and recipients.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
schema_version: 1
|
||||||
|
elements:
|
||||||
|
cloud-cluster:
|
||||||
|
type: cloud
|
||||||
|
label: Cloud cluster
|
||||||
|
sublabel: Node
|
||||||
|
brand: kubernetes
|
||||||
|
tag: Node
|
||||||
|
description: >
|
||||||
|
Container cluster that runs the TMS, portal, EDI gateway and telematics.
|
||||||
|
diagram: 0300-technology/infrastructure.archify
|
||||||
|
message-broker:
|
||||||
|
type: messagebus
|
||||||
|
label: Message broker
|
||||||
|
sublabel: System Software
|
||||||
|
brand: apache-kafka
|
||||||
|
tag: System Software
|
||||||
|
description: >
|
||||||
|
Carries tracking events from devices and vehicles to the applications.
|
||||||
|
db-server:
|
||||||
|
type: database
|
||||||
|
label: Database server
|
||||||
|
sublabel: System Software
|
||||||
|
brand: postgresql
|
||||||
|
tag: System Software
|
||||||
|
description: >
|
||||||
|
Stores shipments, tracking events and proofs of delivery.
|
||||||
|
event-streaming:
|
||||||
|
type: backend
|
||||||
|
label: Event streaming
|
||||||
|
sublabel: Tech Service
|
||||||
|
tag: Tech Service
|
||||||
|
description: >
|
||||||
|
Durable, ordered topics that applications publish to and read from.
|
||||||
|
handhelds:
|
||||||
|
type: external
|
||||||
|
label: Handhelds
|
||||||
|
sublabel: Device
|
||||||
|
tag: Device
|
||||||
|
description: >
|
||||||
|
Scanners carried by drivers and hub staff; run the driver app.
|
||||||
|
telematics-units:
|
||||||
|
type: external
|
||||||
|
label: Telematics units
|
||||||
|
sublabel: Device
|
||||||
|
tag: Device
|
||||||
|
description: >
|
||||||
|
GPS boxes in vans and trucks; report position every minute.
|
||||||
|
mobile-network:
|
||||||
|
type: security
|
||||||
|
label: Mobile network
|
||||||
|
sublabel: Network
|
||||||
|
tag: Network
|
||||||
|
description: >
|
||||||
|
Carrier-grade mobile network that links devices to the cluster.
|
||||||
+17
-9
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import { parse } from 'yaml';
|
import { parse } from 'yaml';
|
||||||
import { validate } from '../src/archify/cli';
|
import { validate } from '../src/archify/cli';
|
||||||
import { MODEL_EXAMPLES, mainFromExample } from '../src/language/examples';
|
import { EXAMPLE_PREFIX, MODEL_EXAMPLES, mainFromExample } from '../src/language/examples';
|
||||||
import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format';
|
import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format';
|
||||||
import { DECISION_EXT, mergeDecisions, parseDecision, validateDecisions } from '../src/model/decisions';
|
import { DECISION_EXT, mergeDecisions, parseDecision, validateDecisions } from '../src/model/decisions';
|
||||||
import { readDiagramSource } from '../src/sources/detect';
|
import { readDiagramSource } from '../src/sources/detect';
|
||||||
@@ -26,13 +26,21 @@ function walk(dir: string, rel = ''): string[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('every example folder is listed, every listed example exists, labels are the three C4 examples', () => {
|
// The folders each framework's examples must have, besides main.archify, registry/ and decisions/.
|
||||||
|
const FRAMEWORK_FOLDERS: Record<string, string[]> = {
|
||||||
|
c4: ['0100-context', '0200-containers', '0300-components'],
|
||||||
|
arc42: ['0300-context', '0500-building-blocks', '0600-runtime', '0700-deployment'],
|
||||||
|
archimate: ['0100-business', '0200-application', '0300-technology'],
|
||||||
|
};
|
||||||
|
|
||||||
|
test('every example folder is listed, every listed example exists, three examples per framework', () => {
|
||||||
const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
||||||
assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders);
|
assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders);
|
||||||
assert.deepEqual(
|
for (const [framework, label] of [['c4', 'C4'], ['arc42', 'arc42'], ['archimate', 'ArchiMate']]) {
|
||||||
MODEL_EXAMPLES.map((e) => e.label),
|
const listed = MODEL_EXAMPLES.filter((e) => e.folder.startsWith(`${framework}-`));
|
||||||
['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration'],
|
assert.equal(listed.length, 3, framework);
|
||||||
);
|
for (const e of listed) assert.ok(e.label.startsWith(`${label} - `), e.label);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const example of MODEL_EXAMPLES) {
|
for (const example of MODEL_EXAMPLES) {
|
||||||
@@ -45,7 +53,7 @@ for (const example of MODEL_EXAMPLES) {
|
|||||||
assert.ok(files.includes(MAIN_FILE));
|
assert.ok(files.includes(MAIN_FILE));
|
||||||
const main = parse(read(MAIN_FILE));
|
const main = parse(read(MAIN_FILE));
|
||||||
const meta = readModelMeta(main);
|
const meta = readModelMeta(main);
|
||||||
assert.equal(meta?.id, example.folder.replace(/^c4-/, ''));
|
assert.equal(meta?.id, example.folder.replace(EXAMPLE_PREFIX, ''));
|
||||||
assert.ok(meta?.name && meta.description);
|
assert.ok(meta?.name && meta.description);
|
||||||
assert.ok(hasDiagramBody(main));
|
assert.ok(hasDiagramBody(main));
|
||||||
|
|
||||||
@@ -68,9 +76,9 @@ for (const example of MODEL_EXAMPLES) {
|
|||||||
assert.deepEqual(validateDecisions(decisions, registry, diagramPaths), []);
|
assert.deepEqual(validateDecisions(decisions, registry, diagramPaths), []);
|
||||||
for (const d of decisions.decisions.values()) assert.ok(d.title && d.status && d.context && d.decision && d.consequences, `${d.id} is complete`);
|
for (const d of decisions.decisions.values()) assert.ok(d.title && d.status && d.context && d.decision && d.consequences, `${d.id} is complete`);
|
||||||
|
|
||||||
// Diagrams: the three C4 levels, each resolving without problems and valid for Archify.
|
// Diagrams: the framework's folders, each diagram resolving without problems and valid for Archify.
|
||||||
const diagrams = files.filter((f) => f.endsWith('.archify')).sort();
|
const diagrams = files.filter((f) => f.endsWith('.archify')).sort();
|
||||||
for (const level of ['0100-context', '0200-containers', '0300-components']) {
|
for (const level of FRAMEWORK_FOLDERS[example.folder.split('-')[0]]) {
|
||||||
assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`);
|
assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`);
|
||||||
}
|
}
|
||||||
const used = new Set<string>();
|
const used = new Set<string>();
|
||||||
|
|||||||
@@ -449,7 +449,11 @@ export async function run(): Promise<void> {
|
|||||||
window.showQuickPick = originalPick;
|
window.showQuickPick = originalPick;
|
||||||
window.showInputBox = originalInput;
|
window.showInputBox = originalInput;
|
||||||
}
|
}
|
||||||
assert.deepEqual(labels, ['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration']);
|
assert.deepEqual(labels, [
|
||||||
|
'C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration',
|
||||||
|
'arc42 - Cinema Tickets', 'arc42 - IoT Telemetry', 'arc42 - Team Wiki',
|
||||||
|
'ArchiMate - Insurance Claims', 'ArchiMate - HR Onboarding', 'ArchiMate - Logistics Tracking',
|
||||||
|
]);
|
||||||
assert.match(String(existingNameMessage), /already exists/);
|
assert.match(String(existingNameMessage), /already exists/);
|
||||||
const folder = vscode.Uri.joinPath(parent, 'shop-two');
|
const folder = vscode.Uri.joinPath(parent, 'shop-two');
|
||||||
const main = vscode.Uri.joinPath(folder, 'main.archify');
|
const main = vscode.Uri.joinPath(folder, 'main.archify');
|
||||||
@@ -493,8 +497,6 @@ export async function run(): Promise<void> {
|
|||||||
// No server of anyone's built in: empty until the user sets one.
|
// No server of anyone's built in: empty until the user sets one.
|
||||||
const config = vscode.workspace.getConfiguration('archify.sync');
|
const config = vscode.workspace.getConfiguration('archify.sync');
|
||||||
assert.equal(config.get('server'), '');
|
assert.equal(config.get('server'), '');
|
||||||
assert.equal(config.get('statusBar'), true);
|
|
||||||
assert.equal(config.get('titleButton'), true);
|
|
||||||
// The extension host's module cache: dist/sync.js (Yjs, the WebSocket) is not loaded.
|
// The extension host's module cache: dist/sync.js (Yjs, the WebSocket) is not loaded.
|
||||||
const loaded = Object.keys(require.cache).filter((file) => file.endsWith(`${path.sep}dist${path.sep}sync.js`));
|
const loaded = Object.keys(require.cache).filter((file) => file.endsWith(`${path.sep}dist${path.sep}sync.js`));
|
||||||
assert.deepEqual(loaded, [], 'the sync bundle is not loaded');
|
assert.deepEqual(loaded, [], 'the sync bundle is not loaded');
|
||||||
|
|||||||
Reference in new issue
Block a user